@fragments-sdk/cli 0.5.2 → 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.
Files changed (124) hide show
  1. package/dist/bin.js +996 -79
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-ICAIQ57V.js → chunk-6JBGU74P.js} +5 -3
  4. package/dist/chunk-6JBGU74P.js.map +1 -0
  5. package/dist/chunk-7OPWMLOE.js +1625 -0
  6. package/dist/chunk-7OPWMLOE.js.map +1 -0
  7. package/dist/{chunk-2H2JAA3U.js → chunk-CVXKXVOY.js} +3 -3
  8. package/dist/{chunk-2H2JAA3U.js.map → chunk-CVXKXVOY.js.map} +1 -1
  9. package/dist/{chunk-IOJE35DZ.js → chunk-NWQ4CJOQ.js} +3 -3
  10. package/dist/{chunk-2DJH4F4P.js → chunk-RVRTRESS.js} +3 -3
  11. package/dist/{chunk-V7YLRR4C.js → chunk-TJ34N7C7.js} +41 -4
  12. package/dist/{chunk-V7YLRR4C.js.map → chunk-TJ34N7C7.js.map} +1 -1
  13. package/dist/{chunk-XNWDI6UT.js → chunk-XHUDJNN3.js} +5 -5
  14. package/dist/{core-DKHB7FYV.js → core-W2HYIQW6.js} +4 -4
  15. package/dist/{generate-KL24VZVD.js → generate-LMTISDIJ.js} +5 -5
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +15 -7
  18. package/dist/index.js.map +1 -1
  19. package/dist/{init-NION5S3M.js → init-7CHRKQ7P.js} +5 -5
  20. package/dist/mcp-bin.js +8 -220
  21. package/dist/mcp-bin.js.map +1 -1
  22. package/dist/scan-WY23TJCP.js +12 -0
  23. package/dist/{service-RWUMZ3EW.js → service-T2L7VLTE.js} +5 -5
  24. package/dist/static-viewer-GBR7YNF3.js +12 -0
  25. package/dist/{test-ECPEXFDN.js → test-OJRXNDO2.js} +4 -4
  26. package/dist/{tokens-ITADYVPF.js → tokens-3BWDESVM.js} +6 -6
  27. package/dist/viewer-SUFOISZM.js +1822 -0
  28. package/dist/viewer-SUFOISZM.js.map +1 -0
  29. package/package.json +6 -5
  30. package/src/bin.ts +31 -0
  31. package/src/build.ts +147 -13
  32. package/src/cli-commands.ts +18 -0
  33. package/src/commands/__tests__/a11y-scoring.test.ts +278 -0
  34. package/src/commands/a11y-report.ts +625 -0
  35. package/src/commands/a11y.ts +168 -14
  36. package/src/commands/build.ts +16 -0
  37. package/src/commands/graph.ts +274 -0
  38. package/src/core/auto-props.ts +464 -0
  39. package/src/core/composition.ts +64 -1
  40. package/src/core/graph-extractor.test.ts +542 -0
  41. package/src/core/graph-extractor.ts +601 -0
  42. package/src/core/importAnalyzer.ts +5 -0
  43. package/src/core/schema.ts +2 -0
  44. package/src/core/types.ts +3 -1
  45. package/src/index.ts +4 -0
  46. package/src/mcp/server.ts +13 -220
  47. package/src/theme/__tests__/component-contrast.test.ts +338 -0
  48. package/src/theme/__tests__/contrast-validation.test.ts +326 -0
  49. package/src/theme/contrast.test.ts +331 -0
  50. package/src/theme/contrast.ts +246 -0
  51. package/src/theme/generator.ts +213 -1
  52. package/src/theme/index.ts +16 -0
  53. package/src/theme/types.ts +51 -0
  54. package/src/viewer/__tests__/a11y-fixes.test.ts +358 -0
  55. package/src/viewer/__tests__/viewer-integration.test.ts +2 -7
  56. package/src/viewer/components/AccessibilityPanel.tsx +493 -433
  57. package/src/viewer/components/ActionCapture.tsx +1 -1
  58. package/src/viewer/components/ActionsPanel.tsx +142 -183
  59. package/src/viewer/components/App.tsx +276 -183
  60. package/src/viewer/components/BottomPanel.tsx +40 -80
  61. package/src/viewer/components/CodePanel.tsx +9 -87
  62. package/src/viewer/components/CommandPalette.tsx +117 -74
  63. package/src/viewer/components/ComponentGraph.tsx +143 -126
  64. package/src/viewer/components/ComponentHeader.tsx +46 -43
  65. package/src/viewer/components/ContractPanel.tsx +124 -117
  66. package/src/viewer/components/ErrorBoundary.tsx +47 -35
  67. package/src/viewer/components/FigmaEmbed.tsx +18 -13
  68. package/src/viewer/components/FragmentEditor.tsx +126 -63
  69. package/src/viewer/components/HealthDashboard.tsx +146 -171
  70. package/src/viewer/components/HmrStatusIndicator.tsx +31 -41
  71. package/src/viewer/components/Icons.tsx +151 -98
  72. package/src/viewer/components/InteractionsPanel.tsx +317 -264
  73. package/src/viewer/components/IsolatedPreviewFrame.tsx +52 -27
  74. package/src/viewer/components/IsolatedRender.tsx +12 -6
  75. package/src/viewer/components/KeyboardShortcutsHelp.tsx +34 -70
  76. package/src/viewer/components/LandingPage.tsx +285 -305
  77. package/src/viewer/components/Layout.tsx +12 -10
  78. package/src/viewer/components/LeftSidebar.tsx +103 -155
  79. package/src/viewer/components/MultiViewportPreview.tsx +254 -63
  80. package/src/viewer/components/PreviewArea.tsx +113 -44
  81. package/src/viewer/components/PreviewFrameHost.tsx +36 -6
  82. package/src/viewer/components/PreviewPane.tsx +2 -3
  83. package/src/viewer/components/PreviewToolbar.tsx +109 -105
  84. package/src/viewer/components/PropsEditor.tsx +154 -74
  85. package/src/viewer/components/PropsTable.tsx +95 -82
  86. package/src/viewer/components/RelationsSection.tsx +71 -40
  87. package/src/viewer/components/ResizablePanel.tsx +158 -55
  88. package/src/viewer/components/RightSidebar.tsx +46 -56
  89. package/src/viewer/components/ScreenshotButton.tsx +12 -12
  90. package/src/viewer/components/SkeletonLoader.tsx +99 -83
  91. package/src/viewer/components/StoryRenderer.tsx +4 -11
  92. package/src/viewer/components/Toast.tsx +3 -67
  93. package/src/viewer/components/TokenStylePanel.tsx +136 -118
  94. package/src/viewer/components/UsageSection.tsx +26 -26
  95. package/src/viewer/components/VariantMatrix.tsx +140 -47
  96. package/src/viewer/components/VariantTabs.tsx +24 -68
  97. package/src/viewer/components/ViewportSelector.tsx +121 -114
  98. package/src/viewer/constants/ui.ts +23 -22
  99. package/src/viewer/entry.tsx +8 -3
  100. package/src/viewer/index.ts +3 -6
  101. package/src/viewer/preview-frame.html +43 -18
  102. package/src/viewer/server.ts +7 -16
  103. package/src/viewer/styles/globals.css +46 -85
  104. package/src/viewer/utils/a11y-fixes.ts +53 -30
  105. package/dist/chunk-ICAIQ57V.js.map +0 -1
  106. package/dist/chunk-U4GQ2JTD.js +0 -832
  107. package/dist/chunk-U4GQ2JTD.js.map +0 -1
  108. package/dist/scan-ESEXV7LF.js +0 -12
  109. package/dist/static-viewer-O37MJ5B6.js +0 -12
  110. package/dist/viewer-YDGFDTK5.js +0 -11104
  111. package/dist/viewer-YDGFDTK5.js.map +0 -1
  112. package/src/viewer/postcss.config.js +0 -6
  113. package/src/viewer/tailwind.config.js +0 -37
  114. /package/dist/{chunk-IOJE35DZ.js.map → chunk-NWQ4CJOQ.js.map} +0 -0
  115. /package/dist/{chunk-2DJH4F4P.js.map → chunk-RVRTRESS.js.map} +0 -0
  116. /package/dist/{chunk-XNWDI6UT.js.map → chunk-XHUDJNN3.js.map} +0 -0
  117. /package/dist/{core-DKHB7FYV.js.map → core-W2HYIQW6.js.map} +0 -0
  118. /package/dist/{generate-KL24VZVD.js.map → generate-LMTISDIJ.js.map} +0 -0
  119. /package/dist/{init-NION5S3M.js.map → init-7CHRKQ7P.js.map} +0 -0
  120. /package/dist/{scan-ESEXV7LF.js.map → scan-WY23TJCP.js.map} +0 -0
  121. /package/dist/{service-RWUMZ3EW.js.map → service-T2L7VLTE.js.map} +0 -0
  122. /package/dist/{static-viewer-O37MJ5B6.js.map → static-viewer-GBR7YNF3.js.map} +0 -0
  123. /package/dist/{test-ECPEXFDN.js.map → test-OJRXNDO2.js.map} +0 -0
  124. /package/dist/{tokens-ITADYVPF.js.map → tokens-3BWDESVM.js.map} +0 -0
