acpilot 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +257 -258
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -44061,12 +44061,81 @@ var undici = __toESM(require_undici(), 1);
44061
44061
  var import_whatwg_mimetype = __toESM(require_mime_type(), 1);
44062
44062
 
44063
44063
  // src/cli.ts
44064
+ var readline = __toESM(require("readline"));
44065
+ var RESET = "\x1B[0m";
44066
+ var BOLD = "\x1B[1m";
44067
+ var DIM = "\x1B[2m";
44068
+ var ITALIC = "\x1B[3m";
44069
+ var GREEN = "\x1B[32m";
44070
+ var RED = "\x1B[31m";
44071
+ var YELLOW = "\x1B[33m";
44072
+ var CYAN = "\x1B[36m";
44073
+ var BLUE = "\x1B[34m";
44074
+ var WHITE = "\x1B[37m";
44075
+ var GRAY = "\x1B[90m";
44076
+ var BRIGHT_WHITE = "\x1B[97m";
44077
+ function scoreColor(score) {
44078
+ if (score >= 90) return GREEN;
44079
+ if (score >= 70) return YELLOW;
44080
+ return RED;
44081
+ }
44082
+ function printLogo() {
44083
+ console.log("");
44084
+ console.log(`${BLUE} \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557${RESET}`);
44085
+ console.log(`${BLUE} \u2551${RESET} ${BLUE}\u2551${RESET}`);
44086
+ console.log(`${BLUE} \u2551${RESET} ${BOLD}${CYAN} \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 ${WHITE}\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 ${CYAN} ${RESET}${BLUE}\u2551${RESET}`);
44087
+ console.log(`${BLUE} \u2551${RESET} ${BOLD}${CYAN}\u2588\u2588 \u2588\u2588 \u2588\u2588 ${WHITE}\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 ${CYAN} ${RESET}${BLUE}\u2551${RESET}`);
44088
+ console.log(`${BLUE} \u2551${RESET} ${BOLD}${CYAN}\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 ${WHITE}\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 ${CYAN} ${RESET}${BLUE}\u2551${RESET}`);
44089
+ console.log(`${BLUE} \u2551${RESET} ${BOLD}${CYAN}\u2588\u2588 \u2588\u2588 \u2588\u2588 ${WHITE}\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 ${CYAN} ${RESET}${BLUE}\u2551${RESET}`);
44090
+ console.log(`${BLUE} \u2551${RESET} ${BOLD}${CYAN}\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588${WHITE}\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 ${CYAN} ${RESET}${BLUE}\u2551${RESET}`);
44091
+ console.log(`${BLUE} \u2551${RESET} ${BLUE}\u2551${RESET}`);
44092
+ console.log(`${BLUE} \u2551${RESET} ${DIM}${ITALIC} Barrierefreiheit f\xFCr Entwickler${RESET} ${DIM}v1.0.1${RESET} ${BLUE}\u2551${RESET}`);
44093
+ console.log(`${BLUE} \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D${RESET}`);
44094
+ console.log("");
44095
+ }
44096
+ function printDivider(label) {
44097
+ if (label) {
44098
+ console.log(`
44099
+ ${GRAY}\u2500\u2500${RESET} ${BOLD}${label}${RESET} ${GRAY}${"\u2500".repeat(Math.max(0, 42 - label.length))}${RESET}
44100
+ `);
44101
+ } else {
44102
+ console.log(` ${GRAY}${"\u2500".repeat(48)}${RESET}`);
44103
+ }
44104
+ }
44105
+ function createRL() {
44106
+ return readline.createInterface({
44107
+ input: process.stdin,
44108
+ output: process.stdout
44109
+ });
44110
+ }
44111
+ function ask(rl, question, defaultValue) {
44112
+ const suffix = defaultValue ? ` ${DIM}(${defaultValue})${RESET}` : "";
44113
+ return new Promise((resolve) => {
44114
+ rl.question(` ${CYAN}\u203A${RESET} ${question}${suffix}: `, (answer) => {
44115
+ resolve(answer.trim() || defaultValue || "");
44116
+ });
44117
+ });
44118
+ }
44119
+ function askChoice(rl, question, choices) {
44120
+ console.log(` ${CYAN}\u203A${RESET} ${question}
44121
+ `);
44122
+ for (const c of choices) {
44123
+ console.log(` ${BOLD}${CYAN}${c.key}${RESET} ${c.label} ${DIM}${c.desc}${RESET}`);
44124
+ }
44125
+ console.log("");
44126
+ return new Promise((resolve) => {
44127
+ rl.question(` ${CYAN}\u203A${RESET} Auswahl: `, (answer) => {
44128
+ resolve(answer.trim() || choices[0].key);
44129
+ });
44130
+ });
44131
+ }
44064
44132
  function parseArgs() {
44065
44133
  const args = process.argv.slice(2);
44066
44134
  const opts = {};
44067
44135
  const flags = /* @__PURE__ */ new Set();
44068
44136
  for (let i = 0; i < args.length; i++) {
44069
44137
  if (args[i] === "--help" || args[i] === "-h") {
44138
+ printLogo();
44070
44139
  printHelp();
44071
44140
  process.exit(0);
44072
44141
  }
@@ -44078,47 +44147,36 @@ function parseArgs() {
44078
44147
  opts[args[i]] = args[++i];
44079
44148
  }
44080
44149
  }
44081
- const url = opts["--url"];
44150
+ const url = opts["--url"] || "";
44082
44151
  const apiKey = opts["--api-key"] || process.env.ACCESSPILOT_API_KEY || "";
44083
- if (!url) {
44084
- console.error("Fehler: --url ist erforderlich\n");
44085
- printHelp();
44086
- process.exit(1);
44087
- }
44088
- if (!apiKey) {
44089
- console.error("Fehler: --api-key ist erforderlich (oder ACCESSPILOT_API_KEY Umgebungsvariable)\n");
44090
- printHelp();
44091
- process.exit(1);
44092
- }
44152
+ const interactive = !url;
44093
44153
  return {
44094
44154
  url,
44095
44155
  apiKey,
44096
44156
  maxPages: parseInt(opts["--max-pages"] || "50", 10),
44097
44157
  maxDepth: parseInt(opts["--max-depth"] || "3", 10),
44098
- server: opts["--server"] || "https://www.accesspilot.de",
44158
+ server: opts["--server"] || "https://acpilot.de",
44099
44159
  projectId: opts["--project"],
44100
44160
  json: flags.has("--json"),
44101
- noCrawl: flags.has("--no-crawl")
44161
+ noCrawl: flags.has("--no-crawl"),
44162
+ interactive
44102
44163
  };
44103
44164
  }
