@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.
- package/dist/bin.js +517 -77
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-WI6SLMSO.js → chunk-5GT62FCB.js} +2 -2
- package/dist/{chunk-CJEGT3WD.js → chunk-BW3ZATBW.js} +20 -3
- package/dist/chunk-BW3ZATBW.js.map +1 -0
- package/dist/{chunk-2JIKCJX3.js → chunk-D7372LQX.js} +13 -6
- package/dist/chunk-D7372LQX.js.map +1 -0
- package/dist/chunk-EZYXYWNF.js +131 -0
- package/dist/chunk-EZYXYWNF.js.map +1 -0
- package/dist/{chunk-NGIMCIK2.js → chunk-GF6OVPIN.js} +2 -2
- package/dist/{chunk-GOVI6COW.js → chunk-NVSPGSKB.js} +12 -4
- package/dist/chunk-NVSPGSKB.js.map +1 -0
- package/dist/core/index.d.ts +105 -3
- package/dist/core/index.js +12 -2
- package/dist/{defineFragment-D0UTve-I.d.ts → defineFragment-CBMS7Bab.d.ts} +21 -1
- package/dist/generate-LQA2R7FN.js +461 -0
- package/dist/generate-LQA2R7FN.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/{init-KFYN37ZY.js → init-2GEGVIUQ.js} +14 -76
- package/dist/init-2GEGVIUQ.js.map +1 -0
- package/dist/mcp-bin.js +4 -3
- package/dist/mcp-bin.js.map +1 -1
- package/dist/{scan-65RH3QMM.js → scan-JGS65S7P.js} +6 -5
- package/dist/{service-A5GIGGGK.js → service-XP2EAJXD.js} +4 -3
- package/dist/{static-viewer-NSODM5VX.js → static-viewer-XCS7UJTO.js} +4 -3
- package/dist/storyFilters-3LUYAFZF.js +15 -0
- package/dist/storyFilters-3LUYAFZF.js.map +1 -0
- package/dist/{test-RPWZAYSJ.js → test-TD6TJNVY.js} +3 -3
- package/dist/{tokens-NIXSZRX7.js → tokens-2EXPCVP3.js} +5 -4
- package/dist/{tokens-NIXSZRX7.js.map → tokens-2EXPCVP3.js.map} +1 -1
- package/dist/{viewer-HZK4BSDK.js → viewer-RFA2KVBG.js} +249 -22
- package/dist/viewer-RFA2KVBG.js.map +1 -0
- package/package.json +2 -2
- package/src/bin.ts +26 -0
- package/src/build.ts +12 -2
- package/src/commands/build.ts +16 -2
- package/src/commands/doctor.ts +498 -0
- package/src/commands/generate.ts +383 -68
- package/src/commands/init-framework.ts +1 -1
- package/src/commands/init.ts +9 -51
- package/src/core/config.ts +15 -2
- package/src/core/generators/typescript-extractor.ts +10 -0
- package/src/core/index.ts +15 -0
- package/src/core/schema.ts +10 -2
- package/src/core/storyFilters.test.ts +350 -0
- package/src/core/storyFilters.ts +253 -0
- package/src/core/types.ts +22 -0
- package/src/migrate/converter.ts +9 -1
- package/src/migrate/parser.ts +2 -0
- package/src/migrate/types.ts +2 -0
- package/src/setup.ts +69 -24
- package/src/viewer/__tests__/viewer-integration.test.ts +1 -1
- package/src/viewer/components/AccessibilityPanel.tsx +305 -312
- package/src/viewer/components/ActionsPanel.tsx +31 -29
- package/src/viewer/components/AllVariantsPreview.tsx +78 -0
- package/src/viewer/components/App.tsx +187 -740
- package/src/viewer/components/BottomPanel.tsx +228 -132
- package/src/viewer/components/CodePanel.tsx +1 -1
- package/src/viewer/components/CommandPalette.tsx +7 -10
- package/src/viewer/components/ComponentDocView.tsx +164 -0
- package/src/viewer/components/ComponentGraph.tsx +111 -142
- package/src/viewer/components/ContractPanel.tsx +6 -6
- package/src/viewer/components/EmptyVariantMessage.tsx +54 -0
- package/src/viewer/components/FigmaEmbed.tsx +20 -18
- package/src/viewer/components/FragmentEditor.tsx +92 -115
- package/src/viewer/components/HeaderSearch.tsx +24 -0
- package/src/viewer/components/HealthDashboard.tsx +16 -2
- package/src/viewer/components/Icons.tsx +9 -0
- package/src/viewer/components/InteractionsPanel.tsx +101 -117
- package/src/viewer/components/IsolatedPreviewFrame.tsx +1 -0
- package/src/viewer/components/LandingPage.tsx +3 -3
- package/src/viewer/components/LeftSidebar.tsx +141 -63
- package/src/viewer/components/LoadErrorMessage.tsx +102 -0
- package/src/viewer/components/MultiViewportPreview.tsx +61 -142
- package/src/viewer/components/NoVariantsMessage.tsx +59 -0
- package/src/viewer/components/PanelShell.tsx +161 -0
- package/src/viewer/components/PerformancePanel.tsx +31 -28
- package/src/viewer/components/PreviewArea.tsx +1 -1
- package/src/viewer/components/PreviewAside.tsx +168 -0
- package/src/viewer/components/PreviewFrameHost.tsx +3 -3
- package/src/viewer/components/PropsEditor.tsx +70 -156
- package/src/viewer/components/ResizablePanel.tsx +103 -263
- package/src/viewer/components/RightSidebar.tsx +3 -9
- package/src/viewer/components/SkeletonLoader.tsx +13 -13
- package/src/viewer/components/TokenStylePanel.tsx +182 -209
- package/src/viewer/components/TopToolbar.tsx +159 -0
- package/src/viewer/components/VariantMatrix.tsx +42 -86
- package/src/viewer/components/VariantTabs.tsx +3 -3
- package/src/viewer/components/ViewerHeader.tsx +69 -0
- package/src/viewer/components/WebMCPDevTools.tsx +17 -23
- package/src/viewer/components/viewer-utils.ts +16 -0
- package/src/viewer/entry.tsx +5 -0
- package/src/viewer/hooks/useAppState.ts +27 -4
- package/src/viewer/hooks/usePreviewBridge.ts +2 -2
- package/src/viewer/preview-frame.html +6 -12
- package/src/viewer/server.ts +184 -6
- package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +10 -0
- package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +2 -0
- package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +274 -0
- package/src/viewer/vendor/shared/src/DocsPageShell.tsx +5 -0
- package/src/viewer/vendor/shared/src/PropsTable.module.scss +68 -0
- package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +2 -0
- package/src/viewer/vendor/shared/src/PropsTable.tsx +76 -0
- package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +122 -0
- package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +2 -0
- package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +134 -0
- package/src/viewer/vendor/shared/src/docs-data/index.ts +32 -0
- package/src/viewer/vendor/shared/src/docs-data/mcp-configs.ts +72 -0
- package/src/viewer/vendor/shared/src/docs-data/palettes.ts +75 -0
- package/src/viewer/vendor/shared/src/docs-data/setup-examples.ts +55 -0
- package/src/viewer/vendor/shared/src/index.ts +8 -0
- package/src/viewer/vendor/shared/src/types.ts +12 -0
- package/src/viewer/vite-plugin.ts +109 -4
- package/dist/chunk-2JIKCJX3.js.map +0 -1
- package/dist/chunk-CJEGT3WD.js.map +0 -1
- package/dist/chunk-GOVI6COW.js.map +0 -1
- package/dist/generate-35OIMW4Y.js +0 -252
- package/dist/generate-35OIMW4Y.js.map +0 -1
- package/dist/init-KFYN37ZY.js.map +0 -1
- package/dist/viewer-HZK4BSDK.js.map +0 -1
- /package/dist/{chunk-WI6SLMSO.js.map → chunk-5GT62FCB.js.map} +0 -0
- /package/dist/{chunk-NGIMCIK2.js.map → chunk-GF6OVPIN.js.map} +0 -0
- /package/dist/{scan-65RH3QMM.js.map → scan-JGS65S7P.js.map} +0 -0
- /package/dist/{service-A5GIGGGK.js.map → service-XP2EAJXD.js.map} +0 -0
- /package/dist/{static-viewer-NSODM5VX.js.map → static-viewer-XCS7UJTO.js.map} +0 -0
- /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
|
+
}
|