@@ -5,6 +5,7 @@
5
5
  * for all components in the design system.
6
6
  */
7
7
 
8
+ import fs from 'node:fs';
8
9
  import pc from 'picocolors';
9
10
  import { BRAND } from '../core/index.js';
10
11
  import { loadConfig } from '../core/node.js';
@@ -12,6 +13,7 @@ import {
12
13
  createDevServerClient,
13
14
  DevServerConnectionError,
14
15
  } from '../shared/index.js';
16
+ import { generateA11yReport } from './a11y-report.js';
15
17
 
16
18
  /**
17
19
  * Options for a11y command
@@ -27,6 +29,14 @@ export interface A11yOptions {
27
29
  component?: string;
28
30
  /** Dev server port */
29
31
  port?: number | string;
32
+ /** Output format: table (default), json, or github (markdown) */
33
+ format?: 'table' | 'json' | 'github';
34
+ /** WCAG standard level to check against */
35
+ standard?: 'AA' | 'AAA';
36
+ /** Generate standalone HTML compliance report */
37
+ report?: boolean;
38
+ /** Output path for the HTML report */
39
+ output?: string;
30
40
  }
31
41
 
32
42
  /**
@@ -69,6 +79,18 @@ export interface A11yComponentResult {
69
79
  totalSerious: number;
70
80
  }
71
81
 
82
+ /**
83
+ * A11y score breakdown
84
+ */
85
+ export interface A11yScore {
86
+ /** Numeric score 0–100 */
87
+ score: number;
88
+ /** Percentage of components meeting AA */
89
+ aaPercent: number;
90
+ /** Percentage of components meeting AAA */
91
+ aaaPercent: number;
92
+ }
93
+
72
94
  /**
73
95
  * Summary of a11y results
74
96
  */
