figma-local 1.9.0 → 2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figma-local",
3
- "version": "1.9.0",
3
+ "version": "2.1.0",
4
4
  "description": "Control Figma Desktop with Claude Code. Smart read, write, and AI-prompt export. No API key required.",
5
5
  "author": "elvke",
6
6
  "license": "MIT",
@@ -0,0 +1,110 @@
1
+ ---
2
+ name: figma-component-audit
3
+ description: |
4
+ Use this skill when the user wants to audit, review, or check the quality of Figma components. Triggers on: "audit", "audit components", "audit all components", "check component quality", "what's wrong with my components", "component issues", "find problems in components", "review my design system components", "component health", "component score", "check for hardcoded colors", "missing descriptions", "detached instances", "incomplete variants". Requires a Figma file to be open and the daemon connected.
5
+ allowed-tools:
6
+ - Bash(fig component-audit *)
7
+ - Bash(fig component-audit)
8
+ - Bash(fig daemon status)
9
+ ---
10
+
11
+ # Figma Component Audit
12
+
13
+ Audit Figma components for design-system quality issues. Returns a score (0–100) and a categorized list of issues per component.
14
+
15
+ ## Prerequisites
16
+
17
+ The `fig` CLI must be connected. Check with `fig daemon status`. If not connected: `fig connect --safe`.
18
+
19
+ ## Usage
20
+
21
+ ### Audit current selection
22
+
23
+ Select a component or frame in Figma, then:
24
+
25
+ ```bash
26
+ fig component-audit
27
+ ```
28
+
29
+ ### Audit a specific component by name
30
+
31
+ ```bash
32
+ fig component-audit "Button"
33
+ fig component-audit "Card"
34
+ fig component-audit "Navigation Bar"
35
+ ```
36
+
37
+ ### Audit ALL components on the current page
38
+
39
+ ```bash
40
+ fig component-audit --all
41
+ ```
42
+
43
+ This is the most useful command for a full design-system review. It scans every `COMPONENT` and `COMPONENT_SET` on the page, ranks them by score (worst first), and prints a summary.
44
+
45
+ ### Audit by node ID
46
+
47
+ ```bash
48
+ fig component-audit --node "123:456"
49
+ ```
50
+
51
+ ### JSON output (for piping or saving)
52
+
53
+ ```bash
54
+ fig component-audit --all --json
55
+ fig component-audit --all --json > audit-report.json
56
+ ```
57
+
58
+ ### Include info-level issues
59
+
60
+ By default, only errors and warnings are shown. Add `--verbose` to also see info-level suggestions:
61
+
62
+ ```bash
63
+ fig component-audit --all --verbose
64
+ fig component-audit "Button" --verbose
65
+ ```
66
+
67
+ ## What Gets Checked
68
+
69
+ | Rule | Severity | What it flags |
70
+ |------|----------|---------------|
71
+ | `missing-description` | warning | Component has no description set |
72
+ | `incomplete-variants` | warning | Component set is missing expected variant combinations |
73
+ | `hidden-layer` | info | A child layer is hidden (dead weight in the file) |
74
+ | `generic-layer-name` | info | Layer has a default name like "Frame 2" or "Rectangle" |
75
+ | `empty-text` | warning | A text node exists but has no content |
76
+ | `hardcoded-color` | warning | A solid fill color with no variable binding |
77
+ | `no-auto-layout` | info | A frame with 2+ children but no auto layout enabled |
78
+ | `detached-instance` | error | An instance whose main component is missing |
79
+ | `deep-nesting` | info | A node nested 7+ levels deep |
80
+
81
+ ## Scoring
82
+
83
+ `score = 100 − (errors × 15) − (warnings × 5) − (info × 2)`
84
+
85
+ - **≥ 80** — Good
86
+ - **60–79** — Fair
87
+ - **< 60** — Needs work
88
+
89
+ ## Workflow: Full Design-System Audit
90
+
91
+ 1. Open the Figma file containing your component library
92
+ 2. Make sure `fig` is connected: `fig daemon status`
93
+ 3. Run the full audit:
94
+ ```bash
95
+ fig component-audit --all
96
+ ```
97
+ 4. Review the output — components sorted worst-first
98
+ 5. For a detailed look at the worst component:
99
+ ```bash
100
+ fig component-audit "ComponentName" --verbose
101
+ ```
102
+ 6. Fix the issues in Figma, then re-run to verify improvement
103
+
104
+ ## Tips
105
+
106
+ - Run `--all --json` to save a baseline report and compare over time
107
+ - `detached-instance` errors (score −15 each) are the highest priority to fix
108
+ - `hardcoded-color` warnings usually mean a token should be created in your variable collection
109
+ - Use `fig var list` to see available variable collections before fixing hardcoded colors
110
+ - `incomplete-variants` means your component set has a property with N options but fewer than the expected NxM combinations
@@ -11,162 +11,145 @@ allowed-tools:
11
11
 