44104
44165
  function printHelp() {
44105
- console.log(`
44106
- acpilot \u2014 Lokalen Dev-Server auf Barrierefreiheit scannen
44107
-
44108
- Verwendung:
44109
- npx acpilot --url http://localhost:3000 --api-key AP_xxx
44110
-
44111
- Optionen:
44112
- --url <url> URL des lokalen Dev-Servers (erforderlich)
44113
- --api-key <key> AccessPilot API-Key (oder ACCESSPILOT_API_KEY env)
44114
- --max-pages <n> Maximale Seitenanzahl (Standard: 50)
44115
- --max-depth <n> Maximale Crawl-Tiefe (Standard: 3)
44116
- --project <id> Projekt-ID in AccessPilot (optional)
44117
- --server <url> AccessPilot Server URL (Standard: https://www.accesspilot.de)
44118
- --no-crawl Nur die angegebene URL scannen (kein Crawling)
44119
- --json JSON-Ausgabe statt Pretty-Print
44120
- -h, --help Hilfe anzeigen
44121
- `);
44166
+ console.log(` ${BOLD}Verwendung:${RESET}`);
44167
+ console.log(` ${GREEN}npx acpilot${RESET} ${DIM}Interaktiver Modus${RESET}`);
44168
+ console.log(` ${GREEN}npx acpilot${RESET} --url http://localhost:3000 ${DIM}Direkter Modus${RESET}`);
44169
+ console.log(` --api-key AP_xxx`);
44170
+ console.log("");
44171
+ console.log(` ${BOLD}Optionen:${RESET}`);
44172
+ console.log(` ${CYAN}--url${RESET} <url> URL des Dev-Servers`);
44173
+ console.log(` ${CYAN}--api-key${RESET} <key> API-Key ${DIM}(oder ACCESSPILOT_API_KEY env)${RESET}`);
44174
+ console.log(` ${CYAN}--max-pages${RESET} <n> Max. Seiten ${DIM}(Standard: 50)${RESET}`);
44175
+ console.log(` ${CYAN}--max-depth${RESET} <n> Crawl-Tiefe ${DIM}(Standard: 3)${RESET}`);
44176
+ console.log(` ${CYAN}--project${RESET} <id> Projekt-ID ${DIM}(optional)${RESET}`);
44177
+ console.log(` ${CYAN}--no-crawl${RESET} Nur eine URL scannen`);
44178
+ console.log(` ${CYAN}--json${RESET} JSON-Ausgabe`);
44179
+ console.log("");
44122
44180
  }
44123
44181
  var SEVERITY_PENALTIES = { critical: 10, serious: 5, moderate: 2, minor: 1 };