@@ -93,13 +115,98 @@ export interface A11ySummary {
93
115
  totalMinor: number;
94
116
  /** Whether CI check passed (no critical/serious) */
95
117
  passed: boolean;
118
+ /** Computed a11y score */
119
+ score?: A11yScore;
120
+ }
121
+
122
+ /**
123
+ * Calculate an accessibility score from the summary.
124
+ *
125
+ * Starts at 100 and subtracts per violation:
126
+ * critical: -10, serious: -5, moderate: -2, minor: -1
127
+ */
128
+ export function calculateA11yScore(summary: A11ySummary): A11yScore {
129
+ const deductions =
130
+ summary.totalCritical * 10 +
131
+ summary.totalSerious * 5 +
132
+ summary.totalModerate * 2 +
133
+ summary.totalMinor * 1;
134
+
135
+ const score = Math.max(0, 100 - deductions);
136
+
137
+ // AA = no critical/serious violations
138
+ const aaComponents = summary.components.filter(
139
+ c => c.totalCritical === 0 && c.totalSerious === 0
140
+ ).length;
141
+ const aaPercent = summary.totalComponents > 0
142
+ ? Math.round((aaComponents / summary.totalComponents) * 100)
143
+ : 100;
144
+
145
+ // AAA = no violations at all
146
+ const aaaComponents = summary.components.filter(
147
+ c => c.totalViolations === 0
148
+ ).length;
149
+ const aaaPercent = summary.totalComponents > 0
150
+ ? Math.round((aaaComponents / summary.totalComponents) * 100)
151
+ : 100;
152
+
153
+ return { score, aaPercent, aaaPercent };
154
+ }
155
+
156
+ /**
157
+ * Format summary as GitHub-flavored Markdown.
158
+ */
159
+ export function formatGitHub(summary: A11ySummary): string {
160
+ const score = summary.score ?? calculateA11yScore(summary);
161
+ const badge = summary.passed ? 'passing' : 'failing';
162
+ const badgeColor = summary.passed ? 'brightgreen' : 'red';
163
+ const lines: string[] = [];
164
+
165
+ lines.push(`## ${BRAND.name} Accessibility Report`);
166
+ lines.push('');
167
+ lines.push(`![a11y](https://img.shields.io/badge/a11y-${badge}-${badgeColor})`);
168
+ lines.push(`**Score:** ${score.score}/100 | **AA:** ${score.aaPercent}% | **AAA:** ${score.aaaPercent}%`);
169
+ lines.push('');
170
+
171
+ // Component table
172
+ lines.push('| Component | Variants | Violations | Critical | Serious | Status |');
173
+ lines.push('|-----------|----------|------------|----------|---------|--------|');
174
+
175
+ for (const result of summary.components) {
176
+ const statusIcon = result.status === 'PASS' ? 'PASS' : result.status === 'WARN' ? 'WARN' : 'FAIL';
177
+ const variantCount = result.results.length || 1;
178
+ lines.push(
179
+ `| ${result.component} | ${variantCount} | ${result.totalViolations} | ${result.totalCritical} | ${result.totalSerious} | ${statusIcon} |`
180
+ );
181
+ }
182
+
183
+ lines.push('');
184
+ lines.push(`**Summary:** ${summary.accessibleComponents}/${summary.totalComponents} components accessible (${summary.accessiblePercent}%)`);
185
+ lines.push(`**Violations:** ${summary.totalViolations} total (${summary.totalCritical} critical, ${summary.totalSerious} serious, ${summary.totalModerate} moderate, ${summary.totalMinor} minor)`);
186
+
187
+ if (summary.totalCritical + summary.totalSerious > 0) {
188
+ lines.push('');
189
+ lines.push(`**Blocking:** ${summary.totalCritical + summary.totalSerious} critical/serious issues must be fixed`);
190
+ }
191
+
192
+ lines.push('');
193
+
194
+ return lines.join('\n');
96
195
  }