12
12
  # Figma Library
13
13
 
14
- Access team library components and variables from other Figma files. Index libraries, search components by name, import by key, and browse design tokens.
14
+ Access team library components and variables from other Figma files. Index libraries via REST API or plugin, search components by name, import by key, and browse design tokens.
15
15
 
16
16
  ## Prerequisites
17
17
 
18
18
  - The `fig` CLI must be connected: `fig daemon status`. If not: `fig connect --safe`.
19
19
  - The Figma plugin must have `teamlibrary` permission in its manifest. If library commands fail with a permission error, re-import the plugin in Figma (Plugins → Development → Import from manifest).
20
- - Libraries must be enabled in the current Figma file (check Assets panel → team library icon).
21
20
 
22
21
  ## IMPORTANT: How to access library components
23
22
 
24
- Figma's plugin API does **not** allow browsing library components directly. To work with library components, you must **index** them first:
23
+ Figma's plugin API does **not** allow browsing library components directly. To work with library components, you must **index** them first. There are three ways to index:
25
24
 
26
- 1. Open the **library file** in Figma (the file that contains the components)
27
- 2. Run `fig library index` — this scans all pages and saves every component with its key
28
- 3. Switch to your **working file**
29
- 4. Run `fig library search --name "button"` — finds components from indexed libraries
30
- 5. Run `fig library import --key "<key>"` — imports the component
25
+ ### Option A: REST API (recommended for large files)
31
26
 
32
- ## Usage
27
+ No plugin needed. Works on any file size without hanging Figma.
33
28
 
34
- ### Index a library (run in the library file)
29
+ **First time provide token and file URL:**
30
+ ```bash
31
+ fig library index --api --token "figd_xxxxx" --file "https://www.figma.com/design/ABC123/MyFile"
32
+ ```
35
33
 
36
- Open the library/design system file in Figma, then:
34
+ The token is saved for future use. Get one from: Figma → Settings → Personal Access Tokens.
37
35
 
36
+ **After first time — just provide the file URL:**
38
37
  ```bash
39
- fig library index
38
+ fig library index --api --file "https://www.figma.com/design/ABC123/MyFile"
40
39
  ```
41
40
 
42
- This scans ALL pages in the file and saves every component (name, key, description, page, size, component set) to `~/.figma-local/libraries/<filename>.json`.
41
+ ### Option B: Page-by-page (for large files via plugin)
43
42
 
44
- Re-run this command after the library is updated to refresh the index.
45
-
46
- ### Search indexed libraries
47
-
48
- Search across all indexed libraries by component name:
43
+ Open the library file in Figma, then scan one page at a time:
49
44
 
50
45
  ```bash
51
- fig library search --name "button"
52
- fig library search --name "input"
53
- fig library search --name "card"
54
- fig library search --name "checkbox"
46
+ fig library index --page "Buttons"
47
+ fig library index --page "Inputs"
48
+ fig library index --page "Cards"
55
49
  ```
56
50
 
57
- Returns: component name, key (for importing), component set, page, size, and library name.
51
+ Each page's components are merged into the same index file. This avoids hanging on large files.
52
+
53
+ ### Option C: Full scan (small files only)
58
54
 
