@skilly-hand/skilly-hand 0.6.0 → 0.7.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/CHANGELOG.md CHANGED
@@ -16,6 +16,36 @@ All notable changes to this project are documented in this file.
16
16
  ### Removed
17
17
  - _None._
18
18
 
19
+ ## [0.7.0] - 2026-04-04
20
+ [View on npm](https://www.npmjs.com/package/@skilly-hand/skilly-hand/v/0.7.0)
21
+
22
+ ### Added
23
+ - _None._
24
+
25
+ ### Changed
26
+ - _None._
27
+
28
+ ### Fixed
29
+ - _None._
30
+
31
+ ### Removed
32
+ - _None._
33
+
34
+ ## [0.6.1] - 2026-04-04
35
+ [View on npm](https://www.npmjs.com/package/@skilly-hand/skilly-hand/v/0.6.1)
36
+
37
+ ### Added
38
+ - _None._
39
+
40
+ ### Changed
41
+ - _None._
42
+
43
+ ### Fixed
44
+ - _None._
45
+
46
+ ### Removed
47
+ - _None._
48
+
19
49
  ## [0.6.0] - 2026-04-03
20
50
  [View on npm](https://www.npmjs.com/package/@skilly-hand/skilly-hand/v/0.6.0)
21
51
 
package/README.md CHANGED
@@ -65,6 +65,7 @@ npx skilly-hand
65
65
 
66
66
  The catalog currently includes:
67
67
 
68
+ - `accessibility-audit`
68
69
  - `agents-root-orchestrator`
69
70
  - `angular-guidelines`
70
71
  - `figma-mcp-0to1`
package/catalog/README.md CHANGED
@@ -4,6 +4,7 @@ Published portable skills consumed by the `skilly-hand` CLI.
4
4
 
5
5
  | Skill | Description | Tags | Installs For |
6
6
  | ----- | ----------- | ---- | ------------ |
7
+ | `accessibility-audit` | Audit web accessibility against W3C WCAG 2.2 Level AA using framework-agnostic checks, remediation patterns, and portable command-line scanning. | frontend, accessibility, workflow, quality | all |
7
8
  | `agents-root-orchestrator` | Author root AGENTS.md as a Where/What/When orchestrator that routes tasks and skill invocation clearly. | core, workflow, orchestration | all |
8
9
  | `angular-guidelines` | Guide Angular code generation and review using latest stable Angular verification and modern framework best practices. | angular, frontend, workflow, best-practices | all |
9
10
  | `figma-mcp-0to1` | Guide users from Figma MCP installation and authentication through first canvas creation, with function-level tool coverage and operational recovery patterns. | figma, mcp, workflow, design | all |
@@ -1,4 +1,5 @@
1
1
  [
2
+ "accessibility-audit",
2
3
  "agents-root-orchestrator",
3
4
  "angular-guidelines",
4
5
  "figma-mcp-0to1",
@@ -0,0 +1,154 @@
1
+ # Accessibility Audit Guide
2
+
3
+ ## When to Use
4
+
5
+ Use this skill when:
6
+
7
+ - Auditing components or pages for WCAG conformance.
8
+ - Reviewing pull requests that change templates, interactive UI, forms, or styles.
9
+ - Defining accessibility acceptance criteria for frontend delivery.
10
+ - Converting automated scanner findings into prioritized remediations.
11
+
12
+ Do not use this skill for:
13
+
14
+ - Product-specific visual token compliance.
15
+ - Framework-only code style reviews unrelated to accessibility behavior.
16
+ - Non-web formats that need a dedicated standard beyond WCAG web content checks.
17
+
18
+ ---
19
+
20
+ ## Baseline and Sources
21
+
22
+ Default baseline:
23
+
24
+ - **WCAG 2.2 Level AA**.
25
+
26
+ W3C status notes (verified from W3C WCAG overview):
27
+
28
+ - WCAG 2.2 was published on **5 October 2023** and updated on **12 December 2024**.
29
+ - W3C encourages using the latest WCAG version.
30
+
31
+ Use only W3C sources for decisions and remediation rationale.
32
+
33
+ ---
34
+
35
+ ## Critical Patterns
36
+
37
+ ### Pattern 1: Audit in POUR Order
38
+
39
+ Audit checks in this order to reduce misses:
40
+
41
+ 1. **Perceivable**: text alternatives, structure, contrast.
42
+ 2. **Operable**: keyboard, focus, target size, predictable interaction.
43
+ 3. **Understandable**: labels, errors, language, clear behavior.
44
+ 4. **Robust**: semantic roles/states and assistive-technology compatibility.
45
+
46
+ ### Pattern 2: Prefer Native Semantics First
47
+
48
+ - Use native controls (`button`, `a`, `input`, `select`, `textarea`) before ARIA-heavy custom widgets.
49
+ - If custom widgets are necessary, define role, keyboard behavior, name, state, and relationship.
50
+ - Never remove focus indicators without a visible replacement.
51
+
52
+ ### Pattern 3: Prioritize by User Impact
53
+
54
+ Fix in this order:
55
+
56
+ 1. Keyboard and focus blockers.
57
+ 2. Missing names/labels for controls and media.
58
+ 3. Form errors and status announcements.
59
+ 4. Contrast and non-text contrast issues.
60
+
61
+ ### Pattern 4: Validate with W3C Tooling
62
+
63
+ Use W3C validators as baseline technical checks, then complete manual WCAG behavior review:
64
+
65
+ - Nu HTML Checker
66
+ - CSS Validator
67
+
68
+ ---
69
+
70
+ ## Decision Tree
71
+
72
+ ```text
73
+ Is this an interactive control? -> Verify keyboard access + visible focus + accessible name
74
+ Is this non-text content (image/icon/media)? -> Verify text alternative strategy
75
+ Is this a form input or validation message? -> Verify labels, instructions, errors, and status messaging
76
+ Is this a custom widget pattern? -> Verify role/state/property model and keyboard model
77
+ Does styling reduce legibility or discernibility? -> Verify text + non-text contrast and target size
78
+ Otherwise -> Run checklist sweep and document residual risk
79
+ ```
80
+
81
+ ---
82
+
83
+ ## Code Examples
84
+
85
+ ### Example 1: Icon Button with Accessible Name and Focus Indicator
86
+
87
+ ```html
88
+ <button type="button" aria-label="Close dialog" class="icon-button">
89
+ <svg aria-hidden="true" focusable="false" viewBox="0 0 24 24">
90
+ <path d="M6 6l12 12M18 6L6 18" />
91
+ </svg>
92
+ </button>
93
+ ```
94
+
95
+ ```css
96
+ .icon-button:focus-visible {
97
+ outline: 2px solid #005a9c;
98
+ outline-offset: 2px;
99
+ }
100
+ ```
101
+
102
+ ### Example 2: Labeled Input with Error Association
103
+
104
+ ```html
105
+ <label for="email">Email address</label>
106
+ <input
107
+ id="email"
108
+ type="email"
109
+ aria-invalid="true"
110
+ aria-describedby="email-error"
111
+ />
112
+ <p id="email-error" role="alert">Enter a valid email address.</p>
113
+ ```
114
+
115
+ ### Example 3: Semantic Click Target Instead of Generic Container
116
+
117
+ ```html
118
+ <button type="button" class="card-action">Open details</button>
119
+ ```
120
+
121
+ ```html
122
+ <div role="button" tabindex="0" aria-label="Open details"></div>
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Commands
128
+
129
+ ```bash
130
+ # Run default scan on current directory
131
+ bash catalog/skills/accessibility-audit/scripts/audit-a11y.sh
132
+
133
+ # Scan a specific path
134
+ bash catalog/skills/accessibility-audit/scripts/audit-a11y.sh src
135
+
136
+ # Generate markdown report
137
+ bash catalog/skills/accessibility-audit/scripts/audit-a11y.sh --report src
138
+
139
+ # Generate JSON output for CI pipelines
140
+ bash catalog/skills/accessibility-audit/scripts/audit-a11y.sh --json src
141
+ ```
142
+
143
+ ---
144
+
145
+ ## Resources
146
+
147
+ - Full checklist: [references/w3c-wcag22-checklist.md](references/w3c-wcag22-checklist.md)
148
+ - WCAG overview (WAI): https://www.w3.org/WAI/standards-guidelines/wcag/
149
+ - WCAG 2.2 Recommendation: https://www.w3.org/TR/WCAG22/
150
+ - How to Meet WCAG 2 (Quick Reference): https://www.w3.org/WAI/WCAG22/quickref/
151
+ - Understanding WCAG 2: https://www.w3.org/WAI/WCAG22/Understanding/
152
+ - W3C standards context: https://www.w3.org/standards/
153
+ - W3C validators and tools: https://www.w3.org/developers/tools/
154
+ - W3C homepage: https://www.w3.org/
@@ -0,0 +1,37 @@
1
+ {
2
+ "id": "accessibility-audit",
3
+ "title": "Accessibility Audit",
4
+ "description": "Audit web accessibility against W3C WCAG 2.2 Level AA using framework-agnostic checks, remediation patterns, and portable command-line scanning.",
5
+ "portable": true,
6
+ "tags": ["frontend", "accessibility", "workflow", "quality"],
7
+ "detectors": ["always"],
8
+ "detectionTriggers": ["manual"],
9
+ "installsFor": ["all"],
10
+ "agentSupport": ["codex", "claude", "cursor", "gemini", "copilot"],
11
+ "skillMetadata": {
12
+ "author": "skilly-hand",
13
+ "last-edit": "2026-04-04",
14
+ "license": "Apache-2.0",
15
+ "version": "1.0.0",
16
+ "changelog": "Added portable WCAG 2.2 Level AA accessibility auditing skill with W3C-only references and scanner script; enables consistent web accessibility review across frameworks; affects catalog skill coverage and install plans for stacks recommending accessibility-audit",
17
+ "auto-invoke": "Auditing, reviewing, or implementing web accessibility against WCAG 2.2 Level AA",
18
+ "allowed-tools": [
19
+ "Read",
20
+ "Edit",
21
+ "Write",
22
+ "Glob",
23
+ "Grep",
24
+ "Bash",
25
+ "WebFetch",
26
+ "WebSearch",
27
+ "Task",
28
+ "SubAgent"
29
+ ]
30
+ },
31
+ "files": [
32
+ { "path": "SKILL.md", "kind": "instruction" },
33
+ { "path": "references/w3c-wcag22-checklist.md", "kind": "reference" },
34
+ { "path": "scripts/audit-a11y.sh", "kind": "asset" }
35
+ ],
36
+ "dependencies": []
37
+ }
@@ -0,0 +1,80 @@
1
+ # W3C WCAG 2.2 Level AA Checklist (Web)
2
+
3
+ Source basis:
4
+
5
+ - WCAG 2 Overview: https://www.w3.org/WAI/standards-guidelines/wcag/
6
+ - WCAG 2.2 Recommendation: https://www.w3.org/TR/WCAG22/
7
+ - Quick Reference: https://www.w3.org/WAI/WCAG22/quickref/
8
+ - Understanding WCAG 2: https://www.w3.org/WAI/WCAG22/Understanding/
9
+
10
+ Default conformance target in this skill: **WCAG 2.2 Level AA**.
11
+
12
+ ## Principle 1: Perceivable
13
+
14
+ | SC | Level | Audit Focus |
15
+ | --- | --- | --- |
16
+ | 1.1.1 Non-text Content | A | Images/icons/media alternatives (`alt`, labels, text alternatives) |
17
+ | 1.3.1 Info and Relationships | A | Semantic headings, labels, table/form relationships |
18
+ | 1.3.2 Meaningful Sequence | A | DOM reading order matches meaning |
19
+ | 1.3.3 Sensory Characteristics | A | Instructions are not only shape/color/position |
20
+ | 1.3.4 Orientation | AA | Content works in portrait and landscape unless essential |
21
+ | 1.3.5 Identify Input Purpose | AA | Appropriate `autocomplete` tokens for user-data fields |
22
+ | 1.4.1 Use of Color | A | Color is not the only information channel |
23
+ | 1.4.3 Contrast (Minimum) | AA | Text contrast thresholds are met |
24
+ | 1.4.4 Resize Text | AA | Content remains usable at 200% text resize |
25
+ | 1.4.10 Reflow | AA | Reflow at narrow viewport without two-dimensional scroll |
26
+ | 1.4.11 Non-text Contrast | AA | Component boundaries/icons/focus indicators are distinguishable |
27
+ | 1.4.12 Text Spacing | AA | Increased spacing does not break content/function |
28
+ | 1.4.13 Content on Hover or Focus | AA | Hover/focus content is dismissible, hoverable, persistent |
29
+
30
+ ## Principle 2: Operable
31
+
32
+ | SC | Level | Audit Focus |
33
+ | --- | --- | --- |
34
+ | 2.1.1 Keyboard | A | All functionality operable via keyboard |
35
+ | 2.1.2 No Keyboard Trap | A | Focus can enter and leave all components |
36
+ | 2.1.4 Character Key Shortcuts | A | Single-character shortcuts are safe/controllable |
37
+ | 2.4.1 Bypass Blocks | A | Mechanism exists to bypass repeated blocks |
38
+ | 2.4.2 Page Titled | A | Document/page has descriptive title |
39
+ | 2.4.3 Focus Order | A | Focus sequence follows meaningful operation |
40
+ | 2.4.4 Link Purpose (In Context) | A | Link purpose is clear from text/context |
41
+ | 2.4.6 Headings and Labels | AA | Headings/labels clearly describe purpose |
42
+ | 2.4.7 Focus Visible | AA | Visible focus indicator for keyboard users |
43
+ | 2.4.11 Focus Not Obscured (Minimum) | AA | Focused element is not fully hidden |
44
+ | 2.5.3 Label in Name | A | Accessible name includes visible label text |
45
+ | 2.5.7 Dragging Movements | AA | Drag interactions have non-drag alternatives |
46
+ | 2.5.8 Target Size (Minimum) | AA | Pointer targets meet minimum size/spacing |
47
+
48
+ ## Principle 3: Understandable
49
+
50
+ | SC | Level | Audit Focus |
51
+ | --- | --- | --- |
52
+ | 3.1.1 Language of Page | A | Correct `lang` on root document |
53
+ | 3.1.2 Language of Parts | AA | Language changes are marked in content |
54
+ | 3.2.1 On Focus | A | Focus alone does not trigger unexpected context change |
55
+ | 3.2.2 On Input | A | Input alone does not trigger unexpected context change |
56
+ | 3.2.3 Consistent Navigation | AA | Repeated navigation mechanisms are consistent |
57
+ | 3.2.4 Consistent Identification | AA | Same functions are identified consistently |
58
+ | 3.2.6 Consistent Help | AA | Repeated help mechanisms are consistently positioned |
59
+ | 3.3.1 Error Identification | A | Errors are identified in text |
60
+ | 3.3.2 Labels or Instructions | A | Inputs include labels/instructions |
61
+ | 3.3.3 Error Suggestion | AA | Suggested correction is provided when possible |
62
+ | 3.3.4 Error Prevention (Legal, Financial, Data) | AA | Reversible/confirmable/validated submissions |
63
+ | 3.3.7 Redundant Entry | A | Re-entry burden reduced where possible |
64
+ | 3.3.8 Accessible Authentication (Minimum) | AA | Authentication avoids inaccessible cognitive tests |
65
+
66
+ ## Principle 4: Robust
67
+
68
+ | SC | Level | Audit Focus |
69
+ | --- | --- | --- |
70
+ | 4.1.2 Name, Role, Value | A | UI controls expose name, role, state/value correctly |
71
+ | 4.1.3 Status Messages | AA | Dynamic status is conveyed programmatically |
72
+
73
+ ## W3C Tooling Complements
74
+
75
+ Use W3C validators from https://www.w3.org/developers/tools/ as supporting checks:
76
+
77
+ - Nu HTML Checker
78
+ - CSS Validator
79
+
80
+ These checks complement, but do not replace, manual WCAG behavior verification.
@@ -0,0 +1,372 @@
1
+ #!/usr/bin/env bash
2
+ # Portable accessibility audit helper aligned to WCAG 2.2 Level AA (W3C)
3
+ # Sources:
4
+ # - https://www.w3.org/WAI/standards-guidelines/wcag/
5
+ # - https://www.w3.org/TR/WCAG22/
6
+ # - https://www.w3.org/WAI/WCAG22/quickref/
7
+
8
+ set -euo pipefail
9
+
10
+ RED='\033[0;31m'
11
+ YELLOW='\033[0;33m'
12
+ CYAN='\033[0;36m'
13
+ GREEN='\033[0;32m'
14
+ BOLD='\033[1m'
15
+ DIM='\033[2m'
16
+ RESET='\033[0m'
17
+
18
+ REPORT_MODE=false
19
+ JSON_MODE=false
20
+ TARGET=""
21
+ ROOT="$(pwd)"
22
+ REPORT_FILE="$ROOT/a11y-audit-report.md"
23
+ REPORT_CONTENT=""
24
+ JSON_ENTRIES=""
25
+
26
+ FILES_SCANNED=0
27
+ FILES_WITH_ISSUES=0
28
+ TOTAL_ISSUES=0
29
+ ISSUES_CRITICAL=0
30
+ ISSUES_SERIOUS=0
31
+ ISSUES_MODERATE=0
32
+
33
+ usage() {
34
+ cat <<USAGE
35
+ Usage:
36
+ bash catalog/skills/accessibility-audit/scripts/audit-a11y.sh [options] [path]
37
+
38
+ Options:
39
+ --report Write markdown report to ./a11y-audit-report.md
40
+ --json Emit JSON findings to stdout
41
+ --help Show this help
42
+
43
+ Default path:
44
+ current directory
45
+ USAGE
46
+ }
47
+
48
+ while [[ $# -gt 0 ]]; do
49
+ case "$1" in
50
+ --report) REPORT_MODE=true; shift ;;
51
+ --json) JSON_MODE=true; shift ;;
52
+ --help|-h) usage; exit 0 ;;
53
+ *) TARGET="$1"; shift ;;
54
+ esac
55
+ done
56
+
57
+ TARGET="${TARGET:-.}"
58
+
59
+ if [[ ! -e "$TARGET" ]]; then
60
+ echo "Target does not exist: $TARGET" >&2
61
+ exit 1
62
+ fi
63
+
64
+ json_escape() {
65
+ echo "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g; s/\r/\\r/g; s/\n/\\n/g'
66
+ }
67
+
68
+ record_finding() {
69
+ local file="$1"
70
+ local relative_path="$2"
71
+ local category="$3"
72
+ local severity="$4"
73
+ local wcag_sc="$5"
74
+ local lineno="$6"
75
+ local raw_line="$7"
76
+ local message="$8"
77
+
78
+ local color_code=""
79
+ local label=""
80
+
81
+ case "$severity" in
82
+ critical) color_code="$RED"; label="CRITICAL"; ISSUES_CRITICAL=$((ISSUES_CRITICAL + 1)) ;;
83
+ serious) color_code="$YELLOW"; label="SERIOUS"; ISSUES_SERIOUS=$((ISSUES_SERIOUS + 1)) ;;
84
+ moderate) color_code="$CYAN"; label="MODERATE"; ISSUES_MODERATE=$((ISSUES_MODERATE + 1)) ;;
85
+ esac
86
+
87
+ if ! $JSON_MODE; then
88
+ printf " ${color_code}%-14s${RESET} [${DIM}%s${RESET}] line %-4s %s\n" "$category" "$wcag_sc" "$lineno:" "$message"
89
+ echo -e " ${DIM}->${RESET} $raw_line"
90
+ fi
91
+
92
+ if $REPORT_MODE; then
93
+ REPORT_CONTENT+="| \`${relative_path}\` | ${lineno} | ${label} | \`${category}\` | ${wcag_sc} | ${message} |"$'\n'
94
+ fi
95
+
96
+ if $JSON_MODE; then
97
+ [[ -n "$JSON_ENTRIES" ]] && JSON_ENTRIES+=","
98
+ JSON_ENTRIES+="{\"file\":\"$(json_escape "$relative_path")\",\"line\":${lineno},\"severity\":\"${severity}\",\"category\":\"$(json_escape "$category")\",\"wcag\":\"$(json_escape "$wcag_sc")\",\"message\":\"$(json_escape "$message")\",\"code\":\"$(json_escape "$raw_line")\"}"
99
+ fi
100
+ }
101
+
102
+ audit_markup_file() {
103
+ local file="$1"
104
+ local relative_path="${file#$ROOT/}"
105
+ local file_issues=0
106
+
107
+ FILES_SCANNED=$((FILES_SCANNED + 1))
108
+
109
+ while IFS=: read -r lineno line; do
110
+ [[ "$line" =~ ^[[:space:]]*\<\!-- ]] && continue
111
+ local trimmed
112
+ trimmed="$(echo "$line" | sed 's/^[[:space:]]*//')"
113
+ record_finding "$file" "$relative_path" "MISSING-ALT" "critical" "1.1.1" "$lineno" "$trimmed" "Image is missing alt attribute"
114
+ file_issues=$((file_issues + 1))
115
+ done < <(grep -n -E '<img[[:space:]]' "$file" 2>/dev/null | grep -v -E 'alt=' || true)
116
+
117
+ while IFS=: read -r lineno line; do
118
+ [[ "$line" =~ ^[[:space:]]*\<\!-- ]] && continue
119
+ echo "$line" | grep -qE 'aria-label|aria-labelledby|title=' && continue
120
+ local trimmed
121
+ trimmed="$(echo "$line" | sed 's/^[[:space:]]*//')"
122
+ if echo "$trimmed" | grep -qE '<button[^>]*>[[:space:]]*</button>' || echo "$trimmed" | grep -qE '<button[^>]*/>'; then
123
+ record_finding "$file" "$relative_path" "EMPTY-BUTTON" "critical" "4.1.2" "$lineno" "$trimmed" "Button has no text or accessible name"
124
+ file_issues=$((file_issues + 1))
125
+ fi
126
+ done < <(grep -n -E '<button' "$file" 2>/dev/null || true)
127
+
128
+ while IFS=: read -r lineno line; do
129
+ [[ "$line" =~ ^[[:space:]]*\<\!-- ]] && continue
130
+ echo "$line" | grep -qE 'aria-label|aria-labelledby|title=' && continue
131
+ local trimmed
132
+ trimmed="$(echo "$line" | sed 's/^[[:space:]]*//')"
133
+ if echo "$trimmed" | grep -qE '<a[^>]*>[[:space:]]*</a>'; then
134
+ record_finding "$file" "$relative_path" "EMPTY-LINK" "critical" "2.4.4" "$lineno" "$trimmed" "Link has no text or accessible name"
135
+ file_issues=$((file_issues + 1))
136
+ fi
137
+ done < <(grep -n -E '<a[[:space:]]' "$file" 2>/dev/null || true)
138
+
139
+ while IFS=: read -r lineno line; do
140
+ [[ "$line" =~ ^[[:space:]]*\<\!-- ]] && continue
141
+ echo "$line" | grep -qE 'aria-label|aria-labelledby' && continue
142
+ echo "$line" | grep -qE 'type="hidden"|type="submit"|type="button"' && continue
143
+
144
+ local has_label=false
145
+ if echo "$line" | grep -qE 'id="[^"]+"'; then
146
+ local input_id
147
+ input_id="$(echo "$line" | sed -n 's/.*id="\([^"]*\)".*/\1/p' | head -1)"
148
+ if [[ -n "$input_id" ]] && grep -qE "for=\"${input_id}\"" "$file" 2>/dev/null; then
149
+ has_label=true
150
+ fi
151
+ fi
152
+
153
+ if [[ "$has_label" == "false" ]]; then
154
+ local trimmed
155
+ trimmed="$(echo "$line" | sed 's/^[[:space:]]*//')"
156
+ record_finding "$file" "$relative_path" "MISSING-LABEL" "serious" "1.3.1/3.3.2" "$lineno" "$trimmed" "Form control missing label association or ARIA naming"
157
+ file_issues=$((file_issues + 1))
158
+ fi
159
+ done < <(grep -n -E '<(input|select|textarea)[[:space:]]' "$file" 2>/dev/null || true)
160
+
161
+ while IFS=: read -r lineno line; do
162
+ [[ "$line" =~ ^[[:space:]]*\<\!-- ]] && continue
163
+ echo "$line" | grep -qE 'role=|tabindex=' && continue
164
+ local trimmed
165
+ trimmed="$(echo "$line" | sed 's/^[[:space:]]*//')"
166
+ record_finding "$file" "$relative_path" "CLICK-NO-ROLE" "serious" "2.1.1" "$lineno" "$trimmed" "Non-semantic clickable element needs keyboard and role semantics"
167
+ file_issues=$((file_issues + 1))
168
+ done < <(grep -n -E '<(div|span|li)[^>]*((onclick=)|(\(click\)))' "$file" 2>/dev/null || true)
169
+
170
+ while IFS=: read -r lineno line; do
171
+ [[ "$line" =~ ^[[:space:]]*\<\!-- ]] && continue
172
+ local tab_val
173
+ tab_val="$(echo "$line" | grep -oE 'tabindex="[0-9]+"' | grep -oE '[0-9]+' | head -1)"
174
+ if [[ -n "$tab_val" ]] && [[ "$tab_val" -gt 0 ]]; then
175
+ local trimmed
176
+ trimmed="$(echo "$line" | sed 's/^[[:space:]]*//')"
177
+ record_finding "$file" "$relative_path" "TABINDEX-POS" "serious" "2.4.3" "$lineno" "$trimmed" "Positive tabindex can break logical focus order"
178
+ file_issues=$((file_issues + 1))
179
+ fi
180
+ done < <(grep -n -E 'tabindex="[1-9]' "$file" 2>/dev/null || true)
181
+
182
+ local prev_level=0
183
+ while IFS=: read -r lineno line; do
184
+ [[ "$line" =~ ^[[:space:]]*\<\!-- ]] && continue
185
+ local level
186
+ level="$(echo "$line" | grep -oE '<h[1-6]' | grep -oE '[1-6]' | head -1)"
187
+ [[ -z "$level" ]] && continue
188
+
189
+ if [[ "$prev_level" -gt 0 ]] && [[ "$level" -gt $((prev_level + 1)) ]]; then
190
+ local trimmed
191
+ trimmed="$(echo "$line" | sed 's/^[[:space:]]*//')"
192
+ record_finding "$file" "$relative_path" "HEADING-SKIP" "moderate" "1.3.1" "$lineno" "$trimmed" "Heading level skipped"
193
+ file_issues=$((file_issues + 1))
194
+ fi
195
+
196
+ prev_level="$level"
197
+ done < <(grep -n -E '<h[1-6]' "$file" 2>/dev/null || true)
198
+
199
+ if grep -qE '<html[[:space:]]' "$file" 2>/dev/null; then
200
+ if ! grep -qE '<html[^>]*lang=' "$file" 2>/dev/null; then
201
+ local lineno
202
+ lineno="$(grep -n -E '<html' "$file" | head -1 | cut -d: -f1)"
203
+ local trimmed
204
+ trimmed="$(grep -E '<html' "$file" | head -1 | sed 's/^[[:space:]]*//')"
205
+ record_finding "$file" "$relative_path" "MISSING-LANG" "serious" "3.1.1" "$lineno" "$trimmed" "Missing language declaration on html element"
206
+ file_issues=$((file_issues + 1))
207
+ fi
208
+ fi
209
+
210
+ if [[ "$file_issues" -gt 0 ]]; then
211
+ FILES_WITH_ISSUES=$((FILES_WITH_ISSUES + 1))
212
+ TOTAL_ISSUES=$((TOTAL_ISSUES + file_issues))
213
+ if ! $JSON_MODE; then
214
+ echo -e "\n${BOLD}${relative_path}${RESET} - ${RED}${file_issues} issue(s)${RESET}"
215
+ fi
216
+ fi
217
+ }
218
+
219
+ audit_style_file() {
220
+ local file="$1"
221
+ local relative_path="${file#$ROOT/}"
222
+ local file_issues=0
223
+
224
+ FILES_SCANNED=$((FILES_SCANNED + 1))
225
+
226
+ while IFS=: read -r lineno line; do
227
+ [[ "$line" =~ ^[[:space:]]*/\* ]] && continue
228
+
229
+ local context_start=$((lineno > 4 ? lineno - 4 : 1))
230
+ local context_end=$((lineno + 8))
231
+ if sed -n "${context_start},${context_end}p" "$file" 2>/dev/null | grep -qE 'focus-visible'; then
232
+ continue
233
+ fi
234
+
235
+ local trimmed
236
+ trimmed="$(echo "$line" | sed 's/^[[:space:]]*//')"
237
+ record_finding "$file" "$relative_path" "FOCUS-REMOVED" "critical" "2.4.7" "$lineno" "$trimmed" "Focus outline removed without visible focus-visible replacement"
238
+ file_issues=$((file_issues + 1))
239
+ done < <(
240
+ grep -n -E ':focus[[:space:]]*\{' "$file" 2>/dev/null | while IFS=: read -r ln _; do
241
+ local end=$((ln + 5))
242
+ if sed -n "${ln},${end}p" "$file" 2>/dev/null | grep -qE 'outline:[[:space:]]*(none|0)'; then
243
+ echo "${ln}:focus"
244
+ fi
245
+ done || true
246
+ )
247
+
248
+ if [[ "$file_issues" -gt 0 ]]; then
249
+ FILES_WITH_ISSUES=$((FILES_WITH_ISSUES + 1))
250
+ TOTAL_ISSUES=$((TOTAL_ISSUES + file_issues))
251
+ if ! $JSON_MODE; then
252
+ echo -e "\n${BOLD}${relative_path}${RESET} - ${RED}${file_issues} issue(s)${RESET}"
253
+ fi
254
+ fi
255
+ }
256
+
257
+ generate_report() {
258
+ local generated_at
259
+ generated_at="$(date '+%Y-%m-%d %H:%M:%S')"
260
+
261
+ cat > "$REPORT_FILE" <<REPORT_HEAD
262
+ # Accessibility Audit Report (WCAG 2.2 Level AA)
263
+
264
+ Generated: ${generated_at}
265
+ Target: \`${TARGET}\`
266
+
267
+ ## Summary
268
+
269
+ | Metric | Count |
270
+ | --- | --- |
271
+ | Files scanned | ${FILES_SCANNED} |
272
+ | Files with issues | ${FILES_WITH_ISSUES} |
273
+ | Critical | ${ISSUES_CRITICAL} |
274
+ | Serious | ${ISSUES_SERIOUS} |
275
+ | Moderate | ${ISSUES_MODERATE} |
276
+ | Total issues | ${TOTAL_ISSUES} |
277
+
278
+ ## Findings
279
+
280
+ | File | Line | Severity | Category | WCAG SC | Description |
281
+ | --- | --- | --- | --- | --- | --- |
282
+ REPORT_HEAD
283
+
284
+ echo "$REPORT_CONTENT" >> "$REPORT_FILE"
285
+
286
+ cat >> "$REPORT_FILE" <<REPORT_TAIL
287
+
288
+ ## Source References (W3C)
289
+
290
+ - https://www.w3.org/WAI/standards-guidelines/wcag/
291
+ - https://www.w3.org/TR/WCAG22/
292
+ - https://www.w3.org/WAI/WCAG22/quickref/
293
+ - https://www.w3.org/WAI/WCAG22/Understanding/
294
+ - https://www.w3.org/developers/tools/
295
+ REPORT_TAIL
296
+ }
297
+
298
+ if ! $JSON_MODE; then
299
+ echo -e "${BOLD}WCAG 2.2 Level AA accessibility audit${RESET}"
300
+ echo -e "Target: ${CYAN}${TARGET}${RESET}"
301
+ fi
302
+
303
+ MARKUP_FILES=()
304
+ STYLE_FILES=()
305
+
306
+ if [[ -f "$TARGET" ]]; then
307
+ case "$TARGET" in
308
+ *.html|*.htm|*.xhtml|*.jsx|*.tsx) MARKUP_FILES+=("$TARGET") ;;
309
+ *.css|*.scss|*.sass|*.less) STYLE_FILES+=("$TARGET") ;;
310
+ esac
311
+ else
312
+ while IFS= read -r f; do MARKUP_FILES+=("$f"); done < <(
313
+ find "$TARGET" -type f \( -name "*.html" -o -name "*.htm" -o -name "*.xhtml" -o -name "*.jsx" -o -name "*.tsx" \) \
314
+ -not -path "*/node_modules/*" \
315
+ -not -path "*/.git/*" \
316
+ -not -path "*/dist/*" \
317
+ -not -path "*/build/*" \
318
+ -not -path "*/coverage/*" \
319
+ -not -path "*/.next/*" \
320
+ -not -path "*/storybook-static/*" \
321
+ | sort
322
+ )
323
+
324
+ while IFS= read -r f; do STYLE_FILES+=("$f"); done < <(
325
+ find "$TARGET" -type f \( -name "*.css" -o -name "*.scss" -o -name "*.sass" -o -name "*.less" \) \
326
+ -not -path "*/node_modules/*" \
327
+ -not -path "*/.git/*" \
328
+ -not -path "*/dist/*" \
329
+ -not -path "*/build/*" \
330
+ -not -path "*/coverage/*" \
331
+ -not -path "*/.next/*" \
332
+ -not -path "*/storybook-static/*" \
333
+ | sort
334
+ )
335
+ fi
336
+
337
+ for file in "${MARKUP_FILES[@]:-}"; do
338
+ [[ -n "$file" ]] && audit_markup_file "$file"
339
+ done
340
+
341
+ for file in "${STYLE_FILES[@]:-}"; do
342
+ [[ -n "$file" ]] && audit_style_file "$file"
343
+ done
344
+
345
+ if $REPORT_MODE; then
346
+ generate_report
347
+ fi
348
+
349
+ if $JSON_MODE; then
350
+ cat <<JSON
351
+ {"standard":"WCAG 2.2 Level AA","target":"$(json_escape "$TARGET")","summary":{"filesScanned":${FILES_SCANNED},"filesWithIssues":${FILES_WITH_ISSUES},"critical":${ISSUES_CRITICAL},"serious":${ISSUES_SERIOUS},"moderate":${ISSUES_MODERATE},"total":${TOTAL_ISSUES}},"findings":[${JSON_ENTRIES}]}
352
+ JSON
353
+ else
354
+ echo ""
355
+ echo -e "${BOLD}Summary${RESET}"
356
+ echo "Files scanned: $FILES_SCANNED"
357
+ echo "Files with issues: $FILES_WITH_ISSUES"
358
+ echo "Critical: $ISSUES_CRITICAL"
359
+ echo "Serious: $ISSUES_SERIOUS"
360
+ echo "Moderate: $ISSUES_MODERATE"
361
+ echo "Total: $TOTAL_ISSUES"
362
+ if $REPORT_MODE; then
363
+ echo "Report: $REPORT_FILE"
364
+ fi
365
+ if [[ "$TOTAL_ISSUES" -eq 0 ]]; then
366
+ echo -e "${GREEN}No issues found by this heuristic scanner.${RESET}"
367
+ fi
368
+ fi
369
+
370
+ if [[ "$TOTAL_ISSUES" -gt 0 ]]; then
371
+ exit 1
372
+ fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skilly-hand/skilly-hand",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "license": "CC-BY-NC-4.0",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import fs from "node:fs";
2
3
  import path from "node:path";
3
4
  import { createRequire } from "node:module";
4
- import { pathToFileURL } from "node:url";
5
+ import { fileURLToPath } from "node:url";
5
6
  import { checkbox as inquirerCheckbox, confirm as inquirerConfirm, select as inquirerSelect } from "@inquirer/prompts";
6
7
  import { loadAllSkills } from "../../catalog/src/index.js";
7
8
  import {
@@ -19,7 +20,17 @@ const { version } = require("../../../package.json");
19
20
 
20
21
  function isExecutedDirectly(metaUrl, argv1) {
21
22
  if (!argv1) return false;
22
- return metaUrl === pathToFileURL(argv1).href;
23
+ const normalizePath = (filePath) => {
24
+ const absolutePath = path.resolve(filePath);
25
+ try {
26
+ const realPath = fs.realpathSync.native ? fs.realpathSync.native(absolutePath) : fs.realpathSync(absolutePath);
27
+ return path.normalize(realPath);
28
+ } catch {
29
+ return path.normalize(absolutePath);
30
+ }
31
+ };
32
+
33
+ return normalizePath(fileURLToPath(metaUrl)) === normalizePath(argv1);
23
34
  }
24
35
 
25
36
  export function parseArgs(argv) {