@fragments-sdk/cli 0.8.1 → 0.9.1

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 (128) hide show
  1. package/dist/bin.js +517 -77
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-WI6SLMSO.js → chunk-5GT62FCB.js} +2 -2
  4. package/dist/{chunk-CJEGT3WD.js → chunk-BW3ZATBW.js} +20 -3
  5. package/dist/chunk-BW3ZATBW.js.map +1 -0
  6. package/dist/{chunk-2JIKCJX3.js → chunk-D7372LQX.js} +13 -6
  7. package/dist/chunk-D7372LQX.js.map +1 -0
  8. package/dist/chunk-EZYXYWNF.js +131 -0
  9. package/dist/chunk-EZYXYWNF.js.map +1 -0
  10. package/dist/{chunk-NGIMCIK2.js → chunk-GF6OVPIN.js} +2 -2
  11. package/dist/{chunk-GOVI6COW.js → chunk-NVSPGSKB.js} +12 -4
  12. package/dist/chunk-NVSPGSKB.js.map +1 -0
  13. package/dist/core/index.d.ts +105 -3
  14. package/dist/core/index.js +12 -2
  15. package/dist/{defineFragment-D0UTve-I.d.ts → defineFragment-CBMS7Bab.d.ts} +21 -1
  16. package/dist/generate-LQA2R7FN.js +461 -0
  17. package/dist/generate-LQA2R7FN.js.map +1 -0
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.js +5 -4
  20. package/dist/index.js.map +1 -1
  21. package/dist/{init-KFYN37ZY.js → init-2GEGVIUQ.js} +14 -76
  22. package/dist/init-2GEGVIUQ.js.map +1 -0
  23. package/dist/mcp-bin.js +4 -3
  24. package/dist/mcp-bin.js.map +1 -1
  25. package/dist/{scan-65RH3QMM.js → scan-JGS65S7P.js} +6 -5
  26. package/dist/{service-A5GIGGGK.js → service-XP2EAJXD.js} +4 -3
  27. package/dist/{static-viewer-NSODM5VX.js → static-viewer-XCS7UJTO.js} +4 -3
  28. package/dist/storyFilters-3LUYAFZF.js +15 -0
  29. package/dist/storyFilters-3LUYAFZF.js.map +1 -0
  30. package/dist/{test-RPWZAYSJ.js → test-TD6TJNVY.js} +3 -3
  31. package/dist/{tokens-NIXSZRX7.js → tokens-2EXPCVP3.js} +5 -4
  32. package/dist/{tokens-NIXSZRX7.js.map → tokens-2EXPCVP3.js.map} +1 -1
  33. package/dist/{viewer-HZK4BSDK.js → viewer-RFA2KVBG.js} +249 -22
  34. package/dist/viewer-RFA2KVBG.js.map +1 -0
  35. package/package.json +2 -2
  36. package/src/bin.ts +26 -0
  37. package/src/build.ts +12 -2
  38. package/src/commands/build.ts +16 -2
  39. package/src/commands/doctor.ts +498 -0
  40. package/src/commands/generate.ts +383 -68
  41. package/src/commands/init-framework.ts +1 -1
  42. package/src/commands/init.ts +9 -51
  43. package/src/core/config.ts +15 -2
  44. package/src/core/generators/typescript-extractor.ts +10 -0
  45. package/src/core/index.ts +15 -0
  46. package/src/core/schema.ts +10 -2
  47. package/src/core/storyFilters.test.ts +350 -0
  48. package/src/core/storyFilters.ts +253 -0
  49. package/src/core/types.ts +22 -0
  50. package/src/migrate/converter.ts +9 -1
  51. package/src/migrate/parser.ts +2 -0
  52. package/src/migrate/types.ts +2 -0
  53. package/src/setup.ts +69 -24
  54. package/src/viewer/__tests__/viewer-integration.test.ts +1 -1
  55. package/src/viewer/components/AccessibilityPanel.tsx +305 -312
  56. package/src/viewer/components/ActionsPanel.tsx +31 -29
  57. package/src/viewer/components/AllVariantsPreview.tsx +78 -0
  58. package/src/viewer/components/App.tsx +187 -740
  59. package/src/viewer/components/BottomPanel.tsx +228 -132
  60. package/src/viewer/components/CodePanel.tsx +1 -1
  61. package/src/viewer/components/CommandPalette.tsx +7 -10
  62. package/src/viewer/components/ComponentDocView.tsx +164 -0
  63. package/src/viewer/components/ComponentGraph.tsx +111 -142
  64. package/src/viewer/components/ContractPanel.tsx +6 -6
  65. package/src/viewer/components/EmptyVariantMessage.tsx +54 -0
  66. package/src/viewer/components/FigmaEmbed.tsx +20 -18
  67. package/src/viewer/components/FragmentEditor.tsx +92 -115
  68. package/src/viewer/components/HeaderSearch.tsx +24 -0
  69. package/src/viewer/components/HealthDashboard.tsx +16 -2
  70. package/src/viewer/components/Icons.tsx +9 -0
  71. package/src/viewer/components/InteractionsPanel.tsx +101 -117
  72. package/src/viewer/components/IsolatedPreviewFrame.tsx +1 -0
  73. package/src/viewer/components/LandingPage.tsx +3 -3
  74. package/src/viewer/components/LeftSidebar.tsx +141 -63
  75. package/src/viewer/components/LoadErrorMessage.tsx +102 -0
  76. package/src/viewer/components/MultiViewportPreview.tsx +61 -142
  77. package/src/viewer/components/NoVariantsMessage.tsx +59 -0
  78. package/src/viewer/components/PanelShell.tsx +161 -0
  79. package/src/viewer/components/PerformancePanel.tsx +31 -28
  80. package/src/viewer/components/PreviewArea.tsx +1 -1
  81. package/src/viewer/components/PreviewAside.tsx +168 -0
  82. package/src/viewer/components/PreviewFrameHost.tsx +3 -3
  83. package/src/viewer/components/PropsEditor.tsx +70 -156
  84. package/src/viewer/components/ResizablePanel.tsx +103 -263
  85. package/src/viewer/components/RightSidebar.tsx +3 -9
  86. package/src/viewer/components/SkeletonLoader.tsx +13 -13
  87. package/src/viewer/components/TokenStylePanel.tsx +182 -209
  88. package/src/viewer/components/TopToolbar.tsx +159 -0
  89. package/src/viewer/components/VariantMatrix.tsx +42 -86
  90. package/src/viewer/components/VariantTabs.tsx +3 -3
  91. package/src/viewer/components/ViewerHeader.tsx +69 -0
  92. package/src/viewer/components/WebMCPDevTools.tsx +17 -23
  93. package/src/viewer/components/viewer-utils.ts +16 -0
  94. package/src/viewer/entry.tsx +5 -0
  95. package/src/viewer/hooks/useAppState.ts +27 -4
  96. package/src/viewer/hooks/usePreviewBridge.ts +2 -2
  97. package/src/viewer/preview-frame.html +6 -12
  98. package/src/viewer/server.ts +184 -6
  99. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +10 -0
  100. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +2 -0
  101. package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +274 -0
  102. package/src/viewer/vendor/shared/src/DocsPageShell.tsx +5 -0
  103. package/src/viewer/vendor/shared/src/PropsTable.module.scss +68 -0
  104. package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +2 -0
  105. package/src/viewer/vendor/shared/src/PropsTable.tsx +76 -0
  106. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +122 -0
  107. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +2 -0
  108. package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +134 -0
  109. package/src/viewer/vendor/shared/src/docs-data/index.ts +32 -0
  110. package/src/viewer/vendor/shared/src/docs-data/mcp-configs.ts +72 -0
  111. package/src/viewer/vendor/shared/src/docs-data/palettes.ts +75 -0
  112. package/src/viewer/vendor/shared/src/docs-data/setup-examples.ts +55 -0
  113. package/src/viewer/vendor/shared/src/index.ts +8 -0
  114. package/src/viewer/vendor/shared/src/types.ts +12 -0
  115. package/src/viewer/vite-plugin.ts +109 -4
  116. package/dist/chunk-2JIKCJX3.js.map +0 -1
  117. package/dist/chunk-CJEGT3WD.js.map +0 -1
  118. package/dist/chunk-GOVI6COW.js.map +0 -1
  119. package/dist/generate-35OIMW4Y.js +0 -252
  120. package/dist/generate-35OIMW4Y.js.map +0 -1
  121. package/dist/init-KFYN37ZY.js.map +0 -1
  122. package/dist/viewer-HZK4BSDK.js.map +0 -1
  123. /package/dist/{chunk-WI6SLMSO.js.map → chunk-5GT62FCB.js.map} +0 -0
  124. /package/dist/{chunk-NGIMCIK2.js.map → chunk-GF6OVPIN.js.map} +0 -0
  125. /package/dist/{scan-65RH3QMM.js.map → scan-JGS65S7P.js.map} +0 -0
  126. /package/dist/{service-A5GIGGGK.js.map → service-XP2EAJXD.js.map} +0 -0
  127. /package/dist/{static-viewer-NSODM5VX.js.map → static-viewer-XCS7UJTO.js.map} +0 -0
  128. /package/dist/{test-RPWZAYSJ.js.map → test-TD6TJNVY.js.map} +0 -0