59
- Use `--json` for structured output:
55
+ Open the library file in Figma, then:
60
56
 
61
57
  ```bash
62
- fig library search --name "button" --json
58
+ fig library index
63
59
  ```
64
60
 
65
- ### List indexed libraries
61
+ **Warning:** This scans ALL pages at once. Only use on small files — large design systems will cause Figma to hang.
66
62
 
67
- See what libraries have been indexed:
63
+ ## After indexing: Search and Import
64
+
65
+ ### Search indexed libraries
68
66
 
69
67
  ```bash
70
- fig library list
68
+ fig library search --name "button"
69
+ fig library search --name "input"
70
+ fig library search --name "card"
71
+ fig library search --name "checkbox"
71
72
  ```
72
73
 
73
- Returns: library name, component count, page count, and when it was indexed.
74
-
75
- ### Import a component by key
74
+ Returns: component name, key (for importing), component set, page, and library name.
76
75
 
77
- Import a library component onto the canvas by its key:
76
+ ### List indexed libraries
78
77
 
79
78
  ```bash
80
- fig library import --key "abc123def456..."
79
+ fig library list
81
80
  ```
82
81
 
83
- Import and rename:
82
+ ### Import by key
84
83
 
85
84
  ```bash
86
- fig library import --key "abc123def456..." --name "PrimaryButton"
85
+ fig library import --key "<key-from-search>"
86
+ fig library import --key "<key>" --name "PrimaryButton"
87
87
  ```
88
88
 
89
- The imported component instance is placed at the viewport center and selected.
89
+ The component is placed at viewport center and selected.
90
90
 
91
- ### List library variable collections
92
-
93
- See what variable collections are available from linked libraries:
91
+ ### Inspect the imported component
94
92
 
95
93
  ```bash
96
- fig library collections
94
+ fig inspect --deep
97
95
  ```
98
96
 
99
- ### List library variables
97
+ ## Variables (no indexing needed)
100
98
 
101
- Browse all variables across all linked library collections:
99
+ Library variables are available directly via the plugin API:
102
100
 
103
101
  ```bash
104
- fig library variables
105
- fig library variables --name "color"
106
- fig library variables --name "spacing"
102
+ fig library collections # List variable collections
103
+ fig library variables # List all variables
104
+ fig library variables --name "color" # Search by name
107
105
  ```
108
106
 
109
- ### List components on current page
107
+ ## Components on current page
110
108
 
111
- Find library components that are already used on the current page:
109
+ Find library components already dragged onto the current page:
112
110
 
113
111
  ```bash
114
112
  fig library components
115
113
  fig library components --name "button"
116
114
  ```
117
115
 
118
- ### JSON output
119
-
120
- All commands support `--json` for structured output.
116
+ ## Full workflow
121
117
 
122
- ## Workflow: Building UI with library components
123
-
124
- 1. **Index** the library (one-time, in the library file):
118
+ 1. **Index** the library (pick one method):
125
119
  ```bash
126
- fig library index
120
+ # Best for large files:
121
+ fig library index --api --token "figd_..." --file "https://..."
122
+ # Or page by page:
123
+ fig library index --page "Buttons"
127
124
  ```
128
125
 
129
- 2. **Switch** to your working file in Figma.
130
-
131
- 3. **Search** for the components you need:
126
+ 2. **Search** for components:
132
127
  ```bash
133
- fig library search --name "button"
134
- fig library search --name "input"
128
+ fig library search --name "button" --json
135
129
  ```
136
130
 
137
- 4. **Import** each component by key:
131
+ 3. **Import** into your working file:
138
132
  ```bash
139
- fig library import --key "<key-from-search>"
133
+ fig library import --key "<key>"
140
134
  ```
141
135
 
142
- 5. **Inspect** the imported component to get its full specs:
136
+ 4. **Inspect** for full specs:
143
137
  ```bash
144
138
  fig inspect --deep
145
139
  ```
146
140
 