44124
44182
  function calculateScore(findings) {
@@ -44164,155 +44222,64 @@ function runRules(html3, url, $2) {
44164
44222
  $2("img").each((_, el) => {
44165
44223
  const alt = $2(el).attr("alt");
44166
44224
  if (alt === void 0) {
44167
- addFinding("image-alt", "critical", "Bild ohne alt-Attribut", $2.html(el) ?? "", $2(el).prop("tagName") ?? "img", "1.1.1", "F\xFCgen Sie ein alt-Attribut hinzu");
44225
+ addFinding("image-alt", "critical", "Bild ohne alt-Attribut", $2.html(el) ?? "", "img", "1.1.1", "F\xFCgen Sie ein alt-Attribut hinzu");
44168
44226
  } else if (alt === "" && !$2(el).attr("role") && !$2(el).closest("a, button").length) {
44169
44227
  addFinding("image-alt-empty", "moderate", "Bild mit leerem alt-Attribut (nicht dekorativ markiert)", $2.html(el) ?? "", "img", "1.1.1", 'Markieren Sie das Bild mit role="presentation" oder f\xFCgen Sie einen alt-Text hinzu');
44170
44228
  }
44171
44229
  });
44172
- const htmlLang = $2("html").attr("lang");
44173
- if (!htmlLang) {
44230
+ if (!$2("html").attr("lang")) {
44174
44231
  addFinding("html-lang", "serious", "HTML-Element hat kein lang-Attribut", "<html>", "html", "3.1.1", 'F\xFCgen Sie lang="de" zum <html>-Element hinzu');
44175
44232
  }
44176
- const title = $2("title").text().trim();
44177
- if (!title) {
44178
- addFinding("page-title", "serious", "Seite hat keinen Titel (<title>)", "<head>", "title", "2.4.2", "F\xFCgen Sie einen aussagekr\xE4ftigen <title> hinzu");
44233
+ if (!$2("title").text().trim()) {
44234
+ addFinding("page-title", "serious", "Seite hat keinen Titel", "<head>", "title", "2.4.2", "F\xFCgen Sie einen aussagekr\xE4ftigen <title> hinzu");
44179
44235
  }
44180
- const headingLevels = [];
44236
+ const levels = [];
44181
44237
  $2("h1, h2, h3, h4, h5, h6").each((_, el) => {
44182
- const tag = $2(el).prop("tagName")?.toLowerCase() ?? "";
44183
- headingLevels.push(parseInt(tag.replace("h", ""), 10));
44238
+ levels.push(parseInt($2(el).prop("tagName")?.toLowerCase().replace("h", "") ?? "0", 10));
44184
44239
  });
44185
- if (headingLevels.length === 0) {
44186
- addFinding("heading-missing", "serious", "Keine \xDCberschriften gefunden", "", "", "1.3.1", "Verwenden Sie \xDCberschriften (h1-h6) um die Seite zu strukturieren");
44240
+ if (levels.length === 0) {
44241
+ addFinding("heading-missing", "serious", "Keine \xDCberschriften gefunden", "", "", "1.3.1", "Verwenden Sie h1-h6 \xDCberschriften");
44187
44242
  } else {
44188
- if (headingLevels[0] !== 1) {
44189
- addFinding("heading-order", "moderate", `Erste \xDCberschrift ist h${headingLevels[0]} statt h1`, "", "", "1.3.1", "Beginnen Sie mit einer h1-\xDCberschrift");
44190
- }
44191
- for (let i = 1; i < headingLevels.length; i++) {
44192
- if (headingLevels[i] > headingLevels[i - 1] + 1) {
44193
- addFinding("heading-skip", "moderate", `\xDCberschriftenebene \xFCbersprungen: h${headingLevels[i - 1]} \u2192 h${headingLevels[i]}`, "", "", "1.3.1", "\xDCberspringen Sie keine \xDCberschriftenebenen");
44243
+ if (levels[0] !== 1) addFinding("heading-order", "moderate", `Erste \xDCberschrift ist h${levels[0]} statt h1`, "", "", "1.3.1", "Beginnen Sie mit h1");
44244
+ for (let i = 1; i < levels.length; i++) {
44245
+ if (levels[i] > levels[i - 1] + 1) {
44246
+ addFinding("heading-skip", "moderate", `\xDCberschriftenebene \xFCbersprungen: h${levels[i - 1]} \u2192 h${levels[i]}`, "", "", "1.3.1", "Keine Ebenen \xFCberspringen");
44194
44247
  break;
44195
44248
  }
44196
44249
  }
44197
44250
  }
44198
44251
  $2("a").each((_, el) => {
44199
44252
  const text3 = $2(el).text().trim();
44200
- const ariaLabel = $2(el).attr("aria-label")?.trim();
44201
- const title2 = $2(el).attr("title")?.trim();
44202
- const img = $2(el).find("img[alt]");
44203
- if (!text3 && !ariaLabel && !title2 && img.length === 0) {
44204
- addFinding("link-text", "serious", "Link ohne erkennbaren Text", $2.html(el)?.slice(0, 200) ?? "", "a", "2.4.4", "F\xFCgen Sie einen Link-Text oder aria-label hinzu");
44253
+ if (!text3 && !$2(el).attr("aria-label")?.trim() && !$2(el).attr("title")?.trim() && !$2(el).find("img[alt]").length) {
44254
+ addFinding("link-text", "serious", "Link ohne erkennbaren Text", $2.html(el)?.slice(0, 200) ?? "", "a", "2.4.4", "Link-Text oder aria-label hinzuf\xFCgen");
44205
44255
  }
44206
44256
  });
44207
44257
  $2("input, select, textarea").each((_, el) => {
44208
44258
  const type = $2(el).attr("type") || "text";
44209
44259
  if (["hidden", "submit", "button", "reset", "image"].includes(type)) return;
44210
44260
  const id = $2(el).attr("id");
44211
- const ariaLabel = $2(el).attr("aria-label");
44212
- const ariaLabelledBy = $2(el).attr("aria-labelledby");
44213
44261
  const hasLabel = id ? $2(`label[for="${id}"]`).length > 0 : false;
44214
- const wrappedInLabel = $2(el).closest("label").length > 0;
44215
- if (!hasLabel && !wrappedInLabel && !ariaLabel && !ariaLabelledBy) {
44216
- addFinding("form-label", "critical", "Formularelement ohne zugeh\xF6riges Label", $2.html(el)?.slice(0, 200) ?? "", "input", "1.3.1", "Verkn\xFCpfen Sie das Element mit einem <label> oder aria-label");
44262
+ if (!hasLabel && !$2(el).closest("label").length && !$2(el).attr("aria-label") && !$2(el).attr("aria-labelledby")) {
44263
+ addFinding("form-label", "critical", "Formularelement ohne Label", $2.html(el)?.slice(0, 200) ?? "", "input", "1.3.1", "Label oder aria-label verkn\xFCpfen");
44217
44264
  }
44218
44265
  });
44219
- const hasMain = $2('main, [role="main"]').length > 0;
44220
- const hasNav = $2('nav, [role="navigation"]').length > 0;
44221
- if (!hasMain) {
44222
- addFinding("landmark-main", "moderate", "Kein <main>-Landmark gefunden", "", "", "1.3.1", "Verwenden Sie ein <main>-Element f\xFCr den Hauptinhalt");
44223
- }
44224
- if (!hasNav) {
44225
- addFinding("landmark-nav", "minor", "Kein <nav>-Landmark gefunden", "", "", "1.3.1", "Verwenden Sie ein <nav>-Element f\xFCr die Navigation");
44226
- }
44266
+ if (!$2('main, [role="main"]').length) addFinding("landmark-main", "moderate", "Kein <main>-Landmark", "", "", "1.3.1", "<main>-Element verwenden");
44267
+ if (!$2('nav, [role="navigation"]').length) addFinding("landmark-nav", "minor", "Kein <nav>-Landmark", "", "", "1.3.1", "<nav>-Element verwenden");
44227
44268
  $2('button, [role="button"]').each((_, el) => {
44228
- const text3 = $2(el).text().trim();
44229
- const ariaLabel = $2(el).attr("aria-label")?.trim();
44230
- const title2 = $2(el).attr("title")?.trim();
44231
- if (!text3 && !ariaLabel && !title2) {
44232
- addFinding("button-text", "critical", "Button ohne erkennbaren Text", $2.html(el)?.slice(0, 200) ?? "", "button", "4.1.2", "F\xFCgen Sie Text oder aria-label zum Button hinzu");
44269
+ if (!$2(el).text().trim() && !$2(el).attr("aria-label")?.trim() && !$2(el).attr("title")?.trim()) {
44270
+ addFinding("button-text", "critical", "Button ohne Text", $2.html(el)?.slice(0, 200) ?? "", "button", "4.1.2", "Text oder aria-label hinzuf\xFCgen");
44233
44271
  }
44234
44272
  });
44235
- const viewport = $2('meta[name="viewport"]').attr("content") || "";
44236
- if (!viewport) {
44237
- addFinding("viewport", "moderate", "Kein viewport meta-Tag gefunden", "", "meta", "1.4.10", 'F\xFCgen Sie <meta name="viewport" content="width=device-width, initial-scale=1"> hinzu');
44238
- } else if (viewport.includes("user-scalable=no") || viewport.includes("maximum-scale=1")) {
44239
- addFinding("viewport-zoom", "critical", "Zoom ist deaktiviert (user-scalable=no oder maximum-scale=1)", "", "meta", "1.4.4", "Entfernen Sie user-scalable=no und maximum-scale=1");
44273
+ const vp = $2('meta[name="viewport"]').attr("content") || "";
44274
+ if (!vp) addFinding("viewport", "moderate", "Kein viewport meta-Tag", "", "meta", "1.4.10", "viewport meta-Tag hinzuf\xFCgen");
44275
+ else if (vp.includes("user-scalable=no") || vp.includes("maximum-scale=1")) {
44276
+ addFinding("viewport-zoom", "critical", "Zoom deaktiviert", "", "meta", "1.4.4", "user-scalable=no entfernen");
44240
44277
  }
44278
+ const validRoles = /* @__PURE__ */ 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"]);
44241
44279
  $2("[role]").each((_, el) => {
44242
44280
  const role = $2(el).attr("role") || "";
44243
- const validRoles = [
44244
- "alert",
44245
- "alertdialog",
44246
- "application",
44247
- "article",
44248
- "banner",
44249
- "button",
44250
- "cell",
44251
- "checkbox",
44252
- "columnheader",
44253
- "combobox",
44254
- "complementary",
44255
- "contentinfo",
44256
- "definition",
44257
- "dialog",
44258
- "directory",
44259
- "document",
44260
- "feed",
44261
- "figure",
44262
- "form",
44263
- "grid",
44264
- "gridcell",
44265
- "group",
44266
- "heading",
44267
- "img",
44268
- "link",
44269
- "list",
44270
- "listbox",
44271
- "listitem",
44272
- "log",
44273
- "main",
44274
- "marquee",
44275
- "math",
44276
- "menu",
44277
- "menubar",
44278
- "menuitem",
44279
- "menuitemcheckbox",
44280
- "menuitemradio",
44281
- "navigation",
44282
- "none",
44283
- "note",
44284
- "option",
44285
- "presentation",
44286
- "progressbar",
44287
- "radio",
44288
- "radiogroup",
44289
- "region",
44290
- "row",
44291
- "rowgroup",
44292
- "rowheader",
44293
- "scrollbar",
44294
- "search",
44295
- "searchbox",
44296
- "separator",
44297
- "slider",
44298
- "spinbutton",
44299
- "status",
44300
- "switch",
44301
- "tab",
44302
- "table",
44303
- "tablist",
44304
- "tabpanel",
44305
- "term",
44306
- "textbox",
44307
- "timer",
44308
- "toolbar",
44309
- "tooltip",
44310
- "tree",
44311
- "treegrid",
44312
- "treeitem"
44313
- ];
44314
- if (!validRoles.includes(role)) {
44315
- addFinding("aria-role", "serious", `Ung\xFCltige ARIA-Rolle: "${role}"`, $2.html(el)?.slice(0, 200) ?? "", `[role="${role}"]`, "4.1.2", "Verwenden Sie eine g\xFCltige ARIA-Rolle");
44281
+ if (!validRoles.has(role)) {
44282
+ addFinding("aria-role", "serious", `Ung\xFCltige ARIA-Rolle: "${role}"`, $2.html(el)?.slice(0, 200) ?? "", `[role="${role}"]`, "4.1.2", "G\xFCltige ARIA-Rolle verwenden");
44316
44283
  }
44317
44284
  });
44318
44285
  return findings;
@@ -44320,16 +44287,11 @@ function runRules(html3, url, $2) {
44320
44287
  async function scanUrl(url) {
44321
44288
  const start = Date.now();
44322
44289
  const response = await fetch(url, {
44323
- headers: {
44324
- "User-Agent": "AccessPilot-Dev/1.0",
44325
- "Accept": "text/html,application/xhtml+xml"
44326
- },
44290
+ headers: { "User-Agent": "ACPilot-Dev/1.0", "Accept": "text/html,application/xhtml+xml" },
44327
44291
  signal: AbortSignal.timeout(15e3),
44328
44292
  redirect: "follow"
44329
44293
  });
44330
- if (!response.ok) {
44331
- throw new Error(`HTTP ${response.status} f\xFCr ${url}`);
44332
- }
44294
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
44333
44295
  const html3 = await response.text();
44334
44296
  const $2 = load(html3);
44335
44297
  const pageTitle = $2("title").text().trim() || "Kein Titel";
@@ -44337,16 +44299,7 @@ async function scanUrl(url) {
44337
44299
  const score = calculateScore(findings);
44338
44300
  const counts = { critical: 0, serious: 0, moderate: 0, minor: 0 };
44339
44301
  for (const f of findings) counts[f.severity]++;
44340
- return {
44341
- url,
44342
- scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
44343
- score,
44344
- totalFindings: findings.length,
44345
- ...counts,
44346
- findings,
44347
- pageTitle,
44348
- loadTimeMs: Date.now() - start
44349
- };
44302
+ return { url, scannedAt: (/* @__PURE__ */ new Date()).toISOString(), score, totalFindings: findings.length, ...counts, findings, pageTitle, loadTimeMs: Date.now() - start };
44350
44303
  }
44351
44304
  async function crawlUrls(baseUrl, maxPages, maxDepth, log) {
44352
44305
  const seen = /* @__PURE__ */ new Set();
@@ -44377,16 +44330,11 @@ async function crawlUrls(baseUrl, maxPages, maxDepth, log) {
44377
44330
  while (queue.length > 0 && discovered.length < maxPages) {
44378
44331
  const { url, depth } = queue.shift();
44379
44332
  if (depth >= maxDepth) continue;
44380
- log(` Crawle ${url.replace(base.origin, "")} (Tiefe ${depth})`);
44333
+ log(` ${GRAY}\u21B3 ${url.replace(base.origin, "") || "/"}${RESET}`);
44381
44334
  try {
44382
- const res = await fetch(url, {
44383
- headers: { "User-Agent": "AccessPilot-Dev/1.0", "Accept": "text/html" },
44384
- signal: AbortSignal.timeout(1e4),
44385
- redirect: "follow"
44386
- });
44335
+ const res = await fetch(url, { headers: { "User-Agent": "ACPilot-Dev/1.0", "Accept": "text/html" }, signal: AbortSignal.timeout(1e4), redirect: "follow" });
44387
44336
  if (!res.ok) continue;
44388
- const ct = res.headers.get("content-type") || "";
44389
- if (!ct.includes("text/html")) continue;
44337
+ if (!(res.headers.get("content-type") || "").includes("text/html")) continue;
44390
44338
  const html3 = await res.text();
44391
44339
  const $2 = load(html3);
44392
44340
  $2("a[href]").each((_, el) => {
@@ -44405,27 +44353,80 @@ async function crawlUrls(baseUrl, maxPages, maxDepth, log) {
44405
44353
  }
44406
44354
  return discovered;
44407
44355
  }
44408
- var RESET = "\x1B[0m";
44409
- var BOLD = "\x1B[1m";
44410
- var GREEN = "\x1B[32m";
44411
- var RED = "\x1B[31m";
44412
- var YELLOW = "\x1B[33m";
44413
- var CYAN = "\x1B[36m";
44414
- var DIM = "\x1B[2m";
44415
- function scoreColor(score) {
44416
- if (score >= 90) return GREEN;
44417
- if (score >= 70) return YELLOW;
44418
- return RED;
44356
+ function progressBar(current, total, width = 30) {
44357
+ const pct = Math.round(current / total * 100);
44358
+ const filled = Math.round(current / total * width);
44359
+ const bar = `${CYAN}${"\u2588".repeat(filled)}${GRAY}${"\u2591".repeat(width - filled)}${RESET}`;
44360
+ return ` ${bar} ${BRIGHT_WHITE}${pct}%${RESET} ${DIM}(${current}/${total})${RESET}`;
44361
+ }
44362
+ function bigScore(score) {
44363
+ const c = scoreColor(score);
44364
+ const label = score >= 90 ? "Sehr gut" : score >= 70 ? "Verbesserungsbedarf" : "Kritisch";
44365
+ return ` ${c}${BOLD} \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
44366
+ \u2551 ${score}/100 \u2551
44367
+ \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D${RESET}
44368
+ ${c}${label}${RESET}`;
44419
44369
  }
44420
44370
  async function main() {
44421
44371
  const args = parseArgs();
44422
- if (!args.json) {
44423
- console.log(`
44424
- ${BOLD} AccessPilot Dev Scanner${RESET}`);
44425
- console.log(`${DIM} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${RESET}
44426
- `);
44427
- }
44428
- if (!args.json) process.stdout.write(` API-Key wird verifiziert... `);
44372
+ if (args.json && args.url && args.apiKey) {
44373
+ return runScan(args);
44374
+ }
44375
+ printLogo();
44376
+ let url = args.url;
44377
+ let apiKey = args.apiKey;
44378
+ let mode = args.noCrawl ? "single" : "crawl";
44379
+ let maxPages = args.maxPages;
44380
+ let maxDepth = args.maxDepth;
44381
+ if (args.interactive || !url || !apiKey) {
44382
+ const rl = createRL();
44383
+ console.log(` ${DIM}Willkommen beim ACPilot Dev Scanner!${RESET}`);
44384
+ console.log(` ${DIM}Scanne deine lokale Website auf WCAG-Barrierefreiheit.${RESET}`);
44385
+ printDivider("Konfiguration");
44386
+ url = await ask(rl, `${BOLD}URL deines Dev-Servers${RESET}`, "http://localhost:3000");
44387
+ try {
44388
+ new URL(url);
44389
+ } catch {
44390
+ console.error(`
44391
+ ${RED}\u2717${RESET} Ung\xFCltige URL: ${url}`);
44392
+ rl.close();
44393
+ process.exit(1);
44394
+ }
44395
+ if (!apiKey) {
44396
+ apiKey = await ask(rl, `${BOLD}API-Key${RESET} ${DIM}(von acpilot.de/dashboard/dev)${RESET}`);
44397
+ }
44398
+ if (!apiKey) {
44399
+ console.error(`
44400
+ ${RED}\u2717${RESET} API-Key ist erforderlich.`);
44401
+ console.log(` ${DIM}Erstelle einen auf: ${CYAN}https://acpilot.de/dashboard/dev${RESET}`);
44402
+ rl.close();
44403
+ process.exit(1);
44404
+ }
44405
+ console.log("");
44406
+ const modeChoice = await askChoice(rl, `${BOLD}Scan-Modus w\xE4hlen${RESET}`, [
44407
+ { key: "1", label: "Komplette Website", desc: "\u2014 Crawlt alle Unterseiten automatisch" },
44408
+ { key: "2", label: "Einzelne Seite", desc: "\u2014 Nur die eingegebene URL scannen" }
44409
+ ]);
44410
+ mode = modeChoice === "2" ? "single" : "crawl";
44411
+ if (mode === "crawl") {
44412
+ const pagesStr = await ask(rl, "Max. Seitenanzahl", "50");
44413
+ maxPages = parseInt(pagesStr, 10) || 50;
44414
+ const depthStr = await ask(rl, "Max. Crawl-Tiefe", "3");
44415
+ maxDepth = parseInt(depthStr, 10) || 3;
44416
+ }
44417
+ rl.close();
44418
+ args.url = url;
44419
+ args.apiKey = apiKey;
44420
+ args.noCrawl = mode === "single";
44421
+ args.maxPages = maxPages;
44422
+ args.maxDepth = maxDepth;
44423
+ }
44424
+ return runScan(args);
44425
+ }
44426
+ async function runScan(args) {
44427
+ const isJson = args.json;
44428
+ printDivider("Verbindung");
44429
+ process.stdout.write(` ${CYAN}\u27F3${RESET} API-Key wird verifiziert... `);
44429
44430
  try {
44430
44431
  const verifyRes = await fetch(`${args.server}/api/dev-agent/verify`, {
44431
44432
  method: "POST",
@@ -44434,118 +44435,116 @@ ${BOLD} AccessPilot Dev Scanner${RESET}`);
44434
44435
  });
44435
44436
  const verifyData = await verifyRes.json();
44436
44437
  if (!verifyData.valid) {
44437
- if (!args.json) console.log(`${RED}ung\xFCltig${RESET}`);
44438
+ console.log(`${RED}\u2717 ung\xFCltig${RESET}`);
44438
44439
  console.error(`
44439
- Fehler: ${verifyData.error || "Ung\xFCltiger API-Key"}`);
44440
+ ${RED}Fehler:${RESET} ${verifyData.error || "Ung\xFCltiger API-Key"}`);
44441
+ console.log(` ${DIM}Erstelle einen neuen Key auf: ${CYAN}https://acpilot.de/dashboard/dev${RESET}`);
44440
44442
  process.exit(1);
44441
44443
  }
44442
- if (!args.json) console.log(`${GREEN}OK${RESET}`);
44443
- } catch (err) {
44444
- if (!args.json) console.log(`${RED}Fehler${RESET}`);
44444
+ console.log(`${GREEN}\u2713 verifiziert${RESET}`);
44445
+ } catch {
44446
+ console.log(`${RED}\u2717 Fehler${RESET}`);
44445
44447
  console.error(`
44446
- Verbindung zu ${args.server} fehlgeschlagen.`);
44447
- console.error(` Ist der AccessPilot-Server erreichbar?
44448
- `);
44448
+ ${RED}Verbindung zu ${args.server} fehlgeschlagen.${RESET}`);
44449
44449
  process.exit(1);
44450
44450
  }
44451
44451
  let urls;
44452
44452
  if (args.noCrawl) {
44453
44453
  urls = [args.url];
44454
- if (!args.json) console.log(` 1 URL (kein Crawling)
44455
- `);
44454
+ console.log(` ${CYAN}\u25C9${RESET} Einzelscan: ${BOLD}${args.url}${RESET}`);
44456
44455
  } else {
44457
- if (!args.json) console.log(`
44458
- URLs werden entdeckt...`);
44456
+ printDivider("URL Discovery");
44457
+ console.log(` ${CYAN}\u27F3${RESET} Crawle ${BOLD}${args.url}${RESET} ${DIM}(max ${args.maxPages} Seiten, Tiefe ${args.maxDepth})${RESET}
44458
+ `);
44459
44459
  urls = await crawlUrls(args.url, args.maxPages, args.maxDepth, (msg) => {
44460
- if (!args.json) console.log(`${DIM}${msg}${RESET}`);
44460
+ if (!isJson) console.log(msg);
44461
44461
  });
44462
- if (!args.json) console.log(`
44463
- ${GREEN}${urls.length} Seiten gefunden${RESET}
44464
- `);
44462
+ console.log(`
44463
+ ${GREEN}\u2713${RESET} ${BOLD}${urls.length}${RESET} Seiten entdeckt`);
44465
44464
  }
44466
44465
  if (urls.length === 0) {
44467
- console.error(" Keine Seiten gefunden. Ist der Dev-Server gestartet?");
44466
+ console.error(`
44467
+ ${RED}\u2717${RESET} Keine Seiten gefunden. Ist der Dev-Server gestartet?`);
44468
44468
  process.exit(1);
44469
44469
  }
44470
- if (!args.json) console.log(` Scanning...`);
44470
+ printDivider("Scanning");
44471
44471
  const results = [];
44472
44472
  const CONCURRENCY = 5;
44473
+ let scanned = 0;
44474
+ const origin = new URL(args.url).origin;
44473
44475
  for (let i = 0; i < urls.length; i += CONCURRENCY) {
44474
44476
  const chunk = urls.slice(i, i + CONCURRENCY);
44475
- const chunkResults = await Promise.allSettled(
44476
- chunk.map((url) => scanUrl(url))
44477
- );
44478
- for (let j = 0; j < chunkResults.length; j++) {
44479
- const settled = chunkResults[j];
44480
- const pageUrl = chunk[j];
44481
- if (settled.status === "fulfilled") {
44482
- const r = settled.value;
44477
+ const settled = await Promise.allSettled(chunk.map((u) => scanUrl(u)));
44478
+ for (let j = 0; j < settled.length; j++) {
44479
+ scanned++;
44480
+ const s = settled[j];
44481
+ const path = chunk[j].replace(origin, "") || "/";
44482
+ if (s.status === "fulfilled") {
44483
+ const r = s.value;
44483
44484
  results.push(r);
44484
- if (!args.json) {
44485
- const sc = scoreColor(r.score);
44486
- const path = pageUrl.replace(new URL(args.url).origin, "") || "/";
44487
- console.log(` ${sc}${r.score}${RESET} ${path} ${DIM}(${r.totalFindings} Findings, ${r.loadTimeMs}ms)${RESET}`);
44488
- }
44485
+ const sc = scoreColor(r.score);
44486
+ const severity = r.critical > 0 ? `${RED}${r.critical} krit.${RESET}` : r.serious > 0 ? `${YELLOW}${r.serious} ernst${RESET}` : `${GREEN}sauber${RESET}`;
44487
+ console.log(` ${sc}${String(r.score).padStart(3)}${RESET} ${path.padEnd(35)} ${severity} ${DIM}${r.loadTimeMs}ms${RESET}`);
44489
44488
  } else {
44490
- if (!args.json) {
44491
- const path = pageUrl.replace(new URL(args.url).origin, "") || "/";
44492
- console.log(` ${RED}ERR${RESET} ${path} ${DIM}${settled.reason instanceof Error ? settled.reason.message : "Fehler"}${RESET}`);
44493
- }
44489
+ console.log(` ${RED}ERR${RESET} ${path.padEnd(35)} ${DIM}${s.reason instanceof Error ? s.reason.message : "Fehler"}${RESET}`);
44494
44490
  }
44495
44491
  }
44492
+ if (!isJson && urls.length > CONCURRENCY) {
44493
+ console.log(progressBar(scanned, urls.length));
44494
+ }
44496
44495
  }
44497
44496
  if (results.length === 0) {
44498
- console.error("\n Keine Seiten konnten gescannt werden.");
44497
+ console.error(`
44498
+ ${RED}\u2717${RESET} Keine Seiten konnten gescannt werden.`);
44499
44499
  process.exit(1);
44500
44500
  }
44501
- if (!args.json) process.stdout.write(`
44502
- Ergebnisse werden hochgeladen... `);
44501
+ printDivider("Upload");
44502
+ process.stdout.write(` ${CYAN}\u27F3${RESET} Ergebnisse werden hochgeladen... `);
44503
44503
  try {
44504
44504
  const pushRes = await fetch(`${args.server}/api/dev-agent/results`, {
44505
44505
  method: "POST",
44506
- headers: {
44507
- "Authorization": `Bearer ${args.apiKey}`,
44508
- "Content-Type": "application/json"
44509
- },
44510
- body: JSON.stringify({
44511
- base_url: args.url,
44512
- pages: results,
44513
- project_id: args.projectId
44514
- })
44506
+ headers: { "Authorization": `Bearer ${args.apiKey}`, "Content-Type": "application/json" },
44507
+ body: JSON.stringify({ base_url: args.url, pages: results, project_id: args.projectId })
44515
44508
  });
44516
44509
  const pushData = await pushRes.json();
44517
44510
  if (!pushRes.ok) {
44518
- if (!args.json) console.log(`${RED}Fehler${RESET}`);
44511
+ console.log(`${RED}\u2717${RESET}`);
44519
44512
  console.error(` ${pushData.error || "Upload fehlgeschlagen"}`);
44520
44513
  process.exit(1);
44521
44514
  }
44522
- if (args.json) {
44515
+ console.log(`${GREEN}\u2713 hochgeladen${RESET}`);
44516
+ if (isJson) {
44523
44517
  console.log(JSON.stringify({ ...pushData, results }, null, 2));
44524
- } else {
44525
- console.log(`${GREEN}OK${RESET}`);
44526
- const avgScore = Math.round(results.reduce((s, r) => s + r.score, 0) / results.length);
44527
- const totalFindings = results.reduce((s, r) => s + r.totalFindings, 0);
44528
- const totalCritical = results.reduce((s, r) => s + r.critical, 0);
44529
- const totalSerious = results.reduce((s, r) => s + r.serious, 0);
44530
- console.log(`
44531
- ${BOLD} \u2500\u2500 Ergebnis \u2500\u2500${RESET}`);
44532
- console.log(` Seiten: ${results.length}`);
44533
- console.log(` Score: ${scoreColor(avgScore)}${avgScore}/100${RESET}`);
44534
- console.log(` Findings: ${totalFindings} gesamt`);
44535
- if (totalCritical > 0) console.log(` Kritisch: ${RED}${totalCritical}${RESET}`);
44536
- if (totalSerious > 0) console.log(` Ernst: ${YELLOW}${totalSerious}${RESET}`);
44537
- console.log(`
44538
- ${CYAN}Dashboard:${RESET} ${args.server}${pushData.dashboard_url}
44539
- `);
44518
+ return;
44540
44519
  }
44520
+ const avgScore = Math.round(results.reduce((s, r) => s + r.score, 0) / results.length);
44521
+ const totalFindings = results.reduce((s, r) => s + r.totalFindings, 0);
44522
+ const totalCritical = results.reduce((s, r) => s + r.critical, 0);
44523
+ const totalSerious = results.reduce((s, r) => s + r.serious, 0);
44524
+ const totalModerate = results.reduce((s, r) => s + r.moderate, 0);
44525
+ const totalMinor = results.reduce((s, r) => s + r.minor, 0);
44526
+ printDivider("Ergebnis");
44527
+ console.log(bigScore(avgScore));
44528
+ console.log("");
44529
+ console.log(` ${BOLD}Seiten gescannt:${RESET} ${results.length}`);
44530
+ console.log(` ${BOLD}Findings gesamt:${RESET} ${totalFindings}`);
44531
+ if (totalCritical > 0) console.log(` ${RED}\u25CF Kritisch:${RESET} ${totalCritical}`);
44532
+ if (totalSerious > 0) console.log(` ${YELLOW}\u25CF Ernst:${RESET} ${totalSerious}`);
44533
+ if (totalModerate > 0) console.log(` ${CYAN}\u25CF Moderat:${RESET} ${totalModerate}`);
44534
+ if (totalMinor > 0) console.log(` ${GRAY}\u25CF Gering:${RESET} ${totalMinor}`);
44535
+ console.log("");
44536
+ console.log(` ${CYAN}\u2197${RESET} Dashboard: ${BOLD}${args.server}${pushData.dashboard_url}${RESET}`);
44537
+ console.log("");
44538
+ console.log(` ${DIM}Detaillierte Ergebnisse im ACPilot Dashboard ansehen.${RESET}`);
44539
+ console.log("");
44541
44540
  } catch (err) {
44542
- if (!args.json) console.log(`${RED}Fehler${RESET}`);
44541
+ console.log(`${RED}\u2717${RESET}`);
44543
44542
  console.error(` Upload fehlgeschlagen: ${err instanceof Error ? err.message : err}`);
44544
44543
  process.exit(1);
44545
44544
  }
44546
44545
  }
44547
44546
  main().catch((err) => {
44548
- console.error(`Unerwarteter Fehler: ${err.message}`);
44547
+ console.error(`${RED}Fehler:${RESET} ${err.message}`);
44549
44548
  process.exit(1);
44550
44549
  });
44551
44550
  /*! Bundled license information:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "acpilot",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Scan your localhost for accessibility issues and push results to AccessPilot",
5
5
  "bin": {
6
6
  "acpilot": "./dist/cli.js"