@@ -0,0 +1,498 @@
1
+ /**
2
+ * fragments doctor - Diagnose design system configuration
3
+ *
4
+ * Checks a consumer project for common setup issues:
5
+ * - Missing styles import
6
+ * - ThemeProvider not found
7
+ * - Invalid SCSS seed values
8
+ * - Missing peer dependencies
9
+ * - MCP configuration
10
+ */
11
+
12
+ import { readFile, access } from 'node:fs/promises';
13
+ import { join, resolve } from 'node:path';
14
+ import pc from 'picocolors';
15
+ import { BRAND } from '../core/index.js';
16
+
17
+ // ============================================
18
+ // Types
19
+ // ============================================
20
+
21
+ export interface DoctorOptions {
22
+ /** Project root directory (defaults to cwd) */
23
+ root?: string;
24
+ /** Output JSON instead of formatted text */
25
+ json?: boolean;
26
+ /** Auto-fix issues where possible */
27
+ fix?: boolean;
28
+ }
29
+
30
+ export interface DoctorCheck {
31
+ name: string;
32
+ status: 'pass' | 'warn' | 'fail';
33
+ message: string;
34
+ fix?: string;
35
+ }
36
+
37
+ export interface DoctorResult {
38
+ success: boolean;
39
+ checks: DoctorCheck[];
40
+ passed: number;
41
+ warned: number;
42
+ failed: number;
43
+ }
44
+
45
+ // ============================================
46
+ // Valid seed values (match _seeds.scss)
47
+ // ============================================
48
+
49
+ const VALID_NEUTRALS = ['stone', 'ice', 'earth', 'sand', 'fire'];
50
+ const VALID_DENSITIES = ['compact', 'default', 'relaxed'];
51
+ const VALID_RADII = ['sharp', 'subtle', 'default', 'rounded', 'pill'];
52
+
53
+ // ============================================
54
+ // Individual Checks
55
+ // ============================================
56
+
57
+ async function checkPackageInstalled(root: string): Promise<DoctorCheck> {
58
+ try {
59
+ const pkgPath = join(root, 'package.json');
60
+ const content = await readFile(pkgPath, 'utf-8');
61
+ const pkg = JSON.parse(content);
62
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
63
+
64
+ if (allDeps['@fragments-sdk/ui']) {
65
+ return {
66
+ name: 'Package installed',
67
+ status: 'pass',
68
+ message: `@fragments-sdk/ui ${allDeps['@fragments-sdk/ui']} found in dependencies`,
69
+ };
70
+ }
71
+
72
+ return {
73
+ name: 'Package installed',
74
+ status: 'fail',
75
+ message: '@fragments-sdk/ui not found in package.json',
76
+ fix: 'npm install @fragments-sdk/ui',
77
+ };
78
+ } catch {
79
+ return {
80
+ name: 'Package installed',
81
+ status: 'fail',
82
+ message: 'No package.json found',
83
+ };
84
+ }
85
+ }
86
+
87
+ async function checkStylesImport(root: string): Promise<DoctorCheck> {
88
+ const entryPatterns = [
89
+ 'src/main.tsx', 'src/main.ts', 'src/index.tsx', 'src/index.ts',
90
+ 'src/App.tsx', 'src/App.ts',
91
+ 'app/layout.tsx', 'app/layout.ts',
92
+ 'src/app/layout.tsx', 'src/app/layout.ts',
93
+ 'app/root.tsx',
94
+ 'pages/_app.tsx', 'pages/_app.ts',
95
+ ];
96
+
97
+ for (const pattern of entryPatterns) {
98
+ try {
99
+ const content = await readFile(join(root, pattern), 'utf-8');
100
+
101
+ // Check for correct import
102
+ if (content.includes("@fragments-sdk/ui/styles")) {
103
+ return {
104
+ name: 'Styles import',
105
+ status: 'pass',
106
+ message: `Found styles import in ${pattern}`,
107
+ };
108
+ }
109
+
110
+ // Check for deprecated ./globals import
111
+ if (content.includes("@fragments-sdk/ui/globals")) {
112
+ return {
113
+ name: 'Styles import',
114
+ status: 'warn',
115
+ message: `${pattern} uses deprecated '@fragments-sdk/ui/globals'. Use '@fragments-sdk/ui/styles' instead`,
116
+ fix: `Replace '@fragments-sdk/ui/globals' with '@fragments-sdk/ui/styles' in ${pattern}`,
117
+ };
118
+ }
119
+ } catch {
120
+ // File doesn't exist, continue
121
+ }
122
+ }
123
+
124
+ // Also check SCSS files for @use import
125
+ const scssPatterns = [
126
+ 'src/styles/globals.scss', 'src/globals.scss',
127
+ 'styles/globals.scss', 'app/globals.scss',
128
+ 'src/app/globals.scss',
129
+ 'app/styles/globals.scss',
130
+ ];
131
+
132
+ for (const pattern of scssPatterns) {
133
+ try {
134
+ const content = await readFile(join(root, pattern), 'utf-8');
135
+ if (content.includes("@fragments-sdk/ui/styles")) {
136
+ return {
137
+ name: 'Styles import',
138
+ status: 'pass',
139
+ message: `Found SCSS @use import in ${pattern}`,
140
+ };
141
+ }
142
+ } catch {
143
+ // File doesn't exist, continue
144
+ }
145
+ }
146
+
147
+ return {
148
+ name: 'Styles import',
149
+ status: 'fail',
150
+ message: 'No @fragments-sdk/ui/styles import found in entry files',
151
+ fix: "Add `import '@fragments-sdk/ui/styles'` to your app's entry file",
152
+ };
153
+ }
154
+
155
+ async function checkThemeProvider(root: string): Promise<DoctorCheck> {
156
+ const providerPatterns = [
157
+ 'src/main.tsx', 'src/App.tsx', 'src/providers.tsx',
158
+ 'app/layout.tsx', 'app/providers.tsx',
159
+ 'src/app/layout.tsx', 'src/app/providers.tsx',
160
+ 'app/root.tsx',
161
+ 'pages/_app.tsx',
162
+ ];
163
+
164
+ for (const pattern of providerPatterns) {
165
+ try {
166
+ const content = await readFile(join(root, pattern), 'utf-8');
167
+
168
+ if (content.includes('ThemeProvider')) {
169
+ // Check for deprecated defaultTheme prop
170
+ if (content.includes('defaultTheme=') || content.includes('defaultTheme =')) {
171
+ return {
172
+ name: 'ThemeProvider',
173
+ status: 'warn',
174
+ message: `${pattern} uses deprecated 'defaultTheme' prop. Use 'defaultMode' instead`,
175
+ fix: `Replace 'defaultTheme' with 'defaultMode' in ${pattern}`,
176
+ };
177
+ }
178
+
179
+ return {
180
+ name: 'ThemeProvider',
181
+ status: 'pass',
182
+ message: `ThemeProvider found in ${pattern}`,
183
+ };
184
+ }
185
+ } catch {
186
+ // File doesn't exist, continue
187
+ }
188
+ }
189
+
190
+ return {
191
+ name: 'ThemeProvider',
192
+ status: 'warn',
193
+ message: 'ThemeProvider not found in common entry files (optional but recommended)',
194
+ fix: "Wrap your app with <ThemeProvider defaultMode=\"system\">",
195
+ };
196
+ }
197
+
198
+ async function checkScssSeeds(root: string): Promise<DoctorCheck[]> {
199
+ const checks: DoctorCheck[] = [];
200
+
201
+ const scssPatterns = [
202
+ 'src/styles/globals.scss', 'src/globals.scss',
203
+ 'styles/globals.scss', 'app/globals.scss',
204
+ 'src/app/globals.scss',
205
+ 'app/styles/globals.scss',
206
+ ];
207
+
208
+ for (const pattern of scssPatterns) {
209
+ try {
210
+ const content = await readFile(join(root, pattern), 'utf-8');
211
+ if (!content.includes("@fragments-sdk/ui/styles")) continue;
212
+
213
+ // Check for standalone variable syntax (wrong)
214
+ const standalonePattern = /^\$fui-\w+:\s*.+;$/m;
215
+ if (standalonePattern.test(content) && !content.includes('@use')) {
216
+ checks.push({
217
+ name: 'SCSS syntax',
218
+ status: 'fail',
219
+ message: `${pattern} uses standalone $fui- variables. Must use @use...with() syntax`,
220
+ fix: "@use '@fragments-sdk/ui/styles' with ($fui-brand: #0066ff);",
221
+ });
222
+ }
223
+
224
+ // Validate neutral value
225
+ const neutralMatch = content.match(/\$fui-neutral:\s*"([^"]+)"/);
226
+ if (neutralMatch && !VALID_NEUTRALS.includes(neutralMatch[1])) {
227
+ checks.push({
228
+ name: 'SCSS seed: neutral',
229
+ status: 'fail',
230
+ message: `Invalid $fui-neutral: "${neutralMatch[1]}" in ${pattern}`,
231
+ fix: `Valid neutrals: ${VALID_NEUTRALS.join(', ')}`,
232
+ });
233
+ }
234
+
235
+ // Validate density value
236
+ const densityMatch = content.match(/\$fui-density:\s*"([^"]+)"/);
237
+ if (densityMatch && !VALID_DENSITIES.includes(densityMatch[1])) {
238
+ checks.push({
239
+ name: 'SCSS seed: density',
240
+ status: 'fail',
241
+ message: `Invalid $fui-density: "${densityMatch[1]}" in ${pattern}`,
242
+ fix: `Valid densities: ${VALID_DENSITIES.join(', ')}`,
243
+ });
244
+ }
245
+
246
+ // Validate radius-style value
247
+ const radiusMatch = content.match(/\$fui-radius-style:\s*"([^"]+)"/);
248
+ if (radiusMatch && !VALID_RADII.includes(radiusMatch[1])) {
249
+ checks.push({
250
+ name: 'SCSS seed: radius-style',
251
+ status: 'fail',
252
+ message: `Invalid $fui-radius-style: "${radiusMatch[1]}" in ${pattern}`,
253
+ fix: `Valid radius styles: ${VALID_RADII.join(', ')}`,
254
+ });
255
+ }
256
+
257
+ // Validate brand is a valid hex color
258
+ const brandMatch = content.match(/\$fui-brand:\s*(#[0-9a-fA-F]+)/);
259
+ if (brandMatch) {
260
+ const hex = brandMatch[1];
261
+ if (!/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(hex)) {
262
+ checks.push({
263
+ name: 'SCSS seed: brand',
264
+ status: 'fail',
265
+ message: `Invalid $fui-brand color: "${hex}" in ${pattern}`,
266
+ fix: 'Must be a valid hex color (e.g., #0066ff)',
267
+ });
268
+ }
269
+ }
270
+
271
+ // If we found the file and parsed it, and no seed issues, add a pass
272
+ if (checks.length === 0) {
273
+ checks.push({
274
+ name: 'SCSS seeds',
275
+ status: 'pass',
276
+ message: `Seed values in ${pattern} are valid`,
277
+ });
278
+ }
279
+
280
+ return checks;
281
+ } catch {
282
+ // File doesn't exist, continue
283
+ }
284
+ }
285
+
286
+ // No SCSS file found — not an error, just informational
287
+ checks.push({
288
+ name: 'SCSS seeds',
289
+ status: 'pass',
290
+ message: 'No custom SCSS seeds configured (using defaults)',
291
+ });
292
+
293
+ return checks;
294
+ }
295
+
296
+ async function checkPeerDeps(root: string): Promise<DoctorCheck[]> {
297
+ const checks: DoctorCheck[] = [];
298
+
299
+ try {
300
+ const pkgPath = join(root, 'package.json');
301
+ const content = await readFile(pkgPath, 'utf-8');
302
+ const pkg = JSON.parse(content);
303
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
304
+
305
+ // React is required
306
+ if (!allDeps['react']) {
307
+ checks.push({
308
+ name: 'Peer dep: react',
309
+ status: 'fail',
310
+ message: 'react not found in dependencies (required)',
311
+ fix: 'npm install react react-dom',
312
+ });
313
+ } else {
314
+ checks.push({
315
+ name: 'Peer dep: react',
316
+ status: 'pass',
317
+ message: `react ${allDeps['react']} installed`,
318
+ });
319
+ }
320
+
321
+ // sass is needed for custom theming
322
+ if (!allDeps['sass']) {
323
+ checks.push({
324
+ name: 'Peer dep: sass',
325
+ status: 'warn',
326
+ message: 'sass not installed (needed for custom SCSS theming)',
327
+ fix: 'npm install -D sass',
328
+ });
329
+ }
330
+
331
+ // Check optional peer deps that components might need
332
+ const optionalPeers: Array<{ pkg: string; components: string }> = [
333
+ { pkg: 'recharts', components: 'Chart' },
334
+ { pkg: 'shiki', components: 'CodeBlock' },
335
+ { pkg: 'react-day-picker', components: 'DatePicker' },
336
+ { pkg: '@tanstack/react-table', components: 'DataTable' },
337
+ ];
338
+
339
+ for (const peer of optionalPeers) {
340
+ if (allDeps[peer.pkg]) {
341
+ checks.push({
342
+ name: `Optional dep: ${peer.pkg}`,
343
+ status: 'pass',
344
+ message: `${peer.pkg} installed (enables ${peer.components})`,
345
+ });
346
+ }
347
+ }
348
+ } catch {
349
+ checks.push({
350
+ name: 'Peer dependencies',
351
+ status: 'fail',
352
+ message: 'Could not read package.json',
353
+ });
354
+ }
355
+
356
+ return checks;
357
+ }
358
+
359
+ async function checkMcpConfig(root: string): Promise<DoctorCheck> {
360
+ const mcpConfigPaths = [
361
+ '.mcp.json',
362
+ '.cursor/mcp.json',
363
+ '.vscode/mcp.json',
364
+ ];
365
+
366
+ for (const configPath of mcpConfigPaths) {
367
+ try {
368
+ const fullPath = join(root, configPath);
369
+ const content = await readFile(fullPath, 'utf-8');
370
+ const config = JSON.parse(content);
371
+
372
+ // Check if fragments MCP server is configured
373
+ const servers = config.mcpServers || config.servers || {};
374
+ const hasFragments = Object.values(servers).some((server: unknown) => {
375
+ const s = server as { command?: string; args?: string[] };
376
+ return (
377
+ s.args?.some((arg: string) => arg.includes('@fragments-sdk/mcp')) ||
378
+ s.command?.includes('fragments')
379
+ );
380
+ });
381
+
382
+ if (hasFragments) {
383
+ return {
384
+ name: 'MCP configuration',
385
+ status: 'pass',
386
+ message: `Fragments MCP server configured in ${configPath}`,
387
+ };
388
+ }
389
+ } catch {
390
+ // File doesn't exist or is invalid, continue
391
+ }
392
+ }
393
+
394
+ return {
395
+ name: 'MCP configuration',
396
+ status: 'warn',
397
+ message: 'No Fragments MCP server configuration found (optional)',
398
+ fix: 'Run `fragments init` or add @fragments-sdk/mcp to your MCP config',
399
+ };
400
+ }
401
+
402
+ async function checkTypeScript(root: string): Promise<DoctorCheck> {
403
+ try {
404
+ const tsconfigPath = join(root, 'tsconfig.json');
405
+ await access(tsconfigPath);
406
+ return {
407
+ name: 'TypeScript',
408
+ status: 'pass',
409
+ message: 'tsconfig.json found',
410
+ };
411
+ } catch {
412
+ return {
413
+ name: 'TypeScript',
414
+ status: 'warn',
415
+ message: 'No tsconfig.json found (TypeScript recommended but not required)',
416
+ };
417
+ }
418
+ }
419
+
420
+ // ============================================
421
+ // Main Doctor Function
422
+ // ============================================
423
+
424
+ /**
425
+ * Run diagnostic checks on a consumer project
426
+ */
427
+ export async function doctor(
428
+ options: DoctorOptions = {}
429
+ ): Promise<DoctorResult> {
430
+ const root = resolve(options.root ?? process.cwd());
431
+ const checks: DoctorCheck[] = [];
432
+
433
+ if (!options.json) {
434
+ console.log(pc.cyan(`\n${BRAND.name} Doctor\n`));
435
+ console.log(pc.dim(`Checking project at ${root}\n`));
436
+ }
437
+
438
+ // Run all checks
439
+ checks.push(await checkPackageInstalled(root));
440
+ checks.push(await checkStylesImport(root));
441
+ checks.push(await checkThemeProvider(root));
442
+ checks.push(...await checkScssSeeds(root));
443
+ checks.push(...await checkPeerDeps(root));
444
+ checks.push(await checkMcpConfig(root));
445
+ checks.push(await checkTypeScript(root));
446
+
447
+ const passed = checks.filter(c => c.status === 'pass').length;
448
+ const warned = checks.filter(c => c.status === 'warn').length;
449
+ const failed = checks.filter(c => c.status === 'fail').length;
450
+
451
+ const result: DoctorResult = {
452
+ success: failed === 0,
453
+ checks,
454
+ passed,
455
+ warned,
456
+ failed,
457
+ };
458
+
459
+ if (options.json) {
460
+ console.log(JSON.stringify(result, null, 2));
461
+ } else {
462
+ // Print results
463
+ for (const check of checks) {
464
+ const icon =
465
+ check.status === 'pass' ? pc.green('✓') :
466
+ check.status === 'warn' ? pc.yellow('!') :
467
+ pc.red('✗');
468
+
469
+ const msg =
470
+ check.status === 'pass' ? check.message :
471
+ check.status === 'warn' ? pc.yellow(check.message) :
472
+ pc.red(check.message);
473
+
474
+ console.log(` ${icon} ${pc.bold(check.name)}: ${msg}`);
475
+
476
+ if (check.fix && check.status !== 'pass') {
477
+ console.log(pc.dim(` → ${check.fix}`));
478
+ }
479
+ }
480
+
481
+ // Summary
482
+ console.log();
483
+ if (failed === 0 && warned === 0) {
484
+ console.log(pc.green(`✓ All ${passed} checks passed — your setup looks great!`));
485
+ } else if (failed === 0) {
486
+ console.log(pc.green(`✓ ${passed} passed`) + pc.yellow(`, ${warned} warning(s)`));
487
+ } else {
488
+ console.log(
489
+ pc.red(`✗ ${failed} failed`) +
490
+ (warned > 0 ? pc.yellow(`, ${warned} warning(s)`) : '') +
491
+ pc.dim(`, ${passed} passed`)
492
+ );
493
+ }
494
+ console.log();
495
+ }
496
+
497
+ return result;
498
+ }