@fragments-sdk/cli 0.8.1 → 0.9.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.
- package/dist/bin.js +436 -46
- package/dist/bin.js.map +1 -1
- package/dist/{init-KFYN37ZY.js → init-KSAAS7X3.js} +2 -2
- package/dist/{init-KFYN37ZY.js.map → init-KSAAS7X3.js.map} +1 -1
- package/dist/{viewer-HZK4BSDK.js → viewer-SBTJDMP7.js} +7 -5
- package/dist/viewer-SBTJDMP7.js.map +1 -0
- package/package.json +2 -2
- package/src/bin.ts +26 -0
- package/src/commands/doctor.ts +498 -0
- package/src/commands/init-framework.ts +1 -1
- package/src/viewer/server.ts +15 -4
- package/src/viewer/vendor/shared/src/DocsHeaderBar.tsx +18 -6
- package/src/viewer/vendor/shared/src/DocsSidebarNav.tsx +16 -5
- 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/dist/viewer-HZK4BSDK.js.map +0 -1
|
@@ -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
|
+
}
|
|
@@ -126,7 +126,7 @@ import { ThemeProvider } from '@fragments-sdk/ui';
|
|
|
126
126
|
|
|
127
127
|
export function Providers({ children }: { children: React.ReactNode }) {
|
|
128
128
|
return (
|
|
129
|
-
<ThemeProvider
|
|
129
|
+
<ThemeProvider defaultMode="system" attribute="data-theme">
|
|
130
130
|
{children}
|
|
131
131
|
</ThemeProvider>
|
|
132
132
|
);
|
package/src/viewer/server.ts
CHANGED
|
@@ -253,6 +253,9 @@ export async function createDevServer(
|
|
|
253
253
|
const sharedLibRoot = resolveSharedLib();
|
|
254
254
|
const webmcpLibRoot = resolveWebMCPLib(nodeModulesPath);
|
|
255
255
|
const contextLibRoot = resolveContextLib(nodeModulesPath);
|
|
256
|
+
// Detect whether resolved libs point to source (.ts) or dist (.js)
|
|
257
|
+
const isWebMCPSource = existsSync(join(webmcpLibRoot, "react/index.ts"));
|
|
258
|
+
const isContextSource = existsSync(join(contextLibRoot, "types/index.ts"));
|
|
256
259
|
console.log(`📁 Using node_modules: ${nodeModulesPath}`);
|
|
257
260
|
|
|
258
261
|
// Collect installed package roots so Vite can serve files from node_modules
|
|
@@ -322,12 +325,20 @@ export async function createDevServer(
|
|
|
322
325
|
// Resolve @fragments-sdk/shared to monorepo source or vendored fallback
|
|
323
326
|
"@fragments-sdk/shared": sharedLibRoot,
|
|
324
327
|
// Resolve @fragments-sdk/webmcp subpaths to monorepo source or installed package
|
|
325
|
-
"@fragments-sdk/webmcp/react":
|
|
326
|
-
|
|
328
|
+
"@fragments-sdk/webmcp/react": isWebMCPSource
|
|
329
|
+
? join(webmcpLibRoot, "react/index.ts")
|
|
330
|
+
: join(webmcpLibRoot, "dist/react/index.js"),
|
|
331
|
+
"@fragments-sdk/webmcp/fragments": isWebMCPSource
|
|
332
|
+
? join(webmcpLibRoot, "fragments/index.ts")
|
|
333
|
+
: join(webmcpLibRoot, "dist/fragments/index.js"),
|
|
327
334
|
"@fragments-sdk/webmcp": webmcpLibRoot,
|
|
328
335
|
// Resolve @fragments-sdk/context subpaths to monorepo source or installed package
|
|
329
|
-
"@fragments-sdk/context/types":
|
|
330
|
-
|
|
336
|
+
"@fragments-sdk/context/types": isContextSource
|
|
337
|
+
? join(contextLibRoot, "types/index.ts")
|
|
338
|
+
: join(contextLibRoot, "dist/types/index.js"),
|
|
339
|
+
"@fragments-sdk/context/mcp-tools": isContextSource
|
|
340
|
+
? join(contextLibRoot, "mcp-tools/index.ts")
|
|
341
|
+
: join(contextLibRoot, "dist/mcp-tools/index.js"),
|
|
331
342
|
"@fragments-sdk/context": contextLibRoot,
|
|
332
343
|
// Resolve @fragments-sdk/cli/core to the CLI's own core source
|
|
333
344
|
"@fragments-sdk/cli/core": resolve(cliPackageRoot, "src/core/index.ts"),
|
|
@@ -20,6 +20,8 @@ interface DocsHeaderBarProps {
|
|
|
20
20
|
navAriaLabel?: string;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
const EMPTY_SECTIONS: NavSection[] = [];
|
|
24
|
+
|
|
23
25
|
const defaultLinkRenderer: DocsNavLinkRenderer = ({ href, label, onClick }) => (
|
|
24
26
|
<a href={href} onClick={onClick}>
|
|
25
27
|
{label}
|
|
@@ -30,10 +32,20 @@ function defaultIsActive(href: string, currentPath: string): boolean {
|
|
|
30
32
|
return currentPath === href || currentPath.startsWith(`${href}/`);
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
/** Named component wrapper to avoid inline render functions in JSX */
|
|
36
|
+
function NavLink({ renderer, href, label, onClick }: {
|
|
37
|
+
renderer: DocsNavLinkRenderer;
|
|
38
|
+
href: string;
|
|
39
|
+
label: string;
|
|
40
|
+
onClick?: () => void;
|
|
41
|
+
}) {
|
|
42
|
+
return <>{renderer({ href, label, onClick })}</>;
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
export function DocsHeaderBar({
|
|
34
46
|
brand,
|
|
35
47
|
headerNav,
|
|
36
|
-
mobileSections =
|
|
48
|
+
mobileSections = EMPTY_SECTIONS,
|
|
37
49
|
currentPath,
|
|
38
50
|
searchItems,
|
|
39
51
|
onSearchSelect,
|
|
@@ -63,7 +75,7 @@ export function DocsHeaderBar({
|
|
|
63
75
|
active={isActive(child.href, currentPath)}
|
|
64
76
|
asChild
|
|
65
77
|
>
|
|
66
|
-
{renderLink
|
|
78
|
+
<NavLink renderer={renderLink} href={child.href} label={child.label} />
|
|
67
79
|
</NavigationMenu.Link>
|
|
68
80
|
))}
|
|
69
81
|
</div>
|
|
@@ -76,7 +88,7 @@ export function DocsHeaderBar({
|
|
|
76
88
|
active={isActive(entry.href, currentPath)}
|
|
77
89
|
asChild
|
|
78
90
|
>
|
|
79
|
-
{renderLink
|
|
91
|
+
<NavLink renderer={renderLink} href={entry.href} label={entry.label} />
|
|
80
92
|
</NavigationMenu.Link>
|
|
81
93
|
</NavigationMenu.Item>
|
|
82
94
|
)
|
|
@@ -94,12 +106,12 @@ export function DocsHeaderBar({
|
|
|
94
106
|
isDropdown(entry) ? (
|
|
95
107
|
entry.items.map((child) => (
|
|
96
108
|
<NavigationMenu.Link key={child.href} href={child.href} asChild>
|
|
97
|
-
{renderLink
|
|
109
|
+
<NavLink renderer={renderLink} href={child.href} label={child.label} />
|
|
98
110
|
</NavigationMenu.Link>
|
|
99
111
|
))
|
|
100
112
|
) : (
|
|
101
113
|
<NavigationMenu.Link key={entry.href} href={entry.href} asChild>
|
|
102
|
-
{renderLink
|
|
114
|
+
<NavLink renderer={renderLink} href={entry.href} label={entry.label} />
|
|
103
115
|
</NavigationMenu.Link>
|
|
104
116
|
)
|
|
105
117
|
)}
|
|
@@ -109,7 +121,7 @@ export function DocsHeaderBar({
|
|
|
109
121
|
<NavigationMenu.MobileSection key={section.title} label={section.title}>
|
|
110
122
|
{section.items.map((item) => (
|
|
111
123
|
<NavigationMenu.Link key={item.href} href={item.href} asChild>
|
|
112
|
-
{renderLink
|
|
124
|
+
<NavLink renderer={renderLink} href={item.href} label={item.label} />
|
|
113
125
|
</NavigationMenu.Link>
|
|
114
126
|
))}
|
|
115
127
|
</NavigationMenu.MobileSection>
|
|
@@ -24,6 +24,16 @@ const defaultLinkRenderer: DocsNavLinkRenderer = ({ href, label, onClick }) => (
|
|
|
24
24
|
</a>
|
|
25
25
|
);
|
|
26
26
|
|
|
27
|
+
/** Named component wrapper to avoid inline render functions in JSX */
|
|
28
|
+
function SidebarLink({ renderer, href, label, onClick }: {
|
|
29
|
+
renderer: DocsNavLinkRenderer;
|
|
30
|
+
href: string;
|
|
31
|
+
label: string;
|
|
32
|
+
onClick?: () => void;
|
|
33
|
+
}) {
|
|
34
|
+
return <>{renderer({ href, label, onClick })}</>;
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
export function DocsSidebarNav({
|
|
28
38
|
sections,
|
|
29
39
|
currentPath,
|
|
@@ -49,11 +59,12 @@ export function DocsSidebarNav({
|
|
|
49
59
|
active={isActive(item.href, currentPath)}
|
|
50
60
|
asChild
|
|
51
61
|
>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
<SidebarLink
|
|
63
|
+
renderer={renderLink}
|
|
64
|
+
href={item.href}
|
|
65
|
+
label={item.label}
|
|
66
|
+
onClick={onNavigate}
|
|
67
|
+
/>
|
|
57
68
|
</Sidebar.Item>
|
|
58
69
|
))}
|
|
59
70
|
</Sidebar.Section>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docs data — single source of truth for content shared across
|
|
3
|
+
* READMEs, docs pages, and MCP tool descriptions.
|
|
4
|
+
*
|
|
5
|
+
* Follows the same pattern as `cli-commands/index.ts`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
NEUTRAL_PALETTES,
|
|
10
|
+
DENSITY_PRESETS,
|
|
11
|
+
RADIUS_STYLES,
|
|
12
|
+
SEED_DEFAULTS,
|
|
13
|
+
type PaletteDef,
|
|
14
|
+
type DensityDef,
|
|
15
|
+
type RadiusDef,
|
|
16
|
+
} from './palettes.js';
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
SCSS_SETUP_MINIMAL,
|
|
20
|
+
SCSS_SETUP_FULL,
|
|
21
|
+
USE_THEME_EXAMPLE,
|
|
22
|
+
USE_THEME_RETURN_SHAPE,
|
|
23
|
+
PROVIDER_SETUP_EXAMPLE,
|
|
24
|
+
MIXIN_IMPORT,
|
|
25
|
+
STYLES_IMPORT_JS,
|
|
26
|
+
STYLES_IMPORT_SCSS,
|
|
27
|
+
} from './setup-examples.js';
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
MCP_CONFIGS,
|
|
31
|
+
type McpConfigDef,
|
|
32
|
+
} from './mcp-configs.js';
|