@luquimbo/bi-superpowers 3.1.1 → 4.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 (186) hide show
  1. package/.claude-plugin/marketplace.json +5 -3
  2. package/.claude-plugin/plugin.json +28 -2
  3. package/.claude-plugin/skill-manifest.json +22 -6
  4. package/.plugin/plugin.json +1 -1
  5. package/AGENTS.md +52 -36
  6. package/CHANGELOG.md +295 -0
  7. package/README.md +75 -26
  8. package/bin/build-plugin.js +17 -10
  9. package/bin/cli.js +278 -322
  10. package/bin/commands/build-desktop.js +35 -16
  11. package/bin/commands/diff.js +31 -13
  12. package/bin/commands/install.js +93 -72
  13. package/bin/commands/lint.js +40 -26
  14. package/bin/commands/mcp-setup.js +3 -10
  15. package/bin/commands/update-check.js +389 -0
  16. package/bin/lib/agents.js +19 -0
  17. package/bin/lib/generators/claude-plugin.js +144 -6
  18. package/bin/lib/generators/shared.js +29 -33
  19. package/bin/lib/mcp-config.js +191 -16
  20. package/bin/lib/skills.js +115 -27
  21. package/bin/postinstall.js +4 -2
  22. package/bin/utils/mcp-detect.js +2 -2
  23. package/commands/bi-start.md +218 -0
  24. package/commands/pbi-connect.md +43 -65
  25. package/commands/project-kickoff.md +393 -673
  26. package/commands/report-design.md +403 -0
  27. package/desktop-extension/manifest.json +5 -12
  28. package/desktop-extension/server.js +34 -25
  29. package/package.json +6 -10
  30. package/skills/bi-start/SKILL.md +220 -0
  31. package/skills/bi-start/scripts/update-check.js +389 -0
  32. package/skills/pbi-connect/SKILL.md +45 -67
  33. package/skills/pbi-connect/scripts/update-check.js +389 -0
  34. package/skills/project-kickoff/SKILL.md +395 -675
  35. package/skills/project-kickoff/scripts/update-check.js +389 -0
  36. package/skills/report-design/SKILL.md +405 -0
  37. package/skills/report-design/references/cli-commands.md +184 -0
  38. package/skills/report-design/references/cli-setup.md +101 -0
  39. package/skills/report-design/references/close-write-open-pattern.md +80 -0
  40. package/skills/report-design/references/layouts/finance.md +65 -0
  41. package/skills/report-design/references/layouts/generic.md +46 -0
  42. package/skills/report-design/references/layouts/hr.md +48 -0
  43. package/skills/report-design/references/layouts/marketing.md +45 -0
  44. package/skills/report-design/references/layouts/operations.md +44 -0
  45. package/skills/report-design/references/layouts/sales.md +50 -0
  46. package/skills/report-design/references/native-visuals.md +341 -0
  47. package/skills/report-design/references/pbi-desktop-installation.md +87 -0
  48. package/skills/report-design/references/pbir-preview-activation.md +40 -0
  49. package/skills/report-design/references/slicer.md +89 -0
  50. package/skills/report-design/references/textbox.md +101 -0
  51. package/skills/report-design/references/themes/BISuperpowers.json +915 -0
  52. package/skills/report-design/references/troubleshooting.md +135 -0
  53. package/skills/report-design/references/visual-types.md +78 -0
  54. package/skills/report-design/scripts/apply-theme.js +243 -0
  55. package/skills/report-design/scripts/create-visual.js +878 -0
  56. package/skills/report-design/scripts/ensure-pbi-cli.sh +41 -0
  57. package/skills/report-design/scripts/update-check.js +389 -0
  58. package/skills/report-design/scripts/validate-pbir.js +322 -0
  59. package/src/content/base.md +12 -68
  60. package/src/content/mcp-requirements.json +0 -25
  61. package/src/content/routing.md +19 -74
  62. package/src/content/skills/bi-start.md +191 -0
  63. package/src/content/skills/pbi-connect.md +22 -65
  64. package/src/content/skills/project-kickoff.md +372 -673
  65. package/src/content/skills/report-design/SKILL.md +376 -0
  66. package/src/content/skills/report-design/references/cli-commands.md +184 -0
  67. package/src/content/skills/report-design/references/cli-setup.md +101 -0
  68. package/src/content/skills/report-design/references/close-write-open-pattern.md +80 -0
  69. package/src/content/skills/report-design/references/layouts/finance.md +65 -0
  70. package/src/content/skills/report-design/references/layouts/generic.md +46 -0
  71. package/src/content/skills/report-design/references/layouts/hr.md +48 -0
  72. package/src/content/skills/report-design/references/layouts/marketing.md +45 -0
  73. package/src/content/skills/report-design/references/layouts/operations.md +44 -0
  74. package/src/content/skills/report-design/references/layouts/sales.md +50 -0
  75. package/src/content/skills/report-design/references/native-visuals.md +341 -0
  76. package/src/content/skills/report-design/references/pbi-desktop-installation.md +87 -0
  77. package/src/content/skills/report-design/references/pbir-preview-activation.md +40 -0
  78. package/src/content/skills/report-design/references/slicer.md +89 -0
  79. package/src/content/skills/report-design/references/textbox.md +101 -0
  80. package/src/content/skills/report-design/references/themes/BISuperpowers.json +915 -0
  81. package/src/content/skills/report-design/references/troubleshooting.md +135 -0
  82. package/src/content/skills/report-design/references/visual-types.md +78 -0
  83. package/src/content/skills/report-design/scripts/apply-theme.js +243 -0
  84. package/src/content/skills/report-design/scripts/create-visual.js +878 -0
  85. package/src/content/skills/report-design/scripts/ensure-pbi-cli.sh +41 -0
  86. package/src/content/skills/report-design/scripts/validate-pbir.js +322 -0
  87. package/bin/commands/add.js +0 -533
  88. package/bin/commands/add.test.js +0 -77
  89. package/bin/commands/changelog.js +0 -443
  90. package/bin/commands/install.test.js +0 -289
  91. package/bin/commands/lint.test.js +0 -103
  92. package/bin/commands/pull.js +0 -287
  93. package/bin/commands/pull.test.js +0 -36
  94. package/bin/commands/push.js +0 -231
  95. package/bin/commands/push.test.js +0 -14
  96. package/bin/commands/search.js +0 -344
  97. package/bin/commands/search.test.js +0 -115
  98. package/bin/commands/setup.js +0 -545
  99. package/bin/commands/setup.test.js +0 -46
  100. package/bin/commands/sync-profile.js +0 -405
  101. package/bin/commands/sync-profile.test.js +0 -14
  102. package/bin/commands/sync-source.js +0 -418
  103. package/bin/commands/sync-source.test.js +0 -14
  104. package/bin/lib/generators/claude-plugin.test.js +0 -111
  105. package/bin/lib/mcp-config.test.js +0 -310
  106. package/bin/lib/microsoft-mcp.test.js +0 -115
  107. package/bin/utils/errors.js +0 -159
  108. package/bin/utils/git.js +0 -298
  109. package/bin/utils/logger.js +0 -142
  110. package/bin/utils/mcp-detect.test.js +0 -81
  111. package/bin/utils/pbix.js +0 -305
  112. package/bin/utils/pbix.test.js +0 -37
  113. package/bin/utils/profiles.js +0 -312
  114. package/bin/utils/projects.js +0 -169
  115. package/bin/utils/readline.js +0 -206
  116. package/bin/utils/readline.test.js +0 -47
  117. package/bin/utils/tui.test.js +0 -127
  118. package/docs/openrouter-free-models.md +0 -92
  119. package/library/examples/README.md +0 -151
  120. package/library/examples/finance-reporting/README.md +0 -351
  121. package/library/examples/finance-reporting/data-model.md +0 -267
  122. package/library/examples/finance-reporting/measures.dax +0 -557
  123. package/library/examples/hr-analytics/README.md +0 -371
  124. package/library/examples/hr-analytics/data-model.md +0 -315
  125. package/library/examples/hr-analytics/measures.dax +0 -460
  126. package/library/examples/marketing-analytics/README.md +0 -37
  127. package/library/examples/marketing-analytics/data-model.md +0 -62
  128. package/library/examples/marketing-analytics/measures.dax +0 -110
  129. package/library/examples/retail-analytics/README.md +0 -439
  130. package/library/examples/retail-analytics/data-model.md +0 -288
  131. package/library/examples/retail-analytics/measures.dax +0 -481
  132. package/library/examples/supply-chain/README.md +0 -37
  133. package/library/examples/supply-chain/data-model.md +0 -69
  134. package/library/examples/supply-chain/measures.dax +0 -77
  135. package/library/examples/udf-library/README.md +0 -228
  136. package/library/examples/udf-library/functions.dax +0 -571
  137. package/library/snippets/dax/README.md +0 -292
  138. package/library/snippets/dax/business-domains.md +0 -576
  139. package/library/snippets/dax/calculate-patterns.md +0 -276
  140. package/library/snippets/dax/calculation-groups.md +0 -489
  141. package/library/snippets/dax/error-handling.md +0 -495
  142. package/library/snippets/dax/iterators-and-aggregations.md +0 -474
  143. package/library/snippets/dax/kpis-and-metrics.md +0 -293
  144. package/library/snippets/dax/rankings-and-topn.md +0 -235
  145. package/library/snippets/dax/security-patterns.md +0 -413
  146. package/library/snippets/dax/text-and-formatting.md +0 -316
  147. package/library/snippets/dax/time-intelligence.md +0 -196
  148. package/library/snippets/dax/user-defined-functions.md +0 -477
  149. package/library/snippets/dax/virtual-tables.md +0 -546
  150. package/library/snippets/excel-formulas/README.md +0 -84
  151. package/library/snippets/excel-formulas/aggregations.md +0 -330
  152. package/library/snippets/excel-formulas/dates-and-times.md +0 -361
  153. package/library/snippets/excel-formulas/dynamic-arrays.md +0 -314
  154. package/library/snippets/excel-formulas/lookups.md +0 -169
  155. package/library/snippets/excel-formulas/text-functions.md +0 -363
  156. package/library/snippets/governance/naming-conventions.md +0 -97
  157. package/library/snippets/governance/review-checklists.md +0 -107
  158. package/library/snippets/power-query/README.md +0 -389
  159. package/library/snippets/power-query/api-integration.md +0 -707
  160. package/library/snippets/power-query/connections.md +0 -434
  161. package/library/snippets/power-query/data-cleaning.md +0 -298
  162. package/library/snippets/power-query/error-handling.md +0 -526
  163. package/library/snippets/power-query/parameters.md +0 -350
  164. package/library/snippets/power-query/performance.md +0 -506
  165. package/library/snippets/power-query/transformations.md +0 -330
  166. package/library/snippets/report-design/accessibility.md +0 -78
  167. package/library/snippets/report-design/chart-selection.md +0 -54
  168. package/library/snippets/report-design/layout-patterns.md +0 -87
  169. package/library/templates/data-models/README.md +0 -93
  170. package/library/templates/data-models/finance-model.md +0 -627
  171. package/library/templates/data-models/retail-star-schema.md +0 -473
  172. package/library/templates/excel/README.md +0 -83
  173. package/library/templates/excel/budget-tracker.md +0 -432
  174. package/library/templates/excel/data-entry-form.md +0 -533
  175. package/library/templates/power-bi/README.md +0 -72
  176. package/library/templates/power-bi/finance-report.md +0 -449
  177. package/library/templates/power-bi/kpi-scorecard.md +0 -461
  178. package/library/templates/power-bi/sales-dashboard.md +0 -281
  179. package/library/themes/excel/README.md +0 -436
  180. package/library/themes/power-bi/README.md +0 -271
  181. package/library/themes/power-bi/accessible.json +0 -307
  182. package/library/themes/power-bi/bi-superpowers-default.json +0 -858
  183. package/library/themes/power-bi/corporate-blue.json +0 -291
  184. package/library/themes/power-bi/dark-mode.json +0 -291
  185. package/library/themes/power-bi/minimal.json +0 -292
  186. package/library/themes/power-bi/print-friendly.json +0 -309
