@fragments-sdk/cli 0.2.2

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 (259) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/bin.d.ts +1 -0
  4. package/dist/bin.js +4783 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/chunk-4FDQSGKX.js +786 -0
  7. package/dist/chunk-4FDQSGKX.js.map +1 -0
  8. package/dist/chunk-7H2MMGYG.js +369 -0
  9. package/dist/chunk-7H2MMGYG.js.map +1 -0
  10. package/dist/chunk-BSCG3IP7.js +619 -0
  11. package/dist/chunk-BSCG3IP7.js.map +1 -0
  12. package/dist/chunk-LY2CFFPY.js +898 -0
  13. package/dist/chunk-LY2CFFPY.js.map +1 -0
  14. package/dist/chunk-MUZ6CM66.js +6636 -0
  15. package/dist/chunk-MUZ6CM66.js.map +1 -0
  16. package/dist/chunk-OAENNG3G.js +1489 -0
  17. package/dist/chunk-OAENNG3G.js.map +1 -0
  18. package/dist/chunk-XHNKNI6J.js +235 -0
  19. package/dist/chunk-XHNKNI6J.js.map +1 -0
  20. package/dist/core-DWKLGY4N.js +68 -0
  21. package/dist/core-DWKLGY4N.js.map +1 -0
  22. package/dist/generate-4LQNJ7SX.js +249 -0
  23. package/dist/generate-4LQNJ7SX.js.map +1 -0
  24. package/dist/index.d.ts +775 -0
  25. package/dist/index.js +41 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/init-EMVI47QG.js +416 -0
  28. package/dist/init-EMVI47QG.js.map +1 -0
  29. package/dist/mcp-bin.d.ts +1 -0
  30. package/dist/mcp-bin.js +1117 -0
  31. package/dist/mcp-bin.js.map +1 -0
  32. package/dist/scan-4YPRF7FV.js +12 -0
  33. package/dist/scan-4YPRF7FV.js.map +1 -0
  34. package/dist/service-QSZMZJBJ.js +208 -0
  35. package/dist/service-QSZMZJBJ.js.map +1 -0
  36. package/dist/static-viewer-MIPGZ4Z7.js +12 -0
  37. package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
  38. package/dist/test-SQ5ZHXWU.js +1067 -0
  39. package/dist/test-SQ5ZHXWU.js.map +1 -0
  40. package/dist/tokens-HSGMYK64.js +173 -0
  41. package/dist/tokens-HSGMYK64.js.map +1 -0
  42. package/dist/viewer-YRF4SQE4.js +11101 -0
  43. package/dist/viewer-YRF4SQE4.js.map +1 -0
  44. package/package.json +107 -0
  45. package/src/ai.ts +266 -0
  46. package/src/analyze.ts +265 -0
  47. package/src/bin.ts +916 -0
  48. package/src/build.ts +248 -0
  49. package/src/commands/a11y.ts +302 -0
  50. package/src/commands/add.ts +313 -0
  51. package/src/commands/audit.ts +195 -0
  52. package/src/commands/baseline.ts +221 -0
  53. package/src/commands/build.ts +144 -0
  54. package/src/commands/compare.ts +337 -0
  55. package/src/commands/context.ts +107 -0
  56. package/src/commands/dev.ts +107 -0
  57. package/src/commands/enhance.ts +858 -0
  58. package/src/commands/generate.ts +391 -0
  59. package/src/commands/init.ts +531 -0
  60. package/src/commands/link/figma.ts +645 -0
  61. package/src/commands/link/index.ts +10 -0
  62. package/src/commands/link/storybook.ts +267 -0
  63. package/src/commands/list.ts +49 -0
  64. package/src/commands/metrics.ts +114 -0
  65. package/src/commands/reset.ts +242 -0
  66. package/src/commands/scan.ts +537 -0
  67. package/src/commands/storygen.ts +207 -0
  68. package/src/commands/tokens.ts +251 -0
  69. package/src/commands/validate.ts +93 -0
  70. package/src/commands/verify.ts +215 -0
  71. package/src/core/composition.test.ts +262 -0
  72. package/src/core/composition.ts +255 -0
  73. package/src/core/config.ts +84 -0
  74. package/src/core/constants.ts +111 -0
  75. package/src/core/context.ts +380 -0
  76. package/src/core/defineSegment.ts +137 -0
  77. package/src/core/discovery.ts +337 -0
  78. package/src/core/figma.ts +263 -0
  79. package/src/core/fragment-types.ts +214 -0
  80. package/src/core/generators/context.ts +389 -0
  81. package/src/core/generators/index.ts +23 -0
  82. package/src/core/generators/registry.ts +364 -0
  83. package/src/core/generators/typescript-extractor.ts +374 -0
  84. package/src/core/importAnalyzer.ts +217 -0
  85. package/src/core/index.ts +149 -0
  86. package/src/core/loader.ts +155 -0
  87. package/src/core/node.ts +63 -0
  88. package/src/core/parser.ts +551 -0
  89. package/src/core/previewLoader.ts +172 -0
  90. package/src/core/schema/fragment.schema.json +189 -0
  91. package/src/core/schema/registry.schema.json +137 -0
  92. package/src/core/schema.ts +182 -0
  93. package/src/core/storyAdapter.test.ts +571 -0
  94. package/src/core/storyAdapter.ts +761 -0
  95. package/src/core/token-types.ts +287 -0
  96. package/src/core/types.ts +754 -0
  97. package/src/diff.ts +323 -0
  98. package/src/index.ts +43 -0
  99. package/src/mcp/__tests__/projectFields.test.ts +130 -0
  100. package/src/mcp/bin.ts +36 -0
  101. package/src/mcp/index.ts +8 -0
  102. package/src/mcp/server.ts +1310 -0
  103. package/src/mcp/utils.ts +54 -0
  104. package/src/mcp-bin.ts +36 -0
  105. package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
  106. package/src/migrate/__tests__/args/args.test.ts +452 -0
  107. package/src/migrate/__tests__/meta/meta.test.ts +198 -0
  108. package/src/migrate/__tests__/stories/stories.test.ts +278 -0
  109. package/src/migrate/__tests__/utils/utils.test.ts +371 -0
  110. package/src/migrate/__tests__/values/values.test.ts +303 -0
  111. package/src/migrate/bin.ts +108 -0
  112. package/src/migrate/converter.ts +658 -0
  113. package/src/migrate/detect.ts +196 -0
  114. package/src/migrate/index.ts +45 -0
  115. package/src/migrate/migrate.ts +163 -0
  116. package/src/migrate/parser.ts +1136 -0
  117. package/src/migrate/report.ts +624 -0
  118. package/src/migrate/types.ts +169 -0
  119. package/src/screenshot.ts +249 -0
  120. package/src/service/__tests__/ast-utils.test.ts +426 -0
  121. package/src/service/__tests__/enhance-scanner.test.ts +200 -0
  122. package/src/service/__tests__/figma/figma.test.ts +652 -0
  123. package/src/service/__tests__/metrics-store.test.ts +409 -0
  124. package/src/service/__tests__/patch-generator.test.ts +186 -0
  125. package/src/service/__tests__/props-extractor.test.ts +365 -0
  126. package/src/service/__tests__/token-registry.test.ts +267 -0
  127. package/src/service/analytics.ts +659 -0
  128. package/src/service/ast-utils.ts +444 -0
  129. package/src/service/browser-pool.ts +339 -0
  130. package/src/service/capture.ts +267 -0
  131. package/src/service/diff.ts +279 -0
  132. package/src/service/enhance/aggregator.ts +489 -0
  133. package/src/service/enhance/cache.ts +275 -0
  134. package/src/service/enhance/codebase-scanner.ts +357 -0
  135. package/src/service/enhance/context-generator.ts +529 -0
  136. package/src/service/enhance/doc-extractor.ts +523 -0
  137. package/src/service/enhance/index.ts +131 -0
  138. package/src/service/enhance/props-extractor.ts +665 -0
  139. package/src/service/enhance/scanner.ts +445 -0
  140. package/src/service/enhance/storybook-parser.ts +552 -0
  141. package/src/service/enhance/types.ts +346 -0
  142. package/src/service/enhance/variant-renderer.ts +479 -0
  143. package/src/service/figma.ts +1008 -0
  144. package/src/service/index.ts +249 -0
  145. package/src/service/metrics-store.ts +333 -0
  146. package/src/service/patch-generator.ts +349 -0
  147. package/src/service/report.ts +854 -0
  148. package/src/service/storage.ts +401 -0
  149. package/src/service/token-fixes.ts +281 -0
  150. package/src/service/token-parser.ts +504 -0
  151. package/src/service/token-registry.ts +721 -0
  152. package/src/service/utils.ts +172 -0
  153. package/src/setup.ts +241 -0
  154. package/src/shared/command-wrapper.ts +81 -0
  155. package/src/shared/dev-server-client.ts +199 -0
  156. package/src/shared/index.ts +8 -0
  157. package/src/shared/segment-loader.ts +59 -0
  158. package/src/shared/types.ts +147 -0
  159. package/src/static-viewer.ts +715 -0
  160. package/src/test/discovery.ts +172 -0
  161. package/src/test/index.ts +281 -0
  162. package/src/test/reporters/console.ts +194 -0
  163. package/src/test/reporters/json.ts +190 -0
  164. package/src/test/reporters/junit.ts +186 -0
  165. package/src/test/runner.ts +598 -0
  166. package/src/test/types.ts +245 -0
  167. package/src/test/watch.ts +200 -0
  168. package/src/validators.ts +152 -0
  169. package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
  170. package/src/viewer/__tests__/render-utils.test.ts +232 -0
  171. package/src/viewer/__tests__/style-utils.test.ts +404 -0
  172. package/src/viewer/bin.ts +86 -0
  173. package/src/viewer/cli/health.ts +256 -0
  174. package/src/viewer/cli/index.ts +33 -0
  175. package/src/viewer/cli/scan.ts +124 -0
  176. package/src/viewer/cli/utils.ts +174 -0
  177. package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
  178. package/src/viewer/components/ActionCapture.tsx +172 -0
  179. package/src/viewer/components/ActionsPanel.tsx +371 -0
  180. package/src/viewer/components/App.tsx +638 -0
  181. package/src/viewer/components/BottomPanel.tsx +224 -0
  182. package/src/viewer/components/CodePanel.tsx +589 -0
  183. package/src/viewer/components/CommandPalette.tsx +336 -0
  184. package/src/viewer/components/ComponentGraph.tsx +394 -0
  185. package/src/viewer/components/ComponentHeader.tsx +85 -0
  186. package/src/viewer/components/ContractPanel.tsx +234 -0
  187. package/src/viewer/components/ErrorBoundary.tsx +85 -0
  188. package/src/viewer/components/FigmaEmbed.tsx +231 -0
  189. package/src/viewer/components/FragmentEditor.tsx +485 -0
  190. package/src/viewer/components/HealthDashboard.tsx +452 -0
  191. package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
  192. package/src/viewer/components/Icons.tsx +417 -0
  193. package/src/viewer/components/InteractionsPanel.tsx +720 -0
  194. package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
  195. package/src/viewer/components/IsolatedRender.tsx +111 -0
  196. package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
  197. package/src/viewer/components/LandingPage.tsx +441 -0
  198. package/src/viewer/components/Layout.tsx +22 -0
  199. package/src/viewer/components/LeftSidebar.tsx +391 -0
  200. package/src/viewer/components/MultiViewportPreview.tsx +429 -0
  201. package/src/viewer/components/PreviewArea.tsx +404 -0
  202. package/src/viewer/components/PreviewFrameHost.tsx +310 -0
  203. package/src/viewer/components/PreviewPane.tsx +150 -0
  204. package/src/viewer/components/PreviewToolbar.tsx +176 -0
  205. package/src/viewer/components/PropsEditor.tsx +512 -0
  206. package/src/viewer/components/PropsTable.tsx +98 -0
  207. package/src/viewer/components/RelationsSection.tsx +57 -0
  208. package/src/viewer/components/ResizablePanel.tsx +328 -0
  209. package/src/viewer/components/RightSidebar.tsx +118 -0
  210. package/src/viewer/components/ScreenshotButton.tsx +90 -0
  211. package/src/viewer/components/Sidebar.tsx +169 -0
  212. package/src/viewer/components/SkeletonLoader.tsx +156 -0
  213. package/src/viewer/components/StoryRenderer.tsx +128 -0
  214. package/src/viewer/components/ThemeProvider.tsx +96 -0
  215. package/src/viewer/components/Toast.tsx +67 -0
  216. package/src/viewer/components/TokenStylePanel.tsx +708 -0
  217. package/src/viewer/components/UsageSection.tsx +95 -0
  218. package/src/viewer/components/VariantMatrix.tsx +350 -0
  219. package/src/viewer/components/VariantRenderer.tsx +131 -0
  220. package/src/viewer/components/VariantTabs.tsx +84 -0
  221. package/src/viewer/components/ViewportSelector.tsx +165 -0
  222. package/src/viewer/components/_future/CreatePage.tsx +836 -0
  223. package/src/viewer/composition-renderer.ts +381 -0
  224. package/src/viewer/constants/index.ts +1 -0
  225. package/src/viewer/constants/ui.ts +185 -0
  226. package/src/viewer/entry.tsx +299 -0
  227. package/src/viewer/hooks/index.ts +2 -0
  228. package/src/viewer/hooks/useA11yCache.ts +383 -0
  229. package/src/viewer/hooks/useA11yService.ts +498 -0
  230. package/src/viewer/hooks/useActions.ts +138 -0
  231. package/src/viewer/hooks/useAppState.ts +124 -0
  232. package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
  233. package/src/viewer/hooks/useHmrStatus.ts +109 -0
  234. package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
  235. package/src/viewer/hooks/usePreviewBridge.ts +347 -0
  236. package/src/viewer/hooks/useScrollSpy.ts +78 -0
  237. package/src/viewer/hooks/useUrlState.ts +330 -0
  238. package/src/viewer/hooks/useViewSettings.ts +125 -0
  239. package/src/viewer/index.html +28 -0
  240. package/src/viewer/index.ts +14 -0
  241. package/src/viewer/intelligence/healthReport.ts +505 -0
  242. package/src/viewer/intelligence/styleDrift.ts +340 -0
  243. package/src/viewer/intelligence/usageScanner.ts +309 -0
  244. package/src/viewer/jsx-parser.ts +485 -0
  245. package/src/viewer/postcss.config.js +6 -0
  246. package/src/viewer/preview-frame-entry.tsx +25 -0
  247. package/src/viewer/preview-frame.html +109 -0
  248. package/src/viewer/render-template.html +68 -0
  249. package/src/viewer/render-utils.ts +170 -0
  250. package/src/viewer/server.ts +276 -0
  251. package/src/viewer/style-utils.ts +414 -0
  252. package/src/viewer/styles/globals.css +355 -0
  253. package/src/viewer/tailwind.config.js +37 -0
  254. package/src/viewer/types/a11y.ts +197 -0
  255. package/src/viewer/utils/a11y-fixes.ts +471 -0
  256. package/src/viewer/utils/actionExport.ts +372 -0
  257. package/src/viewer/utils/colorSchemes.ts +201 -0
  258. package/src/viewer/utils/detectRelationships.ts +256 -0
  259. package/src/viewer/vite-plugin.ts +2143 -0
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ import pc from 'picocolors';
3
+ import { BRAND } from '../core/index.js';
4
+ import { createDevServer } from './server.js';
5
+ import { runCommand } from './cli/index.js';
6
+
7
+ async function startDevServer(): Promise<void> {
8
+ const args = process.argv.slice(2);
9
+ const portArg = args.find((arg) => arg.startsWith('--port='));
10
+ const port = portArg ? parseInt(portArg.split('=')[1], 10) : 6006;
11
+ const noOpen = args.includes('--no-open');
12
+
13
+ console.log(pc.cyan(`\n${BRAND.name} Dev Server\n`));
14
+ console.log(pc.dim('Starting development server...\n'));
15
+
16
+ try {
17
+ const server = await createDevServer({
18
+ port,
19
+ open: !noOpen,
20
+ });
21
+
22
+ const address = server.httpServer?.address();
23
+ const actualPort = typeof address === 'object' && address ? address.port : port;
24
+
25
+ console.log(pc.green(`✓ Server running at http://localhost:${actualPort}\n`));
26
+ console.log(pc.dim('Press Ctrl+C to stop\n'));
27
+ } catch (error) {
28
+ console.error(pc.red('Failed to start server:'), error);
29
+ process.exit(1);
30
+ }
31
+ }
32
+
33
+ function showHelp(): void {
34
+ console.log(pc.cyan(`\n${BRAND.name} CLI\n`));
35
+ console.log(`Usage: ${BRAND.cliCommand} <command> [options]\n`);
36
+ console.log('Commands:');
37
+ console.log(' dev Start the development server (default)');
38
+ console.log(' scan <dir> Scan codebase for component usage');
39
+ console.log(' health Generate design system health report');
40
+ console.log('');
41
+ console.log('Dev Server Options:');
42
+ console.log(' --port=<port> Port to run on (default: 6006)');
43
+ console.log(' --no-open Don\'t open browser automatically');
44
+ console.log('');
45
+ console.log('Scan Options:');
46
+ console.log(' --format=<fmt> Output format: table, json (default: table)');
47
+ console.log(' --component=<n> Filter to specific component');
48
+ console.log(' --verbose Show detailed usage info');
49
+ console.log('');
50
+ console.log('Health Options:');
51
+ console.log(' --format=<fmt> Output format: table, json, markdown (default: table)');
52
+ console.log(' --scan=<dir> Include codebase usage scan');
53
+ console.log(' --ci CI mode: exit 1 if score below threshold');
54
+ console.log(' --threshold=<n> Minimum score for CI mode (default: 80)');
55
+ console.log('');
56
+ }
57
+
58
+ async function main(): Promise<void> {
59
+ const args = process.argv.slice(2);
60
+ const command = args[0];
61
+
62
+ // Show help
63
+ if (command === '--help' || command === '-h' || command === 'help') {
64
+ showHelp();
65
+ return;
66
+ }
67
+
68
+ // Route to subcommands
69
+ if (command === 'scan' || command === 'health') {
70
+ await runCommand(command, args.slice(1));
71
+ return;
72
+ }
73
+
74
+ // Default: start dev server (handles 'dev' command or no command)
75
+ if (!command || command === 'dev' || command.startsWith('--')) {
76
+ await startDevServer();
77
+ return;
78
+ }
79
+
80
+ // Unknown command
81
+ console.error(pc.red(`Unknown command: ${command}`));
82
+ console.log(pc.dim(`Run "${BRAND.cliCommand} --help" for usage information.\n`));
83
+ process.exit(1);
84
+ }
85
+
86
+ main();
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Health Command
3
+ * Generates a comprehensive design system health report
4
+ */
5
+
6
+ import pc from 'picocolors';
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import { BRAND } from '../../core/index.js';
10
+ import { loadConfig } from '../../core/node.js';
11
+ import { scanForUsages } from '../intelligence/usageScanner.js';
12
+ import { generateHealthReport, formatHealthReportAsMarkdown, type HealthReport } from '../intelligence/healthReport.js';
13
+ import {
14
+ parseArgs,
15
+ createSpinner,
16
+ formatScore,
17
+ printHeader,
18
+ printKV,
19
+ printWarning,
20
+ printSuccess,
21
+ formatList,
22
+ } from './utils.js';
23
+
24
+ type OutputFormat = 'table' | 'json' | 'markdown';
25
+
26
+ interface HealthCommandOptions {
27
+ format: OutputFormat;
28
+ scanDir?: string;
29
+ ci: boolean;
30
+ threshold: number;
31
+ }
32
+
33
+ function parseOptions(args: string[]): HealthCommandOptions {
34
+ const { options } = parseArgs(args);
35
+
36
+ return {
37
+ format: (options.format as OutputFormat) || 'table',
38
+ scanDir: options.scan as string | undefined,
39
+ ci: !!options.ci,
40
+ threshold: options.threshold ? parseInt(options.threshold as string, 10) : 80,
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Load segments from the project
46
+ */
47
+ async function loadSegments(): Promise<Array<{ path: string; segment: any }>> {
48
+ const configResult = await loadConfig();
49
+
50
+ if (!configResult) {
51
+ throw new Error('No fragments.config.ts found. Run this command from a Fragments project.');
52
+ }
53
+
54
+ const { config } = configResult;
55
+
56
+ // Try to load compiled segments
57
+ const segmentsPath = path.resolve(process.cwd(), config.outFile || `${BRAND.dataDir}/${BRAND.outFile}`);
58
+
59
+ if (fs.existsSync(segmentsPath)) {
60
+ const data = JSON.parse(fs.readFileSync(segmentsPath, 'utf-8'));
61
+ return Object.entries(data.segments || {}).map(([name, segment]: [string, any]) => ({
62
+ path: segment.filePath || name,
63
+ segment: {
64
+ meta: segment.meta || { name, description: '', category: '' },
65
+ usage: segment.usage || { when: [], whenNot: [] },
66
+ props: segment.props || {},
67
+ variants: segment.variants || [],
68
+ relations: segment.relations,
69
+ contract: segment.contract,
70
+ _generated: segment._generated,
71
+ component: () => null, // Placeholder
72
+ },
73
+ }));
74
+ }
75
+
76
+ // Fallback: return empty if no compiled segments
77
+ console.log(pc.yellow(`No compiled segments found. Run \`${BRAND.cliCommand} build\` first for complete analysis.`));
78
+ return [];
79
+ }
80
+
81
+ function formatGrade(grade: string): string {
82
+ switch (grade) {
83
+ case 'A':
84
+ return pc.green(pc.bold(grade));
85
+ case 'B':
86
+ return pc.cyan(grade);
87
+ case 'C':
88
+ return pc.yellow(grade);
89
+ case 'D':
90
+ return pc.magenta(grade);
91
+ case 'F':
92
+ return pc.red(pc.bold(grade));
93
+ default:
94
+ return grade;
95
+ }
96
+ }
97
+
98
+ function formatTableOutput(report: HealthReport): void {
99
+ console.log(pc.cyan(`\n${BRAND.name} Health Report\n`));
100
+ console.log(pc.dim(`Generated: ${new Date(report.generatedAt).toLocaleString()}\n`));
101
+
102
+ // Overall score
103
+ const scoreColor = report.overallScore >= 80 ? pc.green : report.overallScore >= 60 ? pc.yellow : pc.red;
104
+ console.log(` ${pc.bold('Overall Score:')} ${scoreColor(pc.bold(`${report.overallScore}%`))}\n`);
105
+
106
+ // Grade summary
107
+ console.log(pc.bold(' Category Grades:\n'));
108
+ console.log(` Usage: ${formatGrade(report.grades.usage)} (${report.usage.adoptionRate}% adoption)`);
109
+ console.log(` Style Drift: ${formatGrade(report.grades.drift)} (${report.styleDrift.averageCompliance}% compliance)`);
110
+ console.log(` Documentation: ${formatGrade(report.grades.documentation)} (${report.documentation.documentationRate}% documented)`);
111
+ console.log(` Token Usage: ${formatGrade(report.grades.tokens)} (${report.tokens.tokenizationRate}% tokenized)`);
112
+ console.log('');
113
+
114
+ // Usage section
115
+ printHeader('Usage Statistics');
116
+ printKV('Total Components', report.usage.totalComponents.toString());
117
+ printKV('Used Components', report.usage.usedComponents.toString());
118
+ printKV('Adoption Rate', formatScore(report.usage.adoptionRate));
119
+
120
+ if (report.usage.mostUsed.length > 0) {
121
+ console.log(pc.dim('\n Most Used:'));
122
+ for (const item of report.usage.mostUsed.slice(0, 5)) {
123
+ console.log(` ${pc.cyan(item.component)}: ${item.count} uses`);
124
+ }
125
+ }
126
+
127
+ if (report.usage.unusedComponents.length > 0 && report.usage.unusedComponents.length <= 5) {
128
+ console.log(pc.dim('\n Unused:'));
129
+ for (const name of report.usage.unusedComponents) {
130
+ printWarning(name);
131
+ }
132
+ } else if (report.usage.unusedComponents.length > 5) {
133
+ printWarning(`${report.usage.unusedComponents.length} unused components`);
134
+ }
135
+
136
+ // Style Drift section
137
+ printHeader('Style Drift Analysis');
138
+ printKV('Components with Drift', report.styleDrift.componentsWithDrift.toString());
139
+ printKV('Total Drifts', report.styleDrift.totalDrifts.toString());
140
+ printKV('Average Compliance', formatScore(report.styleDrift.averageCompliance));
141
+
142
+ if (report.styleDrift.bySeverity.high > 0) {
143
+ console.log('');
144
+ printWarning(`${report.styleDrift.bySeverity.high} high-severity drift(s) - affects brand identity`);
145
+ }
146
+
147
+ if (report.styleDrift.worstOffenders.length > 0) {
148
+ console.log(pc.dim('\n Needs Attention:'));
149
+ for (const item of report.styleDrift.worstOffenders.slice(0, 3)) {
150
+ console.log(` ${pc.red('!')} ${item.component} (${item.variant}): ${item.driftCount} drifts`);
151
+ }
152
+ }
153
+
154
+ // Documentation section
155
+ printHeader('Documentation Coverage');
156
+ printKV('Documented', `${report.documentation.documented} components`);
157
+ printKV('Coverage', formatScore(report.documentation.documentationRate));
158
+
159
+ if (report.documentation.missingDescriptions.length > 0) {
160
+ printWarning(`${report.documentation.missingDescriptions.length} missing descriptions`);
161
+ }
162
+
163
+ if (report.documentation.missingUsage.length > 0) {
164
+ printWarning(`${report.documentation.missingUsage.length} missing usage guidelines`);
165
+ }
166
+
167
+ // Token section
168
+ printHeader('Design Token Usage');
169
+ printKV('Token Compliance', formatScore(report.tokens.complianceScore));
170
+ printKV('Tokenized Values', report.tokens.tokenizedValues.toString());
171
+ printKV('Hardcoded Values', report.tokens.hardcodedValues.toString(), report.tokens.hardcodedValues > 0 ? pc.yellow : undefined);
172
+
173
+ // Recommendations
174
+ if (report.recommendations.length > 0) {
175
+ printHeader('Recommendations');
176
+ console.log('');
177
+ for (const rec of report.recommendations) {
178
+ console.log(` ${pc.yellow('→')} ${rec}`);
179
+ }
180
+ }
181
+
182
+ console.log('');
183
+ }
184
+
185
+ function formatJsonOutput(report: HealthReport): void {
186
+ console.log(JSON.stringify(report, null, 2));
187
+ }
188
+
189
+ function formatMarkdownOutput(report: HealthReport): void {
190
+ console.log(formatHealthReportAsMarkdown(report));
191
+ }
192
+
193
+ /**
194
+ * Run the health command
195
+ */
196
+ export async function healthCommand(args: string[]): Promise<void> {
197
+ const options = parseOptions(args);
198
+
199
+ const spinner = createSpinner('Analyzing design system health...');
200
+ spinner.start();
201
+
202
+ try {
203
+ // Load segments
204
+ spinner.update('Loading segments...');
205
+ const segments = await loadSegments();
206
+
207
+ if (segments.length === 0) {
208
+ spinner.stop(false, 'No segments found');
209
+ console.log(pc.yellow(`\nNo components to analyze. Create segment files or run \`${BRAND.cliCommand} build\`.\n`));
210
+ return;
211
+ }
212
+
213
+ // Run usage scan if directory provided
214
+ let usageScan;
215
+ if (options.scanDir) {
216
+ spinner.update(`Scanning ${options.scanDir} for component usage...`);
217
+ usageScan = await scanForUsages({ directory: options.scanDir });
218
+ }
219
+
220
+ // Generate report
221
+ spinner.update('Generating health report...');
222
+ const report = generateHealthReport({
223
+ segments,
224
+ usageScan,
225
+ // Note: Drift reports would require running the viewer and extracting styles
226
+ // For CLI, we focus on what can be statically analyzed
227
+ });
228
+
229
+ spinner.stop(true, 'Health analysis complete');
230
+
231
+ // Output
232
+ switch (options.format) {
233
+ case 'json':
234
+ formatJsonOutput(report);
235
+ break;
236
+ case 'markdown':
237
+ formatMarkdownOutput(report);
238
+ break;
239
+ default:
240
+ formatTableOutput(report);
241
+ }
242
+
243
+ // CI mode: exit with error if below threshold
244
+ if (options.ci) {
245
+ if (report.overallScore < options.threshold) {
246
+ console.log(pc.red(`\nCI Check Failed: Score ${report.overallScore}% is below threshold ${options.threshold}%\n`));
247
+ process.exit(1);
248
+ } else {
249
+ console.log(pc.green(`\nCI Check Passed: Score ${report.overallScore}% meets threshold ${options.threshold}%\n`));
250
+ }
251
+ }
252
+ } catch (error) {
253
+ spinner.stop(false, 'Health analysis failed');
254
+ throw error;
255
+ }
256
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * CLI Command Router
3
+ * Routes subcommands to their respective handlers
4
+ */
5
+
6
+ import pc from 'picocolors';
7
+ import { scanCommand } from './scan.js';
8
+ import { healthCommand } from './health.js';
9
+
10
+ export type CommandName = 'scan' | 'health';
11
+
12
+ /**
13
+ * Run a CLI command with the given arguments
14
+ */
15
+ export async function runCommand(command: CommandName, args: string[]): Promise<void> {
16
+ try {
17
+ switch (command) {
18
+ case 'scan':
19
+ await scanCommand(args);
20
+ break;
21
+ case 'health':
22
+ await healthCommand(args);
23
+ break;
24
+ default:
25
+ console.error(pc.red(`Unknown command: ${command}`));
26
+ process.exit(1);
27
+ }
28
+ } catch (error) {
29
+ const message = error instanceof Error ? error.message : String(error);
30
+ console.error(pc.red(`\nError: ${message}\n`));
31
+ process.exit(1);
32
+ }
33
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Scan Command
3
+ * Scans a codebase for component usage
4
+ */
5
+
6
+ import pc from 'picocolors';
7
+ import path from 'path';
8
+ import { scanForUsages, type FullScanResult, type UsageScanResult } from '../intelligence/usageScanner.js';
9
+ import { parseArgs, createSpinner, formatTable, formatDuration, printHeader, printKV } from './utils.js';
10
+
11
+ type OutputFormat = 'table' | 'json';
12
+
13
+ interface ScanCommandOptions {
14
+ format: OutputFormat;
15
+ component?: string;
16
+ verbose: boolean;
17
+ }
18
+
19
+ function parseOptions(args: string[]): { directory: string; options: ScanCommandOptions } {
20
+ const { positional, options } = parseArgs(args);
21
+
22
+ return {
23
+ directory: positional[0] || '.',
24
+ options: {
25
+ format: (options.format as OutputFormat) || 'table',
26
+ component: options.component as string | undefined,
27
+ verbose: !!options.verbose || !!options.v,
28
+ },
29
+ };
30
+ }
31
+
32
+ function formatTableOutput(result: FullScanResult, options: ScanCommandOptions): void {
33
+ const { results, summary } = result;
34
+
35
+ printHeader('Component Usage Scan');
36
+ printKV('Directory scanned', path.resolve(result.results[0]?.usages[0]?.file || '.').split(path.sep).slice(0, -1).join(path.sep) || '.');
37
+ printKV('Files scanned', summary.totalFiles.toString());
38
+ printKV('Files with usage', summary.filesWithUsage.toString());
39
+ printKV('Scan time', formatDuration(summary.scanTimeMs));
40
+ console.log('');
41
+
42
+ if (results.length === 0) {
43
+ console.log(pc.yellow(' No component usages found.\n'));
44
+ return;
45
+ }
46
+
47
+ // Summary table
48
+ const headers = ['Component', 'Files', 'Total Uses'];
49
+ const rows = results.map((r) => [
50
+ r.component,
51
+ String(r.usages.length),
52
+ String(r.totalUsages),
53
+ ]);
54
+
55
+ console.log(formatTable(headers, rows));
56
+ console.log('');
57
+
58
+ // Verbose: show file details
59
+ if (options.verbose) {
60
+ console.log(pc.bold('\nDetailed Usage:\n'));
61
+
62
+ for (const r of results) {
63
+ console.log(pc.cyan(` ${r.component}`));
64
+ for (const usage of r.usages) {
65
+ const relativePath = path.relative(process.cwd(), usage.file);
66
+ const importIcon = usage.importType === 'named' ? '{}' : usage.importType === 'default' ? '=>' : '*';
67
+ console.log(pc.dim(` ${importIcon} ${relativePath}:${usage.line} (${usage.usageCount} uses)`));
68
+ }
69
+ console.log('');
70
+ }
71
+ }
72
+
73
+ // Summary footer
74
+ console.log(pc.dim('─'.repeat(50)));
75
+ console.log(` ${pc.bold(String(summary.totalComponents))} components used ${pc.bold(String(summary.totalUsages))} times across ${pc.bold(String(summary.filesWithUsage))} files`);
76
+ console.log('');
77
+ }
78
+
79
+ function formatJsonOutput(result: FullScanResult): void {
80
+ const output = {
81
+ summary: result.summary,
82
+ components: result.results.map((r) => ({
83
+ name: r.component,
84
+ totalUsages: r.totalUsages,
85
+ fileCount: r.usages.length,
86
+ usages: r.usages.map((u) => ({
87
+ file: path.relative(process.cwd(), u.file),
88
+ line: u.line,
89
+ importType: u.importType,
90
+ usageCount: u.usageCount,
91
+ })),
92
+ })),
93
+ };
94
+
95
+ console.log(JSON.stringify(output, null, 2));
96
+ }
97
+
98
+ /**
99
+ * Run the scan command
100
+ */
101
+ export async function scanCommand(args: string[]): Promise<void> {
102
+ const { directory, options } = parseOptions(args);
103
+
104
+ const spinner = createSpinner(`Scanning ${directory} for component usage...`);
105
+ spinner.start();
106
+
107
+ try {
108
+ const result = await scanForUsages({
109
+ directory,
110
+ components: options.component ? [options.component] : undefined,
111
+ });
112
+
113
+ spinner.stop(true, `Scanned ${result.summary.totalFiles} files in ${formatDuration(result.summary.scanTimeMs)}`);
114
+
115
+ if (options.format === 'json') {
116
+ formatJsonOutput(result);
117
+ } else {
118
+ formatTableOutput(result, options);
119
+ }
120
+ } catch (error) {
121
+ spinner.stop(false, 'Scan failed');
122
+ throw error;
123
+ }
124
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Shared CLI Utilities
3
+ * Common helpers for CLI commands
4
+ */
5
+
6
+ import pc from 'picocolors';
7
+
8
+ /**
9
+ * Parse command line arguments into options object
10
+ */
11
+ export function parseArgs(args: string[]): {
12
+ positional: string[];
13
+ options: Record<string, string | boolean>;
14
+ } {
15
+ const positional: string[] = [];
16
+ const options: Record<string, string | boolean> = {};
17
+
18
+ for (const arg of args) {
19
+ if (arg.startsWith('--')) {
20
+ const [key, value] = arg.slice(2).split('=');
21
+ options[key] = value ?? true;
22
+ } else if (arg.startsWith('-')) {
23
+ const key = arg.slice(1);
24
+ options[key] = true;
25
+ } else {
26
+ positional.push(arg);
27
+ }
28
+ }
29
+
30
+ return { positional, options };
31
+ }
32
+
33
+ /**
34
+ * Spinner for async operations
35
+ */
36
+ export function createSpinner(message: string): {
37
+ start: () => void;
38
+ stop: (success?: boolean, finalMessage?: string) => void;
39
+ update: (message: string) => void;
40
+ } {
41
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
42
+ let frameIndex = 0;
43
+ let interval: NodeJS.Timeout | null = null;
44
+ let currentMessage = message;
45
+
46
+ return {
47
+ start() {
48
+ process.stdout.write('\n');
49
+ interval = setInterval(() => {
50
+ process.stdout.write(`\r${pc.cyan(frames[frameIndex])} ${currentMessage}`);
51
+ frameIndex = (frameIndex + 1) % frames.length;
52
+ }, 80);
53
+ },
54
+ stop(success = true, finalMessage?: string) {
55
+ if (interval) {
56
+ clearInterval(interval);
57
+ interval = null;
58
+ }
59
+ const icon = success ? pc.green('✓') : pc.red('✗');
60
+ process.stdout.write(`\r${icon} ${finalMessage || currentMessage}\n`);
61
+ },
62
+ update(message: string) {
63
+ currentMessage = message;
64
+ },
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Format a table for console output
70
+ */
71
+ export function formatTable(
72
+ headers: string[],
73
+ rows: string[][],
74
+ options: { padding?: number; maxWidth?: number } = {}
75
+ ): string {
76
+ const { padding = 2, maxWidth = 50 } = options;
77
+
78
+ // Calculate column widths
79
+ const widths = headers.map((h, i) => {
80
+ const columnValues = [h, ...rows.map((r) => r[i] || '')];
81
+ return Math.min(maxWidth, Math.max(...columnValues.map((v) => v.length)));
82
+ });
83
+
84
+ // Format header
85
+ const headerLine = headers
86
+ .map((h, i) => h.padEnd(widths[i] + padding))
87
+ .join('');
88
+ const separator = widths.map((w) => '─'.repeat(w + padding)).join('');
89
+
90
+ // Format rows
91
+ const formattedRows = rows.map((row) =>
92
+ row.map((cell, i) => (cell || '').padEnd(widths[i] + padding)).join('')
93
+ );
94
+
95
+ return [
96
+ pc.bold(headerLine),
97
+ pc.dim(separator),
98
+ ...formattedRows,
99
+ ].join('\n');
100
+ }
101
+
102
+ /**
103
+ * Format bytes as human-readable size
104
+ */
105
+ export function formatBytes(bytes: number): string {
106
+ if (bytes === 0) return '0 B';
107
+ const k = 1024;
108
+ const sizes = ['B', 'KB', 'MB', 'GB'];
109
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
110
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
111
+ }
112
+
113
+ /**
114
+ * Format duration in milliseconds
115
+ */
116
+ export function formatDuration(ms: number): string {
117
+ if (ms < 1000) return `${ms}ms`;
118
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
119
+ return `${(ms / 60000).toFixed(1)}m`;
120
+ }
121
+
122
+ /**
123
+ * Format a score with color based on thresholds
124
+ */
125
+ export function formatScore(score: number, thresholds = { good: 80, warn: 50 }): string {
126
+ const formatted = `${score}%`;
127
+ if (score >= thresholds.good) return pc.green(formatted);
128
+ if (score >= thresholds.warn) return pc.yellow(formatted);
129
+ return pc.red(formatted);
130
+ }
131
+
132
+ /**
133
+ * Print a section header
134
+ */
135
+ export function printHeader(title: string): void {
136
+ console.log(`\n${pc.bold(pc.cyan(title))}`);
137
+ console.log(pc.dim('─'.repeat(title.length + 4)));
138
+ }
139
+
140
+ /**
141
+ * Print a key-value pair
142
+ */
143
+ export function printKV(key: string, value: string | number, color?: (s: string) => string): void {
144
+ const valueStr = String(value);
145
+ console.log(` ${pc.dim(key + ':')} ${color ? color(valueStr) : valueStr}`);
146
+ }
147
+
148
+ /**
149
+ * Print a warning message
150
+ */
151
+ export function printWarning(message: string): void {
152
+ console.log(pc.yellow(` ⚠ ${message}`));
153
+ }
154
+
155
+ /**
156
+ * Print an error message
157
+ */
158
+ export function printError(message: string): void {
159
+ console.log(pc.red(` ✗ ${message}`));
160
+ }
161
+
162
+ /**
163
+ * Print a success message
164
+ */
165
+ export function printSuccess(message: string): void {
166
+ console.log(pc.green(` ✓ ${message}`));
167
+ }
168
+
169
+ /**
170
+ * Format a list for bullet points
171
+ */
172
+ export function formatList(items: string[], bullet = '•'): string {
173
+ return items.map((item) => ` ${pc.dim(bullet)} ${item}`).join('\n');
174
+ }