97
196
 
98
197
  /**
99
198
  * Run the a11y command
100
199
  */
101
200
  export async function a11y(options: A11yOptions = {}): Promise<A11ySummary> {
102
- const { config: configPath, json = false, ci = false, component, port = 6006 } = options;
201
+ const {
202
+ config: configPath,
203
+ json = false,
204
+ ci = false,
205
+ component,
206
+ port = 6006,
207
+ format = json ? 'json' : 'table',
208
+ standard = 'AA',
209
+ } = options;
103
210
 
104
211
  // Load config
105
212
  await loadConfig(configPath);
@@ -107,7 +214,9 @@ export async function a11y(options: A11yOptions = {}): Promise<A11ySummary> {
107
214
  const client = createDevServerClient(port);
108
215
  const componentResults: A11yComponentResult[] = [];
109
216
 
110
- if (!json) {
217
+ const isJsonOutput = format === 'json' || json;
218
+
219
+ if (!isJsonOutput && format !== 'github') {
111
220
  console.log(pc.cyan(`\n${BRAND.name} Accessibility Report\n`));
112
221
  }
113
222
 
@@ -124,7 +233,7 @@ export async function a11y(options: A11yOptions = {}): Promise<A11ySummary> {
124
233
  const segments = await client.getSegments();
125
234
 
126
235
  if (segments.length === 0) {
127
- if (json) {
236
+ if (isJsonOutput) {
128
237
  console.log(JSON.stringify({ error: 'No fragments found', components: [] }));
129
238
  } else {
130
239
  console.log(pc.yellow('No fragments found.\n'));
@@ -150,7 +259,7 @@ export async function a11y(options: A11yOptions = {}): Promise<A11ySummary> {
150
259
 
151
260
  if (component && componentsToCheck.length === 0) {
152
261
  const error = `Component '${component}' not found. Available: ${segments.map(s => s.name).join(', ')}`;
153
- if (json) {
262
+ if (isJsonOutput) {
154
263
  console.log(JSON.stringify({ error }));
155
264
  } else {
156
265
  console.log(pc.red(error));
@@ -158,7 +267,7 @@ export async function a11y(options: A11yOptions = {}): Promise<A11ySummary> {
158
267
  throw new Error(error);
159
268
  }
160
269
 
161
- if (!json) {
270
+ if (!isJsonOutput && format !== 'github') {
162
271
  console.log(pc.dim(`Checking ${componentsToCheck.length} component(s) for accessibility issues...\n`));
163
272
  }
164
273
 
@@ -186,6 +295,11 @@ export async function a11y(options: A11yOptions = {}): Promise<A11ySummary> {
186
295
  status = 'WARN';
187
296
  }
188
297
 
298
+ // In AAA mode, any violation is a failure
299
+ if (standard === 'AAA' && totalViolations > 0) {
300
+ status = 'FAIL';
301
+ }
302
+
189
303
  componentResults.push({
190
304
  component: seg.name,
191
305
  results: a11yResult.results,
@@ -194,7 +308,7 @@ export async function a11y(options: A11yOptions = {}): Promise<A11ySummary> {
194
308
  totalCritical,
195
309
  totalSerious,
196
310
  });
197
- } catch (error) {
311
+ } catch {
198
312
  // Handle individual component errors
199
313
  componentResults.push({
200
314
  component: seg.name,
@@ -223,6 +337,10 @@ export async function a11y(options: A11yOptions = {}): Promise<A11ySummary> {
223
337
  }
224
338
  }
225
339
 
340
+ const passed = standard === 'AAA'
341
+ ? totalViolations === 0
342
+ : totalCritical === 0 && totalSerious === 0;
343
+
226
344
  const summary: A11ySummary = {
227
345
  totalComponents: componentResults.length,
228
346
  accessibleComponents,
@@ -235,14 +353,18 @@ export async function a11y(options: A11yOptions = {}): Promise<A11ySummary> {
235
353
  totalSerious,
236
354
  totalModerate,
237
355
  totalMinor,
238
- passed: totalCritical === 0 && totalSerious === 0,
356
+ passed,
239
357
  };
240
358
 
241
- if (json) {
242
- // JSON output for CI/automation
359
+ summary.score = calculateA11yScore(summary);
360
+
361
+ // --- Output ---
362
+ if (format === 'github') {
363
+ console.log(formatGitHub(summary));
364
+ } else if (isJsonOutput) {
243
365
  console.log(JSON.stringify(summary, null, 2));
244
366
  } else {
245
- // Table output for humans
367
+ // Rich table output
246
368
  console.log(pc.bold(
247
369
  'Component'.padEnd(20) +
248
370
  'Variants'.padEnd(10) +
@@ -273,6 +395,30 @@ export async function a11y(options: A11yOptions = {}): Promise<A11ySummary> {
273
395
  }
274
396
 
275
397
  console.log(pc.dim('─'.repeat(72)));
398
+ console.log();
399
+
400
+ // Violation breakdown with dotted leaders
401
+ const categories = [
402
+ { label: 'Critical', count: totalCritical, color: pc.red },
403
+ { label: 'Serious', count: totalSerious, color: pc.red },
404
+ { label: 'Moderate', count: totalModerate, color: pc.yellow },
405
+ { label: 'Minor', count: totalMinor, color: pc.dim },
406
+ ];
407
+
408
+ for (const cat of categories) {
409
+ if (cat.count > 0) {
410
+ const dots = '.'.repeat(Math.max(1, 30 - cat.label.length));
411
+ console.log(` ${cat.label} ${pc.dim(dots)} ${cat.color(String(cat.count))}`);
412
+ }
413
+ }
414
+
415
+ // Score box
416
+ const { score, aaPercent, aaaPercent } = summary.score;
417
+ console.log();
418
+ console.log(pc.bold(` Score: ${score}/100`));
419
+ console.log(` AA compliance .... ${aaPercent}%`);
420
+ console.log(` AAA compliance ... ${aaaPercent}%`);
421
+ console.log(` Standard ......... WCAG ${standard}`);
276
422
 
277
423
  console.log();
278
424
  console.log(pc.bold('Summary:'));
@@ -281,19 +427,27 @@ export async function a11y(options: A11yOptions = {}): Promise<A11ySummary> {
281
427
 
282
428
  if (!summary.passed) {
283
429
  console.log();
284
- console.log(pc.red(' Accessibility check failed - critical/serious violations found'));
430
+ console.log(pc.red('x Accessibility check failed - critical/serious violations found'));
285
431
  } else if (totalViolations > 0) {
286
432
  console.log();
287
- console.log(pc.yellow(' Minor/moderate violations found - consider fixing for better accessibility'));
433
+ console.log(pc.yellow('! Minor/moderate violations found - consider fixing for better accessibility'));
288
434
  } else {
289
435
  console.log();
290
- console.log(pc.green(' All components pass accessibility checks'));
436
+ console.log(pc.green('v All components pass accessibility checks'));
291
437
  }
292
438
 
293
439
  console.log();
294
440
  }
295
441
 
296
- // In CI mode, throw if there are critical/serious violations
442
+ // Generate HTML report if requested
443
+ if (options.report) {
444
+ const outputPath = options.output ?? 'a11y-report.html';
445
+ const html = generateA11yReport(summary);
446
+ fs.writeFileSync(outputPath, html, 'utf-8');
447
+ console.log(pc.green('v Report generated: ' + outputPath));
448
+ }
449
+
450
+ // In CI mode, throw if check did not pass
297
451
  if (ci && !summary.passed) {
298
452
  throw new Error(`Accessibility check failed: ${totalCritical} critical, ${totalSerious} serious violations found`);
299
453
  }
@@ -101,6 +101,14 @@ export async function build(options: BuildOptions = {}): Promise<BuildResult> {
101
101
  console.log();
102
102
  }
103
103
 
104
+ if (result.warnings.length > 0) {
105
+ console.log(pc.yellow('Build warnings:\n'));
106
+ for (const warning of result.warnings) {
107
+ console.log(` ${pc.yellow('⚠')} ${warning.file}: ${warning.warning}`);
108
+ }
109
+ console.log();
110
+ }
111
+
104
112
  segmentCount = result.segmentCount;
105
113
  outputPath = result.outputPath;
106
114
 
@@ -123,6 +131,14 @@ export async function build(options: BuildOptions = {}): Promise<BuildResult> {
123
131
  console.log();
124
132
  }
125
133
 
134
+ if (fragmentsResult.warnings.length > 0) {
135
+ console.log(pc.yellow('Registry warnings:\n'));
136
+ for (const warning of fragmentsResult.warnings) {
137
+ console.log(` ${pc.yellow('⚠')} ${warning.file}: ${warning.warning}`);
138
+ }
139
+ console.log();
140
+ }
141
+
126
142
  componentCount = fragmentsResult.componentCount;
127
143
  registryPath = fragmentsResult.registryPath;
128
144
  contextPath = fragmentsResult.contextPath;
@@ -0,0 +1,274 @@
1
+ /**
2
+ * `fragments graph` — query the component relationship graph from the CLI.
3
+ *
4
+ * Loads fragments.json, instantiates the ComponentGraphEngine, and runs
5
+ * the requested query mode. Output formats: table (colored terminal), json, dot.
6
+ */
7
+
8
+ import pc from 'picocolors';
9
+ import { readFile } from 'node:fs/promises';
10
+ import { resolve } from 'node:path';
11
+ import type { CompiledSegmentsFile } from '../core/index.js';
12
+ import { BRAND } from '../core/index.js';
13
+ import { loadConfig } from '../core/node.js';
14
+ import {
15
+ ComponentGraphEngine,
16
+ deserializeGraph,
17
+ } from '@fragments-sdk/context/graph';
18
+ import type { GraphEdgeType } from '@fragments-sdk/context/graph';
19
+
20
+ export interface GraphCommandOptions {
21
+ config?: string;
22
+ mode?: string;
23
+ target?: string;
24
+ edgeTypes?: string;
25
+ depth?: number;
26
+ format?: 'table' | 'json' | 'dot';
27
+ }
28
+
29
+ export async function graph(
30
+ component: string | undefined,
31
+ options: GraphCommandOptions,
32
+ ): Promise<void> {
33
+ const { config, configDir } = await loadConfig(options.config);
34
+ const outputPath = resolve(configDir, config.outFile ?? BRAND.outFile);
35
+
36
+ let data: CompiledSegmentsFile;
37
+ try {
38
+ const content = await readFile(outputPath, 'utf-8');
39
+ data = JSON.parse(content) as CompiledSegmentsFile;
40
+ } catch {
41
+ console.error(
42
+ pc.red(`Error: Could not load ${BRAND.outFile}. Run \`${BRAND.cliCommand} build\` first.`),
43
+ );
44
+ process.exit(1);
45
+ }
46
+
47
+ if (!data.graph) {
48
+ console.error(
49
+ pc.red(`Error: No graph data in ${BRAND.outFile}. Rebuild with the latest CLI.`),
50
+ );
51
+ process.exit(1);
52
+ }
53
+
54
+ const graph = deserializeGraph(data.graph);
55
+ const blocks = data.blocks
56
+ ? Object.fromEntries(
57
+ Object.entries(data.blocks).map(([k, v]) => [k, { components: v.components }]),
58
+ )
59
+ : undefined;
60
+ const engine = new ComponentGraphEngine(graph, blocks);
61
+
62
+ const mode = options.mode ?? (component ? 'dependencies' : 'health');
63
+ const format = options.format ?? 'table';
64
+ const edgeTypes = options.edgeTypes
65
+ ? (options.edgeTypes.split(',') as GraphEdgeType[])
66
+ : undefined;
67
+
68
+ // Validate component exists for modes that require it
69
+ const needsComponent = ['dependencies', 'dependents', 'impact', 'path', 'composition', 'alternatives'];
70
+ if (needsComponent.includes(mode) && !component) {
71
+ console.error(pc.red(`Error: "${mode}" mode requires a component name.`));
72
+ process.exit(1);
73
+ }
74
+ if (component && !engine.hasNode(component)) {
75
+ console.error(pc.red(`Error: Component "${component}" not found in graph.`));
76
+ process.exit(1);
77
+ }
78
+
79
+ switch (mode) {
80
+ case 'health': {
81
+ const health = engine.getHealth();
82
+ if (format === 'json') {
83
+ console.log(JSON.stringify(health, null, 2));
84
+ return;
85
+ }
86
+
87
+ console.log(pc.bold('\nComponent Graph Health\n'));
88
+ console.log(` ${pc.cyan('Nodes:')} ${health.nodeCount}`);
89
+ console.log(` ${pc.cyan('Edges:')} ${health.edgeCount}`);
90
+ console.log(` ${pc.cyan('Avg degree:')} ${health.averageDegree}`);
91
+ console.log(` ${pc.cyan('Islands:')} ${health.connectedComponents.length}`);
92
+ console.log(` ${pc.cyan('Coverage:')} ${health.compositionCoverage}% in blocks`);
93
+ console.log(` ${pc.cyan('Orphans:')} ${health.orphans.length > 0 ? health.orphans.join(', ') : pc.green('none')}`);
94
+
95
+ if (health.hubs.length > 0) {
96
+ console.log(`\n ${pc.bold('Top hubs:')}`);
97
+ for (const hub of health.hubs.slice(0, 5)) {
98
+ console.log(` ${pc.yellow(hub.name)} — ${hub.degree} connections`);
99
+ }
100
+ }
101
+ console.log();
102
+ break;
103
+ }
104
+
105
+ case 'dependencies': {
106
+ const deps = engine.dependencies(component!, edgeTypes);
107
+ if (format === 'json') {
108
+ console.log(JSON.stringify({ component, dependencies: deps }, null, 2));
109
+ return;
110
+ }
111
+
112
+ console.log(pc.bold(`\nDependencies of ${component}\n`));
113
+ if (deps.length === 0) {
114
+ console.log(' No outgoing dependencies.');
115
+ } else {
116
+ for (const dep of deps) {
117
+ console.log(` ${pc.yellow(dep.target)} ${pc.dim(`(${dep.type})`)}${dep.note ? ` — ${dep.note}` : ''}`);
118
+ }
119
+ }
120
+ console.log();
121
+ break;
122
+ }
123
+
124
+ case 'dependents': {
125
+ const deps = engine.dependents(component!, edgeTypes);
126
+ if (format === 'json') {
127
+ console.log(JSON.stringify({ component, dependents: deps }, null, 2));
128
+ return;
129
+ }
130
+
131
+ console.log(pc.bold(`\nDependents of ${component}\n`));
132
+ if (deps.length === 0) {
133
+ console.log(' No incoming dependents.');
134
+ } else {
135
+ for (const dep of deps) {
136
+ console.log(` ${pc.yellow(dep.source)} ${pc.dim(`(${dep.type})`)}${dep.note ? ` — ${dep.note}` : ''}`);
137
+ }
138
+ }
139
+ console.log();
140
+ break;
141
+ }
142
+
143
+ case 'impact': {
144
+ const result = engine.impact(component!, options.depth ?? 3);
145
+ if (format === 'json') {
146
+ console.log(JSON.stringify(result, null, 2));
147
+ return;
148
+ }
149
+
150
+ console.log(pc.bold(`\nImpact analysis: ${component}\n`));
151
+ console.log(` ${pc.red(`${result.totalAffected}`)} affected components, ${pc.red(`${result.affectedBlocks.length}`)} affected blocks\n`);
152
+
153
+ if (result.affected.length > 0) {
154
+ console.log(` ${pc.bold('Affected components:')}`);
155
+ for (const entry of result.affected) {
156
+ const indent = ' '.repeat(entry.depth + 1);
157
+ console.log(`${indent}${pc.yellow(entry.component)} ${pc.dim(`(depth ${entry.depth}, via ${entry.edgeType})`)}`);
158
+ }
159
+ }
160
+
161
+ if (result.affectedBlocks.length > 0) {
162
+ console.log(`\n ${pc.bold('Affected blocks:')} ${result.affectedBlocks.join(', ')}`);
163
+ }
164
+ console.log();
165
+ break;
166
+ }
167
+
168
+ case 'path': {
169
+ if (!options.target) {
170
+ console.error(pc.red('Error: --target is required for path mode.'));
171
+ process.exit(1);
172
+ }
173
+
174
+ const result = engine.path(component!, options.target);
175
+ if (format === 'json') {
176
+ console.log(JSON.stringify(result, null, 2));
177
+ return;
178
+ }
179
+
180
+ console.log(pc.bold(`\nPath: ${component} → ${options.target}\n`));
181
+ if (!result.found) {
182
+ console.log(` ${pc.red('No path found.')}`);
183
+ } else {
184
+ console.log(` ${result.path.map(n => pc.yellow(n)).join(pc.dim(' → '))}`);
185
+ if (result.edges.length > 0) {
186
+ console.log(` ${pc.dim(`(${result.edges.map(e => e.type).join(' → ')})`)}`);
187
+ }
188
+ }
189
+ console.log();
190
+ break;
191
+ }
192
+
193
+ case 'composition': {
194
+ const tree = engine.composition(component!);
195
+ if (format === 'json') {
196
+ console.log(JSON.stringify(tree, null, 2));
197
+ return;
198
+ }
199
+
200
+ console.log(pc.bold(`\nComposition: ${component}\n`));
201
+ console.log(` ${pc.cyan('Pattern:')} ${tree.compositionPattern ?? 'unknown'}`);
202
+ if (tree.parent) {
203
+ console.log(` ${pc.cyan('Parent:')} ${tree.parent}`);
204
+ }
205
+ if (tree.subComponents.length > 0) {
206
+ console.log(` ${pc.cyan('Sub-components:')}`);
207
+ for (const sub of tree.subComponents) {
208
+ const isRequired = tree.requiredChildren.includes(sub);
209
+ console.log(` ${pc.yellow(sub)}${isRequired ? pc.red(' (required)') : ''}`);
210
+ }
211
+ }
212
+ if (tree.siblings.length > 0) {
213
+ console.log(` ${pc.cyan('Siblings:')} ${tree.siblings.join(', ')}`);
214
+ }
215
+ if (tree.blocks.length > 0) {
216
+ console.log(` ${pc.cyan('In blocks:')} ${tree.blocks.join(', ')}`);
217
+ }
218
+ console.log();
219
+ break;
220
+ }
221
+
222
+ case 'alternatives': {
223
+ const alts = engine.alternatives(component!);
224
+ if (format === 'json') {
225
+ console.log(JSON.stringify({ component, alternatives: alts }, null, 2));
226
+ return;
227
+ }
228
+
229
+ console.log(pc.bold(`\nAlternatives for ${component}\n`));
230
+ if (alts.length === 0) {
231
+ console.log(' No known alternatives.');
232
+ } else {
233
+ for (const alt of alts) {
234
+ console.log(` ${pc.yellow(alt.component)}${alt.note ? ` — ${alt.note}` : ''}`);
235
+ }
236
+ }
237
+ console.log();
238
+ break;
239
+ }
240
+
241
+ case 'islands': {
242
+ const islands = engine.islands();
243
+ if (format === 'json') {
244
+ console.log(JSON.stringify({ islands }, null, 2));
245
+ return;
246
+ }
247
+
248
+ console.log(pc.bold(`\nConnected Islands (${islands.length})\n`));
249
+ for (let i = 0; i < islands.length; i++) {
250
+ console.log(` ${pc.cyan(`Island ${i + 1}`)} (${islands[i].length} components): ${islands[i].join(', ')}`);
251
+ }
252
+ console.log();
253
+ break;
254
+ }
255
+
256
+ default:
257
+ console.error(pc.red(`Unknown mode: "${mode}". Valid: health, dependencies, dependents, impact, path, composition, alternatives, islands`));
258
+ process.exit(1);
259
+ }
260
+
261
+ // Graphviz dot output
262
+ if (format === 'dot') {
263
+ const lines = ['digraph ComponentGraph {', ' rankdir=LR;', ' node [shape=box, style=rounded];'];
264
+ for (const node of graph.nodes) {
265
+ lines.push(` "${node.name}" [label="${node.name}\\n(${node.category})"];`);
266
+ }
267
+ for (const edge of graph.edges) {
268
+ const style = edge.type === 'alternative-to' ? 'dashed' : 'solid';
269
+ lines.push(` "${edge.source}" -> "${edge.target}" [label="${edge.type}", style=${style}];`);
270
+ }
271
+ lines.push('}');
272
+ console.log(lines.join('\n'));
273
+ }
274
+ }