@@ -0,0 +1,41 @@
1
+ #!/bin/bash
2
+ # ensure-pbi-cli.sh — idempotent installer for pbi-cli-tool + pywin32
3
+ # Run from any directory. Requires Python 3.10+ and pip on PATH.
4
+ # Exit code 0 = ready, 1 = install failed
5
+
6
+ set -e
7
+
8
+ check_python() {
9
+ python --version 2>/dev/null | grep -qE "Python 3\.(1[0-9]|[2-9][0-9])" && return 0
10
+ python3 --version 2>/dev/null | grep -qE "Python 3\.(1[0-9]|[2-9][0-9])" && return 0
11
+ echo "❌ Python 3.10+ required. Install from Microsoft Store or python.org."
12
+ exit 1
13
+ }
14
+
15
+ check_pipx() {
16
+ pipx --version 2>/dev/null && return 0
17
+ python -m pipx --version 2>/dev/null && return 0
18
+ echo "📦 Installing pipx..."
19
+ pip install --user pipx 2>/dev/null || python -m pip install --user pipx
20
+ python -m pipx ensurepath
21
+ echo "⚠ Close and reopen your terminal, then re-run this script."
22
+ exit 1
23
+ }
24
+
25
+ check_pbi() {
26
+ pbi --version 2>/dev/null && return 0
27
+ echo "📦 Installing pbi-cli-tool..."
28
+ python -m pipx install pbi-cli-tool
29
+ }
30
+
31
+ inject_pywin32() {
32
+ python -m pipx inject pbi-cli-tool pywin32 2>/dev/null || true
33
+ }
34
+
35
+ echo "Checking pbi-cli-tool environment..."
36
+ check_python
37
+ check_pipx
38
+ check_pbi
39
+ inject_pywin32
40
+ echo "✅ pbi-cli-tool is ready."
41
+ pbi --version
@@ -0,0 +1,322 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * validate-pbir.js — allowlist-driven PBIR validator.
4
+ *
5
+ * Complements `pbi report validate` (which does schema sanity) by enforcing
6
+ * the native-visuals allowlist maintained in create-visual.js. Catches:
7
+ *
8
+ * - visualType that's not a native PBIR type (e.g. `stackedBarChart`,
9
+ * typos) — Desktop renders these as "objeto visual personalizado" but
10
+ * the CLI validate passes them. This is the exact bug F1 that motivated
11
+ * the migration away from the CLI.
12
+ *
13
+ * - bindings on roles that the type doesn't declare (e.g. `--bind size`
14
+ * on a funnelChart) — CLI validate doesn't catch these either.
15
+ *
16
+ * - missing required roles (e.g. a barChart with no Category).
17
+ *
18
+ * - visualContainer schema drift (wrong $schema URL or missing `name`).
19
+ *
20
+ * Usage:
21
+ * node validate-pbir.js <reportPath>
22
+ * node validate-pbir.js --json <reportPath> # machine-readable output
23
+ *
24
+ * Exit codes:
25
+ * 0 valid
26
+ * 1 validation errors found
27
+ * 2 I/O error (bad path, corrupt JSON)
28
+ *
29
+ * NOT a replacement for `pbi report validate` — run both. This validator
30
+ * trusts that the .Report folder is structurally correct (has definition/,
31
+ * pages/, etc.) and focuses on visual-level correctness that the CLI misses.
32
+ */
33
+
34
+ 'use strict';
35
+
36
+ const fs = require('fs');
37
+ const path = require('path');
38
+
39
+ const { NATIVE_VISUAL_TYPES, KNOWN_NON_NATIVE_TYPES } = require('./create-visual.js');
40
+
41
+ const EXPECTED_VISUAL_SCHEMA =
42
+ 'https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.7.0/schema.json';
43
+
44
+ function fail(msg, code = 2) {
45
+ process.stderr.write(`validate-pbir: ${msg}\n`);
46
+ process.exit(code);
47
+ }
48
+
49
+ function parseArgs(argv) {
50
+ const out = {};
51
+ const positional = [];
52
+ for (let i = 0; i < argv.length; i += 1) {
53
+ const a = argv[i];
54
+ if (a === '--json') out.json = true;
55
+ else if (a === '-h' || a === '--help') out.help = true;
56
+ else if (a.startsWith('-')) fail(`unknown flag: ${a}`, 2);
57
+ else positional.push(a);
58
+ }
59
+ out.reportPath = positional[0];
60
+ return out;
61
+ }
62
+
63
+ function printHelp() {
64
+ process.stdout.write(
65
+ [
66
+ 'Usage: validate-pbir.js [--json] <reportPath>',
67
+ '',
68
+ 'Arguments:',
69
+ ' reportPath Path to a .Report folder',
70
+ '',
71
+ 'Options:',
72
+ ' --json Emit JSON instead of human-readable output',
73
+ ' -h, --help Show this help',
74
+ '',
75
+ 'Exit codes:',
76
+ ' 0 valid',
77
+ ' 1 validation errors found',
78
+ ' 2 I/O error',
79
+ '',
80
+ ].join('\n')
81
+ );
82
+ }
83
+
84
+ function listPages(reportPath) {
85
+ const pagesDir = path.join(reportPath, 'definition', 'pages');
86
+ if (!fs.existsSync(pagesDir)) {
87
+ fail(`pages directory not found: ${pagesDir}`, 2);
88
+ }
89
+ const out = [];
90
+ for (const entry of fs.readdirSync(pagesDir, { withFileTypes: true })) {
91
+ if (!entry.isDirectory()) continue;
92
+ const pageJsonPath = path.join(pagesDir, entry.name, 'page.json');
93
+ if (!fs.existsSync(pageJsonPath)) continue;
94
+ out.push({ name: entry.name, dir: path.join(pagesDir, entry.name) });
95
+ }
96
+ return out;
97
+ }
98
+
99
+ function listVisuals(pageDir) {
100
+ const visualsDir = path.join(pageDir, 'visuals');
101
+ if (!fs.existsSync(visualsDir)) return [];
102
+ const out = [];
103
+ for (const entry of fs.readdirSync(visualsDir, { withFileTypes: true })) {
104
+ if (!entry.isDirectory()) continue;
105
+ const visualJsonPath = path.join(visualsDir, entry.name, 'visual.json');
106
+ if (!fs.existsSync(visualJsonPath)) continue;
107
+ out.push({ name: entry.name, path: visualJsonPath });
108
+ }
109
+ return out;
110
+ }
111
+
112
+ function readJsonSafe(filePath) {
113
+ try {
114
+ const raw = fs.readFileSync(filePath, 'utf8');
115
+ return { data: JSON.parse(raw), err: null };
116
+ } catch (err) {
117
+ return { data: null, err };
118
+ }
119
+ }
120
+
121
+ // Extracts the set of roles that have at least one projection bound.
122
+ function boundRoles(visualData) {
123
+ const qs = visualData && visualData.visual && visualData.visual.query && visualData.visual.query.queryState;
124
+ if (!qs || typeof qs !== 'object') return new Set();
125
+ const bound = new Set();
126
+ for (const [role, spec] of Object.entries(qs)) {
127
+ const projections = spec && spec.projections;
128
+ if (Array.isArray(projections) && projections.length > 0) bound.add(role);
129
+ }
130
+ return bound;
131
+ }
132
+
133
+ function validateVisual(visual) {
134
+ const errors = [];
135
+ const { data, path: filePath } = visual;
136
+
137
+ // Schema URL check.
138
+ if (data.$schema !== EXPECTED_VISUAL_SCHEMA) {
139
+ errors.push({
140
+ severity: 'warn',
141
+ rule: 'schema-url',
142
+ message: `unexpected $schema (got ${data.$schema || 'none'})`,
143
+ });
144
+ }
145
+
146
+ // `name` must exist and match the folder.
147
+ if (typeof data.name !== 'string' || !data.name) {
148
+ errors.push({ severity: 'error', rule: 'name', message: 'missing "name"' });
149
+ }
150
+
151
+ const vt = data.visual && data.visual.visualType;
152
+ if (!vt) {
153
+ errors.push({ severity: 'error', rule: 'visual-type', message: 'missing visualType' });
154
+ return errors;
155
+ }
156
+
157
+ // Allowlist: known non-native (like stackedBarChart).
158
+ if (KNOWN_NON_NATIVE_TYPES[vt]) {
159
+ errors.push({
160
+ severity: 'error',
161
+ rule: 'non-native-type',
162
+ visualType: vt,
163
+ message: KNOWN_NON_NATIVE_TYPES[vt],
164
+ });
165
+ return errors;
166
+ }
167
+
168
+ const def = NATIVE_VISUAL_TYPES[vt];
169
+ if (!def) {
170
+ errors.push({
171
+ severity: 'error',
172
+ rule: 'unknown-type',
173
+ visualType: vt,
174
+ message: `visualType "${vt}" is not in the native allowlist. Run create-visual.js --list-types for the canonical list.`,
175
+ });
176
+ return errors;
177
+ }
178
+
179
+ // Check required roles are bound (skip for textbox — no roles).
180
+ if (vt !== 'textbox') {
181
+ const bound = boundRoles(data);
182
+ for (const [role, spec] of Object.entries(def.roles)) {
183
+ if (spec.required && !bound.has(role)) {
184
+ errors.push({
185
+ severity: 'error',
186
+ rule: 'missing-required-role',
187
+ visualType: vt,
188
+ role,
189
+ message: `required role "${role}" has no projections (visualType: ${vt})`,
190
+ });
191
+ }
192
+ }
193
+
194
+ // Check that every bound role is valid for this type.
195
+ for (const role of bound) {
196
+ if (!(role in def.roles)) {
197
+ errors.push({
198
+ severity: 'error',
199
+ rule: 'invalid-role',
200
+ visualType: vt,
201
+ role,
202
+ message: `role "${role}" is not valid for visualType ${vt}. Valid: ${Object.keys(def.roles).join(', ') || '(none)'}`,
203
+ });
204
+ }
205
+ }
206
+ }
207
+
208
+ return errors;
209
+ }
210
+
211
+ function main() {
212
+ const args = parseArgs(process.argv.slice(2));
213
+ if (args.help) {
214
+ printHelp();
215
+ return;
216
+ }
217
+ if (!args.reportPath) fail('reportPath is required. Run --help for usage.', 2);
218
+
219
+ const reportPath = path.resolve(args.reportPath);
220
+ if (!fs.existsSync(reportPath)) fail(`not found: ${reportPath}`, 2);
221
+
222
+ const pages = listPages(reportPath);
223
+ const results = {
224
+ reportPath,
225
+ pagesChecked: pages.length,
226
+ visualsChecked: 0,
227
+ errors: [],
228
+ };
229
+
230
+ for (const page of pages) {
231
+ const visuals = listVisuals(page.dir);
232
+ // Track visual.name per page so we can flag duplicates. PBI Desktop
233
+ // does not reject a duplicate-name pair on load, but downstream tooling
234
+ // that references visuals by name (filters, bookmarks, selections) gets
235
+ // ambiguous — validate-pbir treats it as a hard error.
236
+ const namesSeenOnPage = new Map();
237
+ for (const v of visuals) {
238
+ results.visualsChecked += 1;
239
+ const { data, err } = readJsonSafe(v.path);
240
+ if (err) {
241
+ results.errors.push({
242
+ page: page.name,
243
+ visual: v.name,
244
+ severity: 'error',
245
+ rule: 'bad-json',
246
+ message: `could not parse visual.json: ${err.message}`,
247
+ });
248
+ continue;
249
+ }
250
+
251
+ // Duplicate-name check (page-scoped).
252
+ const declaredName = data && typeof data.name === 'string' ? data.name : null;
253
+ if (declaredName) {
254
+ if (namesSeenOnPage.has(declaredName)) {
255
+ results.errors.push({
256
+ page: page.name,
257
+ visual: v.name,
258
+ severity: 'error',
259
+ rule: 'duplicate-name',
260
+ message: `visual name "${declaredName}" is also used by "${namesSeenOnPage.get(declaredName)}" on this page`,
261
+ });
262
+ } else {
263
+ namesSeenOnPage.set(declaredName, v.name);
264
+ }
265
+ }
266
+
267
+ const visualErrors = validateVisual({ data, path: v.path });
268
+ for (const e of visualErrors) {
269
+ results.errors.push({
270
+ page: page.name,
271
+ visual: v.name,
272
+ ...e,
273
+ });
274
+ }
275
+ }
276
+ }
277
+
278
+ const errorCount = results.errors.filter((e) => e.severity === 'error').length;
279
+ const warnCount = results.errors.filter((e) => e.severity === 'warn').length;
280
+ const valid = errorCount === 0;
281
+
282
+ if (args.json) {
283
+ process.stdout.write(JSON.stringify({ ...results, valid, errorCount, warnCount }, null, 2) + '\n');
284
+ } else {
285
+ const W = 77; // inner width between the | borders
286
+ const padRow = (label, value) => {
287
+ const text = `${label}: ${value}`;
288
+ return `| ${text}${' '.repeat(Math.max(0, W - text.length - 1))}|`;
289
+ };
290
+ const border = `+${'-'.repeat(W)}+`;
291
+ process.stdout.write(border + '\n');
292
+ process.stdout.write(padRow('valid', valid ? 'True' : 'False') + '\n');
293
+ process.stdout.write(padRow('pages_checked', String(results.pagesChecked)) + '\n');
294
+ process.stdout.write(padRow('visuals_checked', String(results.visualsChecked)) + '\n');
295
+ process.stdout.write(padRow('errors', String(errorCount)) + '\n');
296
+ process.stdout.write(padRow('warnings', String(warnCount)) + '\n');
297
+ process.stdout.write(border + '\n');
298
+ if (results.errors.length) {
299
+ process.stdout.write('\nIssues:\n');
300
+ for (const e of results.errors) {
301
+ const icon = e.severity === 'error' ? '✗' : '⚠';
302
+ const loc = `${e.page}/${e.visual}`;
303
+ process.stdout.write(` ${icon} [${e.rule}] ${loc}: ${e.message}\n`);
304
+ }
305
+ process.stdout.write('\n');
306
+ }
307
+ }
308
+
309
+ process.exit(valid ? 0 : 1);
310
+ }
311
+
312
+ module.exports = {
313
+ validateVisual,
314
+ boundRoles,
315
+ listPages,
316
+ listVisuals,
317
+ parseArgs,
318
+ };
319
+
320
+ if (require.main === module) {
321
+ main();
322
+ }