147
- 6. **Get variables** for design tokens:
141
+ 5. **Get variables** for tokens:
148
142
  ```bash
149
143
  fig library variables --name "primary" --json
150
144
  ```
151
145
 
152
- 7. **Replicate** in code using the exact specs and token values.
153
-
154
- ## Workflow: Extracting a full design system
155
-
156
- 1. Open the design system file → `fig library index`
157
- 2. Get all components: `fig library search --name "" --json`
158
- 3. Get all variables: `fig library variables --json`
159
- 4. Import key components one by one and document them:
160
- ```bash
161
- fig library import --key "<key>"
162
- fig document --json
163
- ```
146
+ 6. **Replicate** in code.
164
147
 
165
148
  ## Tips
166
149
 
167
- - **Index once, search many times** the index is saved locally and persists across sessions.
168
- - Re-index when the library is updated: open the library file → `fig library index`.
169
- - `fig library components` only finds components already on the current page. Use `search` for the full library.
170
- - After importing a component, use `fig inspect --deep` to get full specs including variable bindings.
150
+ - **REST API is fastest** for large design systems no file opening needed.
151
+ - Token is saved after first use in `~/.figma-local/figma-token`.
152
+ - Page-by-page indexing merges results run multiple times to build up the index.
153
+ - Re-index when the library is updated to get new components.
154
+ - `fig library search --name "" --json` returns ALL indexed components.
171
155
  - Component keys are stable across file versions — save them for repeated imports.
172
- - Use `fig library search --name "button" --json | jq '.[].key'` to extract just the keys.
@@ -0,0 +1,312 @@
1
+ /**
2
+ * component-audit.js — Figma component audit logic
3
+ *
4
+ * Generates Figma plugin JS code that inspects components for:
5
+ * - Naming issues (unnamed layers, generic names)
6
+ * - Missing descriptions
7
+ * - Hardcoded colors (no variable bindings)
8
+ * - Missing auto layout
9
+ * - Hidden layers (dead weight)
10
+ * - Empty text nodes
11
+ * - Excessive nesting depth (>6 levels)
12
+ * - Variant completeness for component sets
13
+ * - Detached instances within a component
14
+ */
15
+
16
+ /**
17
+ * Build the Figma JS code to audit a single component node by ID.
18
+ * @param {string} nodeId
19
+ */
20
+ export function buildSingleAuditCode(nodeId) {
21
+ return `
22
+ (function() {
23
+ var node = figma.getNodeById(${JSON.stringify(nodeId)});
24
+ if (!node) return { error: 'Node not found: ${nodeId}' };
25
+ if (node.type !== 'COMPONENT' && node.type !== 'COMPONENT_SET' && node.type !== 'FRAME') {
26
+ return { error: 'Node is not a COMPONENT, COMPONENT_SET, or FRAME. Got: ' + node.type };
27
+ }
28
+ return auditComponent(node);
29
+ ${AUDIT_HELPERS}
30
+ })()
31
+ `;
32
+ }
33
+
34
+ /**
35
+ * Build the Figma JS code to audit ALL components on the current page.
36
+ */
37
+ export function buildAllAuditCode() {
38
+ return `
39
+ (function() {
40
+ var page = figma.currentPage;
41
+ var results = [];
42
+
43
+ function collectComponents(node) {
44
+ if (node.type === 'COMPONENT_SET') {
45
+ results.push(auditComponent(node));
46
+ return; // children are COMPONENT variants — covered by set audit
47
+ }
48
+ if (node.type === 'COMPONENT') {
49
+ // Skip components that are children of a COMPONENT_SET (audited via the set)
50
+ if (!node.parent || node.parent.type !== 'COMPONENT_SET') {
51
+ results.push(auditComponent(node));
52
+ }
53
+ return;
54
+ }
55
+ if (node.children) {
56
+ for (var i = 0; i < node.children.length; i++) {
57
+ collectComponents(node.children[i]);
58
+ }
59
+ }
60
+ }
61
+
62
+ collectComponents(page);
63
+ return {
64
+ page: page.name,
65
+ total: results.length,
66
+ components: results
67
+ };
68
+ ${AUDIT_HELPERS}
69
+ })()
70
+ `;
71
+ }
72
+
73
+ /**
74
+ * Build the Figma JS code to audit the current selection.
75
+ */
76
+ export function buildSelectionAuditCode() {
77
+ return `
78
+ (function() {
79
+ var sel = figma.currentPage.selection;
80
+ if (!sel || sel.length === 0) return { error: 'Nothing selected. Select a component or frame in Figma first.' };
81
+ var node = sel[0];
82
+ if (node.type !== 'COMPONENT' && node.type !== 'COMPONENT_SET' && node.type !== 'FRAME') {
83
+ return { error: 'Selection is not a COMPONENT, COMPONENT_SET, or FRAME. Got: ' + node.type };
84
+ }
85
+ return auditComponent(node);
86
+ ${AUDIT_HELPERS}
87
+ })()
88
+ `;
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Shared helper code injected into every eval string.
93
+ // Written as a plain string so it can be appended inside the IIFE.
94
+ // ---------------------------------------------------------------------------
95
+ const AUDIT_HELPERS = `
96
+ function auditComponent(node) {
97
+ var issues = [];
98
+ var stats = { textNodes: 0, hiddenNodes: 0, instances: 0, detachedInstances: 0, maxDepth: 0 };
99
+
100
+ // ── 1. Description check ─────────────────────────────────────────────────
101
+ if (node.type === 'COMPONENT' || node.type === 'COMPONENT_SET') {
102
+ if (!node.description || node.description.trim() === '') {
103
+ issues.push({ rule: 'missing-description', severity: 'warning', message: 'Component has no description' });
104
+ }
105
+ }
106
+
107
+ // ── 2. Variant completeness (COMPONENT_SET only) ─────────────────────────
108
+ if (node.type === 'COMPONENT_SET') {
109
+ var propDefs = node.componentPropertyDefinitions || {};
110
+ var propKeys = Object.keys(propDefs);
111
+ var variantProps = propKeys.filter(function(k) { return propDefs[k].type === 'VARIANT'; });
112
+ if (variantProps.length > 0) {
113
+ var expected = 1;
114
+ variantProps.forEach(function(k) {
115
+ expected *= (propDefs[k].variantOptions || []).length;
116
+ });
117
+ var actual = (node.children || []).length;
118
+ if (actual < expected) {
119
+ issues.push({
120
+ rule: 'incomplete-variants',
121
+ severity: 'warning',
122
+ message: 'Variant set has ' + actual + ' of ' + expected + ' expected combinations',
123
+ details: { expected: expected, actual: actual, properties: variantProps }
124
+ });
125
+ }
126
+ }
127
+ }
128
+
129
+ // ── 3. Deep tree walk ────────────────────────────────────────────────────
130
+ var GENERIC_NAMES = /^(Frame|Rectangle|Ellipse|Group|Vector|Polygon|Star|Line|Image|Component)\\s*\\d*$/i;
131
+
132
+ function walk(n, depth) {
133
+ if (depth > stats.maxDepth) stats.maxDepth = depth;
134
+
135
+ // Hidden layers
136
+ if (depth > 0 && n.visible === false) {
137
+ stats.hiddenNodes++;
138
+ issues.push({ rule: 'hidden-layer', severity: 'info', message: 'Hidden layer: "' + n.name + '"', nodeId: n.id });
139
+ }
140
+
141
+ // Generic / unnamed layer
142
+ if (depth > 0 && GENERIC_NAMES.test(n.name)) {
143
+ issues.push({ rule: 'generic-layer-name', severity: 'info', message: 'Generic layer name: "' + n.name + '"', nodeId: n.id });
144
+ }
145
+
146
+ // Text nodes
147
+ if (n.type === 'TEXT') {
148
+ stats.textNodes++;
149
+ if (!n.characters || n.characters.trim() === '') {
150
+ issues.push({ rule: 'empty-text', severity: 'warning', message: 'Empty text node: "' + n.name + '"', nodeId: n.id });
151
+ }
152
+ }
153
+
154
+ // Hardcoded colors — fills with no variable binding
155
+ if (n.fills && Array.isArray(n.fills)) {
156
+ for (var i = 0; i < n.fills.length; i++) {
157
+ var fill = n.fills[i];
158
+ if (fill.type === 'SOLID' && fill.visible !== false) {
159
+ var hasBinding = n.boundVariables && n.boundVariables.fills;
160
+ if (!hasBinding) {
161
+ var r = Math.round((fill.color.r || 0) * 255);
162
+ var g = Math.round((fill.color.g || 0) * 255);
163
+ var b = Math.round((fill.color.b || 0) * 255);
164
+ var hex = '#' + r.toString(16).padStart(2,'0') + g.toString(16).padStart(2,'0') + b.toString(16).padStart(2,'0');
165
+ // Only flag non-transparent, non-white fills that look like intentional colors
166
+ if (hex !== '#ffffff' && hex !== '#000000' && !(r === g && g === b)) {
167
+ issues.push({ rule: 'hardcoded-color', severity: 'warning', message: 'Hardcoded fill color ' + hex + ' on "' + n.name + '" — consider using a variable', nodeId: n.id });
168
+ }
169
+ }
170
+ }
171
+ }
172
+ }
173
+
174
+ // Missing auto layout on FRAME nodes that contain multiple children
175
+ if (n.type === 'FRAME' && depth > 0) {
176
+ var childCount = (n.children || []).length;
177
+ if (childCount >= 2 && n.layoutMode === 'NONE') {
178
+ issues.push({ rule: 'no-auto-layout', severity: 'info', message: 'Frame "' + n.name + '" has ' + childCount + ' children but no auto layout', nodeId: n.id });
179
+ }
180
+ }
181
+
182
+ // Instances (check for detached)
183
+ if (n.type === 'INSTANCE') {
184
+ stats.instances++;
185
+ if (!n.mainComponent) {
186
+ stats.detachedInstances++;
187
+ issues.push({ rule: 'detached-instance', severity: 'error', message: 'Detached instance: "' + n.name + '" — main component missing', nodeId: n.id });
188
+ }
189
+ }
190
+
191
+ // Excessive nesting
192
+ if (depth === 7) {
193
+ issues.push({ rule: 'deep-nesting', severity: 'info', message: 'Node "' + n.name + '" is nested 7+ levels deep', nodeId: n.id });
194
+ }
195
+
196
+ if (n.children) {
197
+ for (var ci = 0; ci < n.children.length; ci++) {
198
+ walk(n.children[ci], depth + 1);
199
+ }
200
+ }
201
+ }
202
+
203
+ walk(node, 0);
204
+
205
+ // ── 4. Score ─────────────────────────────────────────────────────────────
206
+ var errors = issues.filter(function(i) { return i.severity === 'error'; }).length;
207
+ var warnings = issues.filter(function(i) { return i.severity === 'warning'; }).length;
208
+ var infos = issues.filter(function(i) { return i.severity === 'info'; }).length;
209
+ var score = Math.max(0, 100 - errors * 15 - warnings * 5 - infos * 2);
210
+
211
+ return {
212
+ id: node.id,
213
+ name: node.name,
214
+ type: node.type,
215
+ score: score,
216
+ summary: { errors: errors, warnings: warnings, info: infos },
217
+ stats: stats,
218
+ issues: issues
219
+ };
220
+ }
221
+ `;
222
+
223
+ // ---------------------------------------------------------------------------
224
+ // Formatter — turns raw audit result into human-readable CLI output
225
+ // ---------------------------------------------------------------------------
226
+
227
+ /**
228
+ * Format a single component audit result for terminal output.
229
+ * @param {object} result - from auditComponent()
230
+ * @param {object} chalk - chalk instance
231
+ * @param {boolean} verbose - show info-level issues
232
+ */
233
+ export function formatAuditResult(result, chalk, verbose = true) {
234
+ if (result.error) return chalk.red('✗ ' + result.error);
235
+
236
+ const lines = [];
237
+ const scoreColor = result.score >= 80 ? chalk.green : result.score >= 60 ? chalk.yellow : chalk.red;
238
+ const scoreLabel = result.score >= 80 ? 'Good' : result.score >= 60 ? 'Fair' : 'Needs work';
239
+
240
+ lines.push(`\n${chalk.bold(result.name)} ${chalk.gray('(' + result.type + ')')}`);
241
+ lines.push(
242
+ ` Score: ${scoreColor(result.score + '/100')} ${chalk.gray('(' + scoreLabel + ')')} ` +
243
+ chalk.red(result.summary.errors + ' errors') + ' ' +
244
+ chalk.yellow(result.summary.warnings + ' warnings') + ' ' +
245
+ chalk.gray(result.summary.info + ' info')
246
+ );
247
+ lines.push(
248
+ chalk.gray(
249
+ ` Stats: ${result.stats.textNodes} text nodes, ${result.stats.instances} instances` +
250
+ (result.stats.detachedInstances ? chalk.red(` (${result.stats.detachedInstances} detached)`) : '') +
251
+ `, ${result.stats.hiddenNodes} hidden, max depth ${result.stats.maxDepth}`
252
+ )
253
+ );
254
+
255
+ const shown = result.issues.filter(i => verbose || i.severity !== 'info');
256
+ if (shown.length === 0) {
257
+ lines.push(chalk.green(' ✓ No issues found'));
258
+ } else {
259
+ lines.push('');
260
+ for (const issue of shown) {
261
+ const icon = issue.severity === 'error' ? chalk.red('✗') :
262
+ issue.severity === 'warning' ? chalk.yellow('⚠') : chalk.gray('ℹ');
263
+ const ruleTag = chalk.gray(`[${issue.rule}]`);
264
+ lines.push(` ${icon} ${issue.message} ${ruleTag}`);
265
+ }
266
+ }
267
+
268
+ return lines.join('\n');
269
+ }
270
+
271
+ /**
272
+ * Format the all-components audit result for terminal output.
273
+ * @param {object} result - { page, total, components[] }
274
+ * @param {object} chalk
275
+ * @param {boolean} verbose
276
+ */
277
+ export function formatAllAuditResult(result, chalk, verbose = false) {
278
+ if (result.error) return chalk.red('✗ ' + result.error);
279
+
280
+ const lines = [];
281
+ const comps = result.components || [];
282
+ const totalErrors = comps.reduce((s, c) => s + c.summary.errors, 0);
283
+ const totalWarnings = comps.reduce((s, c) => s + c.summary.warnings, 0);
284
+ const avgScore = comps.length ? Math.round(comps.reduce((s, c) => s + c.score, 0) / comps.length) : 0;
285
+
286
+ lines.push('');
287
+ lines.push(chalk.bold(`Component Audit — ${result.page}`));
288
+ lines.push(
289
+ ` ${comps.length} component${comps.length !== 1 ? 's' : ''} scanned ` +
290
+ chalk.red(totalErrors + ' errors') + ' ' +
291
+ chalk.yellow(totalWarnings + ' warnings') + ' ' +
292
+ `Avg score: ${avgScore}/100`
293
+ );
294
+ lines.push('');
295
+
296
+ // Sort: worst score first
297
+ const sorted = [...comps].sort((a, b) => a.score - b.score);
298
+
299
+ for (const comp of sorted) {
300
+ lines.push(formatAuditResult(comp, chalk, verbose));
301
+ }
302
+
303
+ lines.push('');
304
+ lines.push(chalk.gray('─'.repeat(60)));
305
+ lines.push(
306
+ `${comps.filter(c => c.score >= 80).length} good ` +
307
+ `${comps.filter(c => c.score >= 60 && c.score < 80).length} fair ` +
308
+ `${comps.filter(c => c.score < 60).length} need work`
309
+ );
310
+
311
+ return lines.join('\n');
312
+ }