@fragments-sdk/cli 0.15.1 → 0.15.3

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.
@@ -0,0 +1,385 @@
1
+ import { createRequire as __banner_createRequire } from 'module'; const require = __banner_createRequire(import.meta.url);
2
+ import "./chunk-D2CDBRNU.js";
3
+ import {
4
+ BRAND
5
+ } from "./chunk-32LIWN2P.js";
6
+
7
+ // src/commands/doctor.ts
8
+ import { readFile, access } from "fs/promises";
9
+ import { join, resolve } from "path";
10
+ import pc from "picocolors";
11
+ import { NEUTRAL_PALETTES } from "@fragments-sdk/viewer/docs-data";
12
+ var VALID_NEUTRALS = NEUTRAL_PALETTES.map((p) => p.name);
13
+ var VALID_DENSITIES = ["compact", "default", "relaxed"];
14
+ var VALID_RADII = ["sharp", "subtle", "default", "rounded", "pill"];
15
+ async function checkPackageInstalled(root) {
16
+ try {
17
+ const pkgPath = join(root, "package.json");
18
+ const content = await readFile(pkgPath, "utf-8");
19
+ const pkg = JSON.parse(content);
20
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
21
+ if (allDeps["@fragments-sdk/ui"]) {
22
+ return {
23
+ name: "Package installed",
24
+ status: "pass",
25
+ message: `@fragments-sdk/ui ${allDeps["@fragments-sdk/ui"]} found in dependencies`
26
+ };
27
+ }
28
+ return {
29
+ name: "Package installed",
30
+ status: "fail",
31
+ message: "@fragments-sdk/ui not found in package.json",
32
+ fix: "npm install @fragments-sdk/ui"
33
+ };
34
+ } catch {
35
+ return {
36
+ name: "Package installed",
37
+ status: "fail",
38
+ message: "No package.json found"
39
+ };
40
+ }
41
+ }
42
+ async function checkStylesImport(root) {
43
+ const entryPatterns = [
44
+ "src/main.tsx",
45
+ "src/main.ts",
46
+ "src/index.tsx",
47
+ "src/index.ts",
48
+ "src/App.tsx",
49
+ "src/App.ts",
50
+ "app/layout.tsx",
51
+ "app/layout.ts",
52
+ "src/app/layout.tsx",
53
+ "src/app/layout.ts",
54
+ "app/root.tsx",
55
+ "pages/_app.tsx",
56
+ "pages/_app.ts"
57
+ ];
58
+ for (const pattern of entryPatterns) {
59
+ try {
60
+ const content = await readFile(join(root, pattern), "utf-8");
61
+ if (content.includes("@fragments-sdk/ui/styles")) {
62
+ return {
63
+ name: "Styles import",
64
+ status: "pass",
65
+ message: `Found styles import in ${pattern}`
66
+ };
67
+ }
68
+ if (content.includes("@fragments-sdk/ui/globals")) {
69
+ return {
70
+ name: "Styles import",
71
+ status: "warn",
72
+ message: `${pattern} uses deprecated '@fragments-sdk/ui/globals'. Use '@fragments-sdk/ui/styles' instead`,
73
+ fix: `Replace '@fragments-sdk/ui/globals' with '@fragments-sdk/ui/styles' in ${pattern}`
74
+ };
75
+ }
76
+ } catch {
77
+ }
78
+ }
79
+ const scssPatterns = [
80
+ "src/styles/globals.scss",
81
+ "src/globals.scss",
82
+ "styles/globals.scss",
83
+ "app/globals.scss",
84
+ "src/app/globals.scss",
85
+ "app/styles/globals.scss"
86
+ ];
87
+ for (const pattern of scssPatterns) {
88
+ try {
89
+ const content = await readFile(join(root, pattern), "utf-8");
90
+ if (content.includes("@fragments-sdk/ui/styles")) {
91
+ return {
92
+ name: "Styles import",
93
+ status: "pass",
94
+ message: `Found SCSS @use import in ${pattern}`
95
+ };
96
+ }
97
+ } catch {
98
+ }
99
+ }
100
+ return {
101
+ name: "Styles import",
102
+ status: "fail",
103
+ message: "No @fragments-sdk/ui/styles import found in entry files",
104
+ fix: "Add `import '@fragments-sdk/ui/styles'` to your app's entry file"
105
+ };
106
+ }
107
+ async function checkThemeProvider(root) {
108
+ const providerPatterns = [
109
+ "src/main.tsx",
110
+ "src/App.tsx",
111
+ "src/providers.tsx",
112
+ "app/layout.tsx",
113
+ "app/providers.tsx",
114
+ "src/app/layout.tsx",
115
+ "src/app/providers.tsx",
116
+ "app/root.tsx",
117
+ "pages/_app.tsx"
118
+ ];
119
+ for (const pattern of providerPatterns) {
120
+ try {
121
+ const content = await readFile(join(root, pattern), "utf-8");
122
+ if (content.includes("ThemeProvider")) {
123
+ if (content.includes("defaultTheme=") || content.includes("defaultTheme =")) {
124
+ return {
125
+ name: "ThemeProvider",
126
+ status: "warn",
127
+ message: `${pattern} uses deprecated 'defaultTheme' prop. Use 'defaultMode' instead`,
128
+ fix: `Replace 'defaultTheme' with 'defaultMode' in ${pattern}`
129
+ };
130
+ }
131
+ return {
132
+ name: "ThemeProvider",
133
+ status: "pass",
134
+ message: `ThemeProvider found in ${pattern}`
135
+ };
136
+ }
137
+ } catch {
138
+ }
139
+ }
140
+ return {
141
+ name: "ThemeProvider",
142
+ status: "warn",
143
+ message: "ThemeProvider not found in common entry files (optional but recommended)",
144
+ fix: 'Wrap your app with <ThemeProvider defaultMode="system">'
145
+ };
146
+ }
147
+ async function checkScssSeeds(root) {
148
+ const checks = [];
149
+ const scssPatterns = [
150
+ "src/styles/globals.scss",
151
+ "src/globals.scss",
152
+ "styles/globals.scss",
153
+ "app/globals.scss",
154
+ "src/app/globals.scss",
155
+ "app/styles/globals.scss"
156
+ ];
157
+ for (const pattern of scssPatterns) {
158
+ try {
159
+ const content = await readFile(join(root, pattern), "utf-8");
160
+ if (!content.includes("@fragments-sdk/ui/styles")) continue;
161
+ const standalonePattern = /^\$fui-\w+:\s*.+;$/m;
162
+ if (standalonePattern.test(content) && !content.includes("@use")) {
163
+ checks.push({
164
+ name: "SCSS syntax",
165
+ status: "fail",
166
+ message: `${pattern} uses standalone $fui- variables. Must use @use...with() syntax`,
167
+ fix: "@use '@fragments-sdk/ui/styles' with ($fui-brand: #0066ff);"
168
+ });
169
+ }
170
+ const neutralMatch = content.match(/\$fui-neutral:\s*"([^"]+)"/);
171
+ if (neutralMatch && !VALID_NEUTRALS.includes(neutralMatch[1])) {
172
+ checks.push({
173
+ name: "SCSS seed: neutral",
174
+ status: "fail",
175
+ message: `Invalid $fui-neutral: "${neutralMatch[1]}" in ${pattern}`,
176
+ fix: `Valid neutrals: ${VALID_NEUTRALS.join(", ")}`
177
+ });
178
+ }
179
+ const densityMatch = content.match(/\$fui-density:\s*"([^"]+)"/);
180
+ if (densityMatch && !VALID_DENSITIES.includes(densityMatch[1])) {
181
+ checks.push({
182
+ name: "SCSS seed: density",
183
+ status: "fail",
184
+ message: `Invalid $fui-density: "${densityMatch[1]}" in ${pattern}`,
185
+ fix: `Valid densities: ${VALID_DENSITIES.join(", ")}`
186
+ });
187
+ }
188
+ const radiusMatch = content.match(/\$fui-radius-style:\s*"([^"]+)"/);
189
+ if (radiusMatch && !VALID_RADII.includes(radiusMatch[1])) {
190
+ checks.push({
191
+ name: "SCSS seed: radius-style",
192
+ status: "fail",
193
+ message: `Invalid $fui-radius-style: "${radiusMatch[1]}" in ${pattern}`,
194
+ fix: `Valid radius styles: ${VALID_RADII.join(", ")}`
195
+ });
196
+ }
197
+ const brandMatch = content.match(/\$fui-brand:\s*(#[0-9a-fA-F]+)/);
198
+ if (brandMatch) {
199
+ const hex = brandMatch[1];
200
+ if (!/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(hex)) {
201
+ checks.push({
202
+ name: "SCSS seed: brand",
203
+ status: "fail",
204
+ message: `Invalid $fui-brand color: "${hex}" in ${pattern}`,
205
+ fix: "Must be a valid hex color (e.g., #0066ff)"
206
+ });
207
+ }
208
+ }
209
+ if (checks.length === 0) {
210
+ checks.push({
211
+ name: "SCSS seeds",
212
+ status: "pass",
213
+ message: `Seed values in ${pattern} are valid`
214
+ });
215
+ }
216
+ return checks;
217
+ } catch {
218
+ }
219
+ }
220
+ checks.push({
221
+ name: "SCSS seeds",
222
+ status: "pass",
223
+ message: "No custom SCSS seeds configured (using defaults)"
224
+ });
225
+ return checks;
226
+ }
227
+ async function checkPeerDeps(root) {
228
+ const checks = [];
229
+ try {
230
+ const pkgPath = join(root, "package.json");
231
+ const content = await readFile(pkgPath, "utf-8");
232
+ const pkg = JSON.parse(content);
233
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
234
+ if (!allDeps["react"]) {
235
+ checks.push({
236
+ name: "Peer dep: react",
237
+ status: "fail",
238
+ message: "react not found in dependencies (required)",
239
+ fix: "npm install react react-dom"
240
+ });
241
+ } else {
242
+ checks.push({
243
+ name: "Peer dep: react",
244
+ status: "pass",
245
+ message: `react ${allDeps["react"]} installed`
246
+ });
247
+ }
248
+ if (!allDeps["sass"]) {
249
+ checks.push({
250
+ name: "Peer dep: sass",
251
+ status: "warn",
252
+ message: "sass not installed (needed for custom SCSS theming)",
253
+ fix: "npm install -D sass"
254
+ });
255
+ }
256
+ const optionalPeers = [
257
+ { pkg: "recharts", components: "Chart" },
258
+ { pkg: "shiki", components: "CodeBlock" },
259
+ { pkg: "react-day-picker", components: "DatePicker" },
260
+ { pkg: "@tanstack/react-table", components: "DataTable" }
261
+ ];
262
+ for (const peer of optionalPeers) {
263
+ if (allDeps[peer.pkg]) {
264
+ checks.push({
265
+ name: `Optional dep: ${peer.pkg}`,
266
+ status: "pass",
267
+ message: `${peer.pkg} installed (enables ${peer.components})`
268
+ });
269
+ }
270
+ }
271
+ } catch {
272
+ checks.push({
273
+ name: "Peer dependencies",
274
+ status: "fail",
275
+ message: "Could not read package.json"
276
+ });
277
+ }
278
+ return checks;
279
+ }
280
+ async function checkMcpConfig(root) {
281
+ const mcpConfigPaths = [
282
+ ".mcp.json",
283
+ ".cursor/mcp.json",
284
+ ".vscode/mcp.json"
285
+ ];
286
+ for (const configPath of mcpConfigPaths) {
287
+ try {
288
+ const fullPath = join(root, configPath);
289
+ const content = await readFile(fullPath, "utf-8");
290
+ const config = JSON.parse(content);
291
+ const servers = config.mcpServers || config.servers || {};
292
+ const hasFragments = Object.values(servers).some((server) => {
293
+ const s = server;
294
+ return s.args?.some((arg) => arg.includes("@fragments-sdk/mcp")) || s.command?.includes("fragments");
295
+ });
296
+ if (hasFragments) {
297
+ return {
298
+ name: "MCP configuration",
299
+ status: "pass",
300
+ message: `Fragments MCP server configured in ${configPath}`
301
+ };
302
+ }
303
+ } catch {
304
+ }
305
+ }
306
+ return {
307
+ name: "MCP configuration",
308
+ status: "warn",
309
+ message: "No Fragments MCP server configuration found (optional)",
310
+ fix: "Run `fragments init` or add @fragments-sdk/mcp to your MCP config"
311
+ };
312
+ }
313
+ async function checkTypeScript(root) {
314
+ try {
315
+ const tsconfigPath = join(root, "tsconfig.json");
316
+ await access(tsconfigPath);
317
+ return {
318
+ name: "TypeScript",
319
+ status: "pass",
320
+ message: "tsconfig.json found"
321
+ };
322
+ } catch {
323
+ return {
324
+ name: "TypeScript",
325
+ status: "warn",
326
+ message: "No tsconfig.json found (TypeScript recommended but not required)"
327
+ };
328
+ }
329
+ }
330
+ async function doctor(options = {}) {
331
+ const root = resolve(options.root ?? process.cwd());
332
+ const checks = [];
333
+ if (!options.json) {
334
+ console.log(pc.cyan(`
335
+ ${BRAND.name} Doctor
336
+ `));
337
+ console.log(pc.dim(`Checking project at ${root}
338
+ `));
339
+ }
340
+ checks.push(await checkPackageInstalled(root));
341
+ checks.push(await checkStylesImport(root));
342
+ checks.push(await checkThemeProvider(root));
343
+ checks.push(...await checkScssSeeds(root));
344
+ checks.push(...await checkPeerDeps(root));
345
+ checks.push(await checkMcpConfig(root));
346
+ checks.push(await checkTypeScript(root));
347
+ const passed = checks.filter((c) => c.status === "pass").length;
348
+ const warned = checks.filter((c) => c.status === "warn").length;
349
+ const failed = checks.filter((c) => c.status === "fail").length;
350
+ const result = {
351
+ success: failed === 0,
352
+ checks,
353
+ passed,
354
+ warned,
355
+ failed
356
+ };
357
+ if (options.json) {
358
+ console.log(JSON.stringify(result, null, 2));
359
+ } else {
360
+ for (const check of checks) {
361
+ const icon = check.status === "pass" ? pc.green("\u2713") : check.status === "warn" ? pc.yellow("!") : pc.red("\u2717");
362
+ const msg = check.status === "pass" ? check.message : check.status === "warn" ? pc.yellow(check.message) : pc.red(check.message);
363
+ console.log(` ${icon} ${pc.bold(check.name)}: ${msg}`);
364
+ if (check.fix && check.status !== "pass") {
365
+ console.log(pc.dim(` \u2192 ${check.fix}`));
366
+ }
367
+ }
368
+ console.log();
369
+ if (failed === 0 && warned === 0) {
370
+ console.log(pc.green(`\u2713 All ${passed} checks passed \u2014 your setup looks great!`));
371
+ } else if (failed === 0) {
372
+ console.log(pc.green(`\u2713 ${passed} passed`) + pc.yellow(`, ${warned} warning(s)`));
373
+ } else {
374
+ console.log(
375
+ pc.red(`\u2717 ${failed} failed`) + (warned > 0 ? pc.yellow(`, ${warned} warning(s)`) : "") + pc.dim(`, ${passed} passed`)
376
+ );
377
+ }
378
+ console.log();
379
+ }
380
+ return result;
381
+ }
382
+ export {
383
+ doctor
384
+ };
385
+ //# sourceMappingURL=doctor-BDPMYYE6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/doctor.ts"],"sourcesContent":["/**\n * fragments doctor - Diagnose design system configuration\n *\n * Checks a consumer project for common setup issues:\n * - Missing styles import\n * - ThemeProvider not found\n * - Invalid SCSS seed values\n * - Missing peer dependencies\n * - MCP configuration\n */\n\nimport { readFile, access } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\nimport pc from 'picocolors';\nimport { BRAND } from '../core/index.js';\nimport { NEUTRAL_PALETTES } from '@fragments-sdk/viewer/docs-data';\n\n// ============================================\n// Types\n// ============================================\n\nexport interface DoctorOptions {\n /** Project root directory (defaults to cwd) */\n root?: string;\n /** Output JSON instead of formatted text */\n json?: boolean;\n /** Auto-fix issues where possible */\n fix?: boolean;\n}\n\nexport interface DoctorCheck {\n name: string;\n status: 'pass' | 'warn' | 'fail';\n message: string;\n fix?: string;\n}\n\nexport interface DoctorResult {\n success: boolean;\n checks: DoctorCheck[];\n passed: number;\n warned: number;\n failed: number;\n}\n\n// ============================================\n// Valid seed values — derived from canonical sources\n// ============================================\n\nconst VALID_NEUTRALS = NEUTRAL_PALETTES.map((p) => p.name);\nconst VALID_DENSITIES = ['compact', 'default', 'relaxed'];\nconst VALID_RADII = ['sharp', 'subtle', 'default', 'rounded', 'pill'];\n\n// ============================================\n// Individual Checks\n// ============================================\n\nasync function checkPackageInstalled(root: string): Promise<DoctorCheck> {\n try {\n const pkgPath = join(root, 'package.json');\n const content = await readFile(pkgPath, 'utf-8');\n const pkg = JSON.parse(content);\n const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };\n\n if (allDeps['@fragments-sdk/ui']) {\n return {\n name: 'Package installed',\n status: 'pass',\n message: `@fragments-sdk/ui ${allDeps['@fragments-sdk/ui']} found in dependencies`,\n };\n }\n\n return {\n name: 'Package installed',\n status: 'fail',\n message: '@fragments-sdk/ui not found in package.json',\n fix: 'npm install @fragments-sdk/ui',\n };\n } catch {\n return {\n name: 'Package installed',\n status: 'fail',\n message: 'No package.json found',\n };\n }\n}\n\nasync function checkStylesImport(root: string): Promise<DoctorCheck> {\n const entryPatterns = [\n 'src/main.tsx', 'src/main.ts', 'src/index.tsx', 'src/index.ts',\n 'src/App.tsx', 'src/App.ts',\n 'app/layout.tsx', 'app/layout.ts',\n 'src/app/layout.tsx', 'src/app/layout.ts',\n 'app/root.tsx',\n 'pages/_app.tsx', 'pages/_app.ts',\n ];\n\n for (const pattern of entryPatterns) {\n try {\n const content = await readFile(join(root, pattern), 'utf-8');\n\n // Check for correct import\n if (content.includes(\"@fragments-sdk/ui/styles\")) {\n return {\n name: 'Styles import',\n status: 'pass',\n message: `Found styles import in ${pattern}`,\n };\n }\n\n // Check for deprecated ./globals import\n if (content.includes(\"@fragments-sdk/ui/globals\")) {\n return {\n name: 'Styles import',\n status: 'warn',\n message: `${pattern} uses deprecated '@fragments-sdk/ui/globals'. Use '@fragments-sdk/ui/styles' instead`,\n fix: `Replace '@fragments-sdk/ui/globals' with '@fragments-sdk/ui/styles' in ${pattern}`,\n };\n }\n } catch {\n // File doesn't exist, continue\n }\n }\n\n // Also check SCSS files for @use import\n const scssPatterns = [\n 'src/styles/globals.scss', 'src/globals.scss',\n 'styles/globals.scss', 'app/globals.scss',\n 'src/app/globals.scss',\n 'app/styles/globals.scss',\n ];\n\n for (const pattern of scssPatterns) {\n try {\n const content = await readFile(join(root, pattern), 'utf-8');\n if (content.includes(\"@fragments-sdk/ui/styles\")) {\n return {\n name: 'Styles import',\n status: 'pass',\n message: `Found SCSS @use import in ${pattern}`,\n };\n }\n } catch {\n // File doesn't exist, continue\n }\n }\n\n return {\n name: 'Styles import',\n status: 'fail',\n message: 'No @fragments-sdk/ui/styles import found in entry files',\n fix: \"Add `import '@fragments-sdk/ui/styles'` to your app's entry file\",\n };\n}\n\nasync function checkThemeProvider(root: string): Promise<DoctorCheck> {\n const providerPatterns = [\n 'src/main.tsx', 'src/App.tsx', 'src/providers.tsx',\n 'app/layout.tsx', 'app/providers.tsx',\n 'src/app/layout.tsx', 'src/app/providers.tsx',\n 'app/root.tsx',\n 'pages/_app.tsx',\n ];\n\n for (const pattern of providerPatterns) {\n try {\n const content = await readFile(join(root, pattern), 'utf-8');\n\n if (content.includes('ThemeProvider')) {\n // Check for deprecated defaultTheme prop\n if (content.includes('defaultTheme=') || content.includes('defaultTheme =')) {\n return {\n name: 'ThemeProvider',\n status: 'warn',\n message: `${pattern} uses deprecated 'defaultTheme' prop. Use 'defaultMode' instead`,\n fix: `Replace 'defaultTheme' with 'defaultMode' in ${pattern}`,\n };\n }\n\n return {\n name: 'ThemeProvider',\n status: 'pass',\n message: `ThemeProvider found in ${pattern}`,\n };\n }\n } catch {\n // File doesn't exist, continue\n }\n }\n\n return {\n name: 'ThemeProvider',\n status: 'warn',\n message: 'ThemeProvider not found in common entry files (optional but recommended)',\n fix: \"Wrap your app with <ThemeProvider defaultMode=\\\"system\\\">\",\n };\n}\n\nasync function checkScssSeeds(root: string): Promise<DoctorCheck[]> {\n const checks: DoctorCheck[] = [];\n\n const scssPatterns = [\n 'src/styles/globals.scss', 'src/globals.scss',\n 'styles/globals.scss', 'app/globals.scss',\n 'src/app/globals.scss',\n 'app/styles/globals.scss',\n ];\n\n for (const pattern of scssPatterns) {\n try {\n const content = await readFile(join(root, pattern), 'utf-8');\n if (!content.includes(\"@fragments-sdk/ui/styles\")) continue;\n\n // Check for standalone variable syntax (wrong)\n const standalonePattern = /^\\$fui-\\w+:\\s*.+;$/m;\n if (standalonePattern.test(content) && !content.includes('@use')) {\n checks.push({\n name: 'SCSS syntax',\n status: 'fail',\n message: `${pattern} uses standalone $fui- variables. Must use @use...with() syntax`,\n fix: \"@use '@fragments-sdk/ui/styles' with ($fui-brand: #0066ff);\",\n });\n }\n\n // Validate neutral value\n const neutralMatch = content.match(/\\$fui-neutral:\\s*\"([^\"]+)\"/);\n if (neutralMatch && !VALID_NEUTRALS.includes(neutralMatch[1])) {\n checks.push({\n name: 'SCSS seed: neutral',\n status: 'fail',\n message: `Invalid $fui-neutral: \"${neutralMatch[1]}\" in ${pattern}`,\n fix: `Valid neutrals: ${VALID_NEUTRALS.join(', ')}`,\n });\n }\n\n // Validate density value\n const densityMatch = content.match(/\\$fui-density:\\s*\"([^\"]+)\"/);\n if (densityMatch && !VALID_DENSITIES.includes(densityMatch[1])) {\n checks.push({\n name: 'SCSS seed: density',\n status: 'fail',\n message: `Invalid $fui-density: \"${densityMatch[1]}\" in ${pattern}`,\n fix: `Valid densities: ${VALID_DENSITIES.join(', ')}`,\n });\n }\n\n // Validate radius-style value\n const radiusMatch = content.match(/\\$fui-radius-style:\\s*\"([^\"]+)\"/);\n if (radiusMatch && !VALID_RADII.includes(radiusMatch[1])) {\n checks.push({\n name: 'SCSS seed: radius-style',\n status: 'fail',\n message: `Invalid $fui-radius-style: \"${radiusMatch[1]}\" in ${pattern}`,\n fix: `Valid radius styles: ${VALID_RADII.join(', ')}`,\n });\n }\n\n // Validate brand is a valid hex color\n const brandMatch = content.match(/\\$fui-brand:\\s*(#[0-9a-fA-F]+)/);\n if (brandMatch) {\n const hex = brandMatch[1];\n if (!/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(hex)) {\n checks.push({\n name: 'SCSS seed: brand',\n status: 'fail',\n message: `Invalid $fui-brand color: \"${hex}\" in ${pattern}`,\n fix: 'Must be a valid hex color (e.g., #0066ff)',\n });\n }\n }\n\n // If we found the file and parsed it, and no seed issues, add a pass\n if (checks.length === 0) {\n checks.push({\n name: 'SCSS seeds',\n status: 'pass',\n message: `Seed values in ${pattern} are valid`,\n });\n }\n\n return checks;\n } catch {\n // File doesn't exist, continue\n }\n }\n\n // No SCSS file found — not an error, just informational\n checks.push({\n name: 'SCSS seeds',\n status: 'pass',\n message: 'No custom SCSS seeds configured (using defaults)',\n });\n\n return checks;\n}\n\nasync function checkPeerDeps(root: string): Promise<DoctorCheck[]> {\n const checks: DoctorCheck[] = [];\n\n try {\n const pkgPath = join(root, 'package.json');\n const content = await readFile(pkgPath, 'utf-8');\n const pkg = JSON.parse(content);\n const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };\n\n // React is required\n if (!allDeps['react']) {\n checks.push({\n name: 'Peer dep: react',\n status: 'fail',\n message: 'react not found in dependencies (required)',\n fix: 'npm install react react-dom',\n });\n } else {\n checks.push({\n name: 'Peer dep: react',\n status: 'pass',\n message: `react ${allDeps['react']} installed`,\n });\n }\n\n // sass is needed for custom theming\n if (!allDeps['sass']) {\n checks.push({\n name: 'Peer dep: sass',\n status: 'warn',\n message: 'sass not installed (needed for custom SCSS theming)',\n fix: 'npm install -D sass',\n });\n }\n\n // Check optional peer deps that components might need\n const optionalPeers: Array<{ pkg: string; components: string }> = [\n { pkg: 'recharts', components: 'Chart' },\n { pkg: 'shiki', components: 'CodeBlock' },\n { pkg: 'react-day-picker', components: 'DatePicker' },\n { pkg: '@tanstack/react-table', components: 'DataTable' },\n ];\n\n for (const peer of optionalPeers) {\n if (allDeps[peer.pkg]) {\n checks.push({\n name: `Optional dep: ${peer.pkg}`,\n status: 'pass',\n message: `${peer.pkg} installed (enables ${peer.components})`,\n });\n }\n }\n } catch {\n checks.push({\n name: 'Peer dependencies',\n status: 'fail',\n message: 'Could not read package.json',\n });\n }\n\n return checks;\n}\n\nasync function checkMcpConfig(root: string): Promise<DoctorCheck> {\n const mcpConfigPaths = [\n '.mcp.json',\n '.cursor/mcp.json',\n '.vscode/mcp.json',\n ];\n\n for (const configPath of mcpConfigPaths) {\n try {\n const fullPath = join(root, configPath);\n const content = await readFile(fullPath, 'utf-8');\n const config = JSON.parse(content);\n\n // Check if fragments MCP server is configured\n const servers = config.mcpServers || config.servers || {};\n const hasFragments = Object.values(servers).some((server: unknown) => {\n const s = server as { command?: string; args?: string[] };\n return (\n s.args?.some((arg: string) => arg.includes('@fragments-sdk/mcp')) ||\n s.command?.includes('fragments')\n );\n });\n\n if (hasFragments) {\n return {\n name: 'MCP configuration',\n status: 'pass',\n message: `Fragments MCP server configured in ${configPath}`,\n };\n }\n } catch {\n // File doesn't exist or is invalid, continue\n }\n }\n\n return {\n name: 'MCP configuration',\n status: 'warn',\n message: 'No Fragments MCP server configuration found (optional)',\n fix: 'Run `fragments init` or add @fragments-sdk/mcp to your MCP config',\n };\n}\n\nasync function checkTypeScript(root: string): Promise<DoctorCheck> {\n try {\n const tsconfigPath = join(root, 'tsconfig.json');\n await access(tsconfigPath);\n return {\n name: 'TypeScript',\n status: 'pass',\n message: 'tsconfig.json found',\n };\n } catch {\n return {\n name: 'TypeScript',\n status: 'warn',\n message: 'No tsconfig.json found (TypeScript recommended but not required)',\n };\n }\n}\n\n// ============================================\n// Main Doctor Function\n// ============================================\n\n/**\n * Run diagnostic checks on a consumer project\n */\nexport async function doctor(\n options: DoctorOptions = {}\n): Promise<DoctorResult> {\n const root = resolve(options.root ?? process.cwd());\n const checks: DoctorCheck[] = [];\n\n if (!options.json) {\n console.log(pc.cyan(`\\n${BRAND.name} Doctor\\n`));\n console.log(pc.dim(`Checking project at ${root}\\n`));\n }\n\n // Run all checks\n checks.push(await checkPackageInstalled(root));\n checks.push(await checkStylesImport(root));\n checks.push(await checkThemeProvider(root));\n checks.push(...await checkScssSeeds(root));\n checks.push(...await checkPeerDeps(root));\n checks.push(await checkMcpConfig(root));\n checks.push(await checkTypeScript(root));\n\n const passed = checks.filter(c => c.status === 'pass').length;\n const warned = checks.filter(c => c.status === 'warn').length;\n const failed = checks.filter(c => c.status === 'fail').length;\n\n const result: DoctorResult = {\n success: failed === 0,\n checks,\n passed,\n warned,\n failed,\n };\n\n if (options.json) {\n console.log(JSON.stringify(result, null, 2));\n } else {\n // Print results\n for (const check of checks) {\n const icon =\n check.status === 'pass' ? pc.green('✓') :\n check.status === 'warn' ? pc.yellow('!') :\n pc.red('✗');\n\n const msg =\n check.status === 'pass' ? check.message :\n check.status === 'warn' ? pc.yellow(check.message) :\n pc.red(check.message);\n\n console.log(` ${icon} ${pc.bold(check.name)}: ${msg}`);\n\n if (check.fix && check.status !== 'pass') {\n console.log(pc.dim(` → ${check.fix}`));\n }\n }\n\n // Summary\n console.log();\n if (failed === 0 && warned === 0) {\n console.log(pc.green(`✓ All ${passed} checks passed — your setup looks great!`));\n } else if (failed === 0) {\n console.log(pc.green(`✓ ${passed} passed`) + pc.yellow(`, ${warned} warning(s)`));\n } else {\n console.log(\n pc.red(`✗ ${failed} failed`) +\n (warned > 0 ? pc.yellow(`, ${warned} warning(s)`) : '') +\n pc.dim(`, ${passed} passed`)\n );\n }\n console.log();\n }\n\n return result;\n}\n"],"mappings":"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","names":[]}
@@ -9,10 +9,11 @@ import pc from "picocolors";
9
9
  import { resolve, relative } from "path";
10
10
  import { existsSync } from "fs";
11
11
  var SCAN_DEFAULT_RULES = {
12
- "safety/no-dangerous-html": true,
12
+ "safety/block-event-handlers": true,
13
+ "safety/block-dangerous-props": true,
14
+ "safety/block-controlled-props": true,
15
+ "safety/block-function-props": true,
13
16
  "safety/sanitize-hrefs": true,
14
- "safety/no-inline-scripts": true,
15
- "safety/no-exposed-secrets": true,
16
17
  "tokens/require-design-tokens": true
17
18
  };
18
19
  function detectRootDir(cwd) {
@@ -410,4 +411,4 @@ export {
410
411
  governScan,
411
412
  governWatch
412
413
  };
413
- //# sourceMappingURL=govern-scan-OYFZYOQW.js.map
414
+ //# sourceMappingURL=govern-scan-DW4QUAYD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/govern-scan.ts"],"sourcesContent":["/**\n * govern scan / govern watch — Zero-config governance scanning\n *\n * Parses real JSX/TSX files via the existing codebase scanner, converts\n * component usages to UISpec, and runs governance checks per file.\n * Optionally submits results to Fragments Cloud.\n */\n\nimport pc from 'picocolors';\nimport { resolve, relative } from 'node:path';\nimport { existsSync } from 'node:fs';\nimport { BRAND } from '../core/index.js';\nimport type { ComponentUsage } from '../service/enhance/types.js';\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\nexport interface GovernScanOptions {\n /** Root directory to scan (default: auto-detect) */\n dir?: string;\n /** Path to govern.config.ts */\n config?: string;\n /** Output format */\n format?: 'summary' | 'json' | 'sarif';\n /** Suppress non-error output */\n quiet?: boolean;\n}\n\nexport interface GovernWatchOptions extends GovernScanOptions {\n /** Debounce interval in ms (default: 300) */\n debounce?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Scan defaults — applied when no config file exists\n// ---------------------------------------------------------------------------\n\nconst SCAN_DEFAULT_RULES: Record<string, boolean | object> = {\n 'safety/block-event-handlers': true,\n 'safety/block-dangerous-props': true,\n 'safety/block-controlled-props': true,\n 'safety/block-function-props': true,\n 'safety/sanitize-hrefs': true,\n 'tokens/require-design-tokens': true,\n};\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Auto-detect root directory by looking for common React project dirs\n */\nfunction detectRootDir(cwd: string): string {\n const candidates = ['src', 'app', 'pages', 'components'];\n for (const dir of candidates) {\n if (existsSync(resolve(cwd, dir))) {\n return cwd;\n }\n }\n return cwd;\n}\n\n/**\n * Group component usages by their source file\n */\nfunction groupByFile(usages: ComponentUsage[]): Map<string, ComponentUsage[]> {\n const grouped = new Map<string, ComponentUsage[]>();\n for (const usage of usages) {\n const existing = grouped.get(usage.filePath);\n if (existing) {\n existing.push(usage);\n } else {\n grouped.set(usage.filePath, [usage]);\n }\n }\n return grouped;\n}\n\n// ---------------------------------------------------------------------------\n// governScan\n// ---------------------------------------------------------------------------\n\nexport async function governScan(\n options: GovernScanOptions = {},\n): Promise<{ exitCode: number }> {\n const {\n loadPolicy,\n createEngine,\n buildAdaptersFromConfig,\n createCloudAdapter,\n formatVerdict,\n computeComponentHealth,\n } = await import('@fragments-sdk/govern');\n\n const { scanCodebase } = await import(\n '../service/enhance/codebase-scanner.js'\n );\n const { usagesToSpec } = await import(\n '../service/enhance/converter.js'\n );\n\n const format = options.format ?? 'summary';\n const quiet = options.quiet ?? false;\n\n if (!quiet) {\n console.log(pc.cyan(`\\n${BRAND.name} Governance Scan\\n`));\n }\n\n // 1. Resolve root directory\n const rootDir = resolve(options.dir ?? detectRootDir(process.cwd()));\n if (!quiet) {\n console.log(pc.dim(` Root: ${rootDir}\\n`));\n }\n\n // 2. Load policy — use scan defaults if no config exists\n let policy = await loadPolicy(options.config);\n const hasRules = Object.keys(policy.rules).length > 0;\n\n if (!hasRules) {\n policy = { ...policy, rules: SCAN_DEFAULT_RULES };\n if (!quiet) {\n console.log(pc.dim(' No config found — using scan defaults (safety + tokens)\\n'));\n }\n }\n\n // 3. Extract code tokens for cloud ingest\n let codeTokens: string | undefined;\n if (process.env.FRAGMENTS_API_KEY) {\n codeTokens = await extractCodeTokens(rootDir, options.config, quiet);\n }\n\n // 3b. Load contract registry for governance + cloud ingest (if fragments.json exists)\n let contractRegistry: string | undefined;\n let registryMap: Record<string, unknown> | undefined;\n {\n const { readFileSync, existsSync } = await import('node:fs');\n const fragmentsJsonPath = resolve(rootDir, 'fragments.json');\n if (existsSync(fragmentsJsonPath)) {\n try {\n const raw = readFileSync(fragmentsJsonPath, 'utf-8');\n const parsed = JSON.parse(raw);\n if (parsed.fragments && Array.isArray(parsed.fragments)) {\n // Build name-keyed map for engine registry injection\n const map: Record<string, unknown> = {};\n for (const f of parsed.fragments) {\n if (f.meta?.name) {\n map[f.meta.name] = f;\n }\n }\n registryMap = map;\n\n if (process.env.FRAGMENTS_API_KEY) {\n contractRegistry = JSON.stringify({ fragments: parsed.fragments });\n }\n if (!quiet) {\n console.log(pc.dim(` Contract registry loaded (${parsed.fragments.length} components)\\n`));\n }\n }\n } catch {\n // Invalid fragments.json — skip\n }\n }\n }\n\n // 4. Build adapters. Auto-add cloud if FRAGMENTS_API_KEY is set\n const adapters = buildAdaptersFromConfig(policy.audit);\n const hasCloudAdapter = adapters.length > 0 && policy.audit?.cloud;\n if (!hasCloudAdapter && process.env.FRAGMENTS_API_KEY) {\n adapters.push(createCloudAdapter({ codeTokens, contractRegistry }));\n if (!quiet) {\n console.log(pc.dim(' Cloud audit enabled (FRAGMENTS_API_KEY detected)\\n'));\n }\n }\n\n // 5. Create engine (with registry for contract-aware validators)\n const engine = createEngine(\n policy,\n adapters,\n registryMap\n ? { registry: { fragments: registryMap as Record<string, Record<string, unknown>> } }\n : undefined,\n );\n\n // 6. Scan codebase\n if (!quiet) {\n console.log(pc.dim(' Scanning files...\\n'));\n }\n\n const analysis = await scanCodebase({\n rootDir,\n useCache: true,\n onProgress: quiet\n ? undefined\n : (progress) => {\n if (progress.phase === 'scanning') {\n process.stdout.write(\n `\\r ${pc.dim(`[${progress.current}/${progress.total}]`)} ${pc.dim(relative(rootDir, progress.currentFile))}`,\n );\n }\n },\n });\n\n if (!quiet) {\n // Clear progress line\n process.stdout.write('\\r' + ' '.repeat(80) + '\\r');\n console.log(\n pc.dim(` Scanned ${analysis.totalFiles} files, found ${analysis.totalComponents} component types\\n`),\n );\n }\n\n // 7. Collect all usages across components\n const allUsages: ComponentUsage[] = [];\n for (const comp of Object.values(analysis.components)) {\n allUsages.push(...comp.usages);\n }\n\n if (allUsages.length === 0) {\n if (!quiet) {\n console.log(pc.yellow(' No component usages found.\\n'));\n }\n return { exitCode: 0 };\n }\n\n // 8. Group by file and run checks\n const grouped = groupByFile(allUsages);\n let totalFiles = 0;\n let passedFiles = 0;\n let totalViolations = 0;\n const violationCounts = new Map<string, number>();\n const allVerdicts: Awaited<ReturnType<typeof engine.check>>[] = [];\n\n // Build per-file usage snapshot for cloud\n const usageSnapshot: Array<{\n file: string;\n components: Array<{\n name: string;\n line: number;\n props: { static: Record<string, unknown>; dynamic: string[] };\n }>;\n }> = [];\n\n for (const [filePath, usages] of grouped) {\n const spec = usagesToSpec(usages, filePath, rootDir);\n const relPath = relative(rootDir, filePath);\n\n const verdict = await engine.check(spec, {\n runner: 'cli',\n input: relPath,\n });\n allVerdicts.push(verdict);\n\n // Collect per-file usage snapshot\n usageSnapshot.push({\n file: relPath,\n components: usages.map((u) => ({\n name: u.componentName,\n line: u.line,\n props: {\n static: u.props.static,\n dynamic: u.props.dynamic,\n },\n })),\n });\n\n totalFiles++;\n\n if (verdict.passed) {\n passedFiles++;\n } else {\n if (!quiet) {\n console.log(pc.red(` ✗ ${relPath}`));\n if (format === 'summary') {\n for (const result of verdict.results) {\n for (const v of result.violations) {\n const count = violationCounts.get(v.rule) ?? 0;\n violationCounts.set(v.rule, count + 1);\n totalViolations++;\n console.log(\n pc.dim(` ${v.severity} `) +\n pc.yellow(v.rule) +\n pc.dim(` — ${v.message}`),\n );\n if (v.nodeId) {\n console.log(pc.dim(` at ${v.nodeId}`));\n }\n }\n }\n }\n }\n }\n\n if (verdict.passed && !quiet && format === 'summary') {\n console.log(pc.green(` ✓ ${relPath}`) + pc.dim(` (${usages.length} components, score: ${verdict.score}/100)`));\n }\n\n // JSON/SARIF: print per-file\n if (format === 'json' || format === 'sarif') {\n const output = formatVerdict(verdict, format);\n console.log(output);\n }\n }\n\n // 8b. Compute component health\n const health = computeComponentHealth(allVerdicts, registryMap ?? {});\n\n // 9. Summary\n if (!quiet && format === 'summary') {\n console.log(pc.dim('\\n ─────────────────────────────────────\\n'));\n console.log(` Files checked: ${totalFiles}`);\n console.log(` Passed: ${passedFiles}/${totalFiles}`);\n console.log(` Violations: ${totalViolations}`);\n\n if (violationCounts.size > 0) {\n console.log(pc.dim('\\n Top violations:'));\n const sorted = [...violationCounts.entries()].sort((a, b) => b[1] - a[1]);\n for (const [rule, count] of sorted.slice(0, 5)) {\n console.log(pc.dim(` ${count}× `) + pc.yellow(rule));\n }\n }\n\n // Component health\n console.log(pc.dim('\\n Component Health:'));\n console.log(` Contract coverage: ${health.contractCoverage}% (${health.contractedComponents}/${health.totalComponents})`);\n console.log(` Compliance rate: ${health.overallCompliance}%`);\n\n if (health.uncontracted.length > 0) {\n console.log(pc.dim(` Uncontracted: ${health.uncontracted.slice(0, 5).join(', ')}${health.uncontracted.length > 5 ? ` (+${health.uncontracted.length - 5} more)` : ''}`));\n }\n\n console.log();\n\n if (passedFiles === totalFiles) {\n console.log(pc.green(` ✓ All files passed governance checks\\n`));\n } else {\n console.log(\n pc.red(` ✗ ${totalFiles - passedFiles} file(s) failed governance checks\\n`),\n );\n }\n }\n\n // 10. Push component usage snapshot + health to Cloud (if API key set)\n if (process.env.FRAGMENTS_API_KEY && usageSnapshot.length > 0) {\n try {\n const apiKey = process.env.FRAGMENTS_API_KEY;\n const url = process.env.FRAGMENTS_URL ?? 'https://app.usefragments.com';\n await fetch(`${url}/api/ingest`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify({\n componentUsage: JSON.stringify(usageSnapshot),\n componentHealth: JSON.stringify(health),\n }),\n });\n } catch {\n // Non-critical — don't fail the scan\n }\n }\n\n return { exitCode: passedFiles === totalFiles ? 0 : 1 };\n}\n\n// ---------------------------------------------------------------------------\n// Token extraction for cloud ingest\n// ---------------------------------------------------------------------------\n\n/**\n * Auto-detect and extract code tokens to send alongside governance verdicts.\n * Tries: 1) tokens config from fragments.config.ts, 2) Tailwind config.\n * Returns a flat JSON string of token name → value, or undefined if nothing found.\n */\nasync function extractCodeTokens(\n rootDir: string,\n configPath?: string,\n quiet?: boolean,\n): Promise<string | undefined> {\n try {\n // 1. Try fragments.config.ts tokens config\n try {\n const { loadConfig } = await import('../core/node.js');\n const { parseTokenFiles } = await import('../service/index.js');\n\n const { config, configDir } = await loadConfig(configPath);\n if (config.tokens?.include?.length) {\n const result = await parseTokenFiles(config.tokens, configDir);\n if (result.tokens.length > 0) {\n const flat: Record<string, string> = {};\n for (const token of result.tokens) {\n flat[token.name] = token.resolvedValue;\n }\n if (!quiet) {\n console.log(\n pc.dim(` Extracted ${result.tokens.length} code tokens from config\\n`),\n );\n }\n return JSON.stringify(flat);\n }\n }\n } catch {\n // No config or no tokens section — fall through\n }\n\n // 2. Try Tailwind config\n const {\n findTailwindConfig,\n loadTailwindConfig,\n } = await import('../service/token-normalizer.js');\n\n const tailwindPath = findTailwindConfig(rootDir);\n if (tailwindPath) {\n const tokens = await loadTailwindConfig(tailwindPath);\n if (tokens.length > 0) {\n const flat: Record<string, string> = {};\n for (const token of tokens) {\n flat[token.name] = token.value;\n }\n if (!quiet) {\n console.log(\n pc.dim(` Extracted ${tokens.length} tokens from Tailwind config\\n`),\n );\n }\n return JSON.stringify(flat);\n }\n }\n } catch (error) {\n if (!quiet) {\n console.log(\n pc.dim(\n ` Token extraction skipped: ${error instanceof Error ? error.message : 'unknown error'}\\n`,\n ),\n );\n }\n }\n\n return undefined;\n}\n\n// ---------------------------------------------------------------------------\n// governWatch\n// ---------------------------------------------------------------------------\n\nexport async function governWatch(\n options: GovernWatchOptions = {},\n): Promise<void> {\n const {\n loadPolicy,\n createEngine,\n buildAdaptersFromConfig,\n createCloudAdapter,\n formatVerdict,\n } = await import('@fragments-sdk/govern');\n\n const { scanFile } = await import('../service/enhance/scanner.js');\n const { usagesToSpec } = await import(\n '../service/enhance/converter.js'\n );\n\n const quiet = options.quiet ?? false;\n const debounceMs = options.debounce ?? 300;\n const format = options.format ?? 'summary';\n\n // 1. Run initial scan\n console.log(pc.cyan(`\\n${BRAND.name} Governance Watch\\n`));\n\n const { exitCode } = await governScan(options);\n if (!quiet) {\n console.log(\n pc.dim(` Initial scan ${exitCode === 0 ? 'passed' : 'completed with violations'}\\n`),\n );\n }\n\n // 2. Set up engine for incremental checks\n const rootDir = resolve(options.dir ?? detectRootDir(process.cwd()));\n let policy = await loadPolicy(options.config);\n if (Object.keys(policy.rules).length === 0) {\n policy = { ...policy, rules: SCAN_DEFAULT_RULES };\n }\n const adapters = buildAdaptersFromConfig(policy.audit);\n if (!adapters.some(() => policy.audit?.cloud) && process.env.FRAGMENTS_API_KEY) {\n adapters.push(createCloudAdapter());\n }\n const engine = createEngine(policy, adapters);\n\n // 3. Watch for changes\n console.log(pc.dim(' Watching for changes... (Ctrl+C to stop)\\n'));\n\n const chokidar = await import('chokidar');\n\n const watcher = chokidar.watch(\n ['**/*.tsx', '**/*.ts', '**/*.jsx', '**/*.js'],\n {\n cwd: rootDir,\n ignoreInitial: true,\n ignored: [\n '**/node_modules/**',\n '**/dist/**',\n '**/build/**',\n '**/.next/**',\n '**/*.test.*',\n '**/*.spec.*',\n '**/*.stories.*',\n ],\n awaitWriteFinish: { stabilityThreshold: debounceMs },\n },\n );\n\n const handleChange = async (changedRelPath: string) => {\n const absolutePath = resolve(rootDir, changedRelPath);\n\n try {\n const { usages } = await scanFile(absolutePath);\n\n if (usages.length === 0) {\n if (!quiet) {\n console.log(pc.dim(` ○ ${changedRelPath} — no component usages`));\n }\n return;\n }\n\n const spec = usagesToSpec(usages, absolutePath, rootDir);\n const verdict = await engine.check(spec, {\n runner: 'cli',\n input: changedRelPath,\n });\n\n if (verdict.passed) {\n console.log(\n pc.green(` ✓ ${changedRelPath}`) +\n pc.dim(` (${usages.length} components, score: ${verdict.score}/100)`),\n );\n } else {\n console.log(pc.red(` ✗ ${changedRelPath}`));\n if (format === 'summary') {\n for (const result of verdict.results) {\n for (const v of result.violations) {\n console.log(\n pc.dim(` ${v.severity} `) +\n pc.yellow(v.rule) +\n pc.dim(` — ${v.message}`),\n );\n }\n }\n } else {\n console.log(formatVerdict(verdict, format));\n }\n }\n } catch (error) {\n if (!quiet) {\n console.log(\n pc.dim(` ⚠ ${changedRelPath} — `) +\n pc.yellow(error instanceof Error ? error.message : 'parse error'),\n );\n }\n }\n };\n\n watcher.on('change', handleChange);\n watcher.on('add', handleChange);\n\n // Keep process alive\n await new Promise(() => {});\n}\n"],"mappings":";;;;;;;AAQA,OAAO,QAAQ;AACf,SAAS,SAAS,gBAAgB;AAClC,SAAS,kBAAkB;AA4B3B,IAAM,qBAAuD;AAAA,EAC3D,+BAA+B;AAAA,EAC/B,gCAAgC;AAAA,EAChC,iCAAiC;AAAA,EACjC,+BAA+B;AAAA,EAC/B,yBAAyB;AAAA,EACzB,gCAAgC;AAClC;AASA,SAAS,cAAc,KAAqB;AAC1C,QAAM,aAAa,CAAC,OAAO,OAAO,SAAS,YAAY;AACvD,aAAW,OAAO,YAAY;AAC5B,QAAI,WAAW,QAAQ,KAAK,GAAG,CAAC,GAAG;AACjC,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAKA,SAAS,YAAY,QAAyD;AAC5E,QAAM,UAAU,oBAAI,IAA8B;AAClD,aAAW,SAAS,QAAQ;AAC1B,UAAM,WAAW,QAAQ,IAAI,MAAM,QAAQ;AAC3C,QAAI,UAAU;AACZ,eAAS,KAAK,KAAK;AAAA,IACrB,OAAO;AACL,cAAQ,IAAI,MAAM,UAAU,CAAC,KAAK,CAAC;AAAA,IACrC;AAAA,EACF;AACA,SAAO;AACT;AAMA,eAAsB,WACpB,UAA6B,CAAC,GACC;AAC/B,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI,MAAM,OAAO,uBAAuB;AAExC,QAAM,EAAE,aAAa,IAAI,MAAM,OAC7B,gCACF;AACA,QAAM,EAAE,aAAa,IAAI,MAAM,OAC7B,yBACF;AAEA,QAAM,SAAS,QAAQ,UAAU;AACjC,QAAM,QAAQ,QAAQ,SAAS;AAE/B,MAAI,CAAC,OAAO;AACV,YAAQ,IAAI,GAAG,KAAK;AAAA,EAAK,MAAM,IAAI;AAAA,CAAoB,CAAC;AAAA,EAC1D;AAGA,QAAM,UAAU,QAAQ,QAAQ,OAAO,cAAc,QAAQ,IAAI,CAAC,CAAC;AACnE,MAAI,CAAC,OAAO;AACV,YAAQ,IAAI,GAAG,IAAI,WAAW,OAAO;AAAA,CAAI,CAAC;AAAA,EAC5C;AAGA,MAAI,SAAS,MAAM,WAAW,QAAQ,MAAM;AAC5C,QAAM,WAAW,OAAO,KAAK,OAAO,KAAK,EAAE,SAAS;AAEpD,MAAI,CAAC,UAAU;AACb,aAAS,EAAE,GAAG,QAAQ,OAAO,mBAAmB;AAChD,QAAI,CAAC,OAAO;AACV,cAAQ,IAAI,GAAG,IAAI,kEAA6D,CAAC;AAAA,IACnF;AAAA,EACF;AAGA,MAAI;AACJ,MAAI,QAAQ,IAAI,mBAAmB;AACjC,iBAAa,MAAM,kBAAkB,SAAS,QAAQ,QAAQ,KAAK;AAAA,EACrE;AAGA,MAAI;AACJ,MAAI;AACJ;AACE,UAAM,EAAE,cAAc,YAAAA,YAAW,IAAI,MAAM,OAAO,IAAS;AAC3D,UAAM,oBAAoB,QAAQ,SAAS,gBAAgB;AAC3D,QAAIA,YAAW,iBAAiB,GAAG;AACjC,UAAI;AACF,cAAM,MAAM,aAAa,mBAAmB,OAAO;AACnD,cAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,YAAI,OAAO,aAAa,MAAM,QAAQ,OAAO,SAAS,GAAG;AAEvD,gBAAM,MAA+B,CAAC;AACtC,qBAAW,KAAK,OAAO,WAAW;AAChC,gBAAI,EAAE,MAAM,MAAM;AAChB,kBAAI,EAAE,KAAK,IAAI,IAAI;AAAA,YACrB;AAAA,UACF;AACA,wBAAc;AAEd,cAAI,QAAQ,IAAI,mBAAmB;AACjC,+BAAmB,KAAK,UAAU,EAAE,WAAW,OAAO,UAAU,CAAC;AAAA,UACnE;AACA,cAAI,CAAC,OAAO;AACV,oBAAQ,IAAI,GAAG,IAAI,+BAA+B,OAAO,UAAU,MAAM;AAAA,CAAgB,CAAC;AAAA,UAC5F;AAAA,QACF;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAGA,QAAM,WAAW,wBAAwB,OAAO,KAAK;AACrD,QAAM,kBAAkB,SAAS,SAAS,KAAK,OAAO,OAAO;AAC7D,MAAI,CAAC,mBAAmB,QAAQ,IAAI,mBAAmB;AACrD,aAAS,KAAK,mBAAmB,EAAE,YAAY,iBAAiB,CAAC,CAAC;AAClE,QAAI,CAAC,OAAO;AACV,cAAQ,IAAI,GAAG,IAAI,sDAAsD,CAAC;AAAA,IAC5E;AAAA,EACF;AAGA,QAAM,SAAS;AAAA,IACb;AAAA,IACA;AAAA,IACA,cACI,EAAE,UAAU,EAAE,WAAW,YAAuD,EAAE,IAClF;AAAA,EACN;AAGA,MAAI,CAAC,OAAO;AACV,YAAQ,IAAI,GAAG,IAAI,uBAAuB,CAAC;AAAA,EAC7C;AAEA,QAAM,WAAW,MAAM,aAAa;AAAA,IAClC;AAAA,IACA,UAAU;AAAA,IACV,YAAY,QACR,SACA,CAAC,aAAa;AACZ,UAAI,SAAS,UAAU,YAAY;AACjC,gBAAQ,OAAO;AAAA,UACb,OAAO,GAAG,IAAI,IAAI,SAAS,OAAO,IAAI,SAAS,KAAK,GAAG,CAAC,IAAI,GAAG,IAAI,SAAS,SAAS,SAAS,WAAW,CAAC,CAAC;AAAA,QAC7G;AAAA,MACF;AAAA,IACF;AAAA,EACN,CAAC;AAED,MAAI,CAAC,OAAO;AAEV,YAAQ,OAAO,MAAM,OAAO,IAAI,OAAO,EAAE,IAAI,IAAI;AACjD,YAAQ;AAAA,MACN,GAAG,IAAI,aAAa,SAAS,UAAU,iBAAiB,SAAS,eAAe;AAAA,CAAoB;AAAA,IACtG;AAAA,EACF;AAGA,QAAM,YAA8B,CAAC;AACrC,aAAW,QAAQ,OAAO,OAAO,SAAS,UAAU,GAAG;AACrD,cAAU,KAAK,GAAG,KAAK,MAAM;AAAA,EAC/B;AAEA,MAAI,UAAU,WAAW,GAAG;AAC1B,QAAI,CAAC,OAAO;AACV,cAAQ,IAAI,GAAG,OAAO,gCAAgC,CAAC;AAAA,IACzD;AACA,WAAO,EAAE,UAAU,EAAE;AAAA,EACvB;AAGA,QAAM,UAAU,YAAY,SAAS;AACrC,MAAI,aAAa;AACjB,MAAI,cAAc;AAClB,MAAI,kBAAkB;AACtB,QAAM,kBAAkB,oBAAI,IAAoB;AAChD,QAAM,cAA0D,CAAC;AAGjE,QAAM,gBAOD,CAAC;AAEN,aAAW,CAAC,UAAU,MAAM,KAAK,SAAS;AACxC,UAAM,OAAO,aAAa,QAAQ,UAAU,OAAO;AACnD,UAAM,UAAU,SAAS,SAAS,QAAQ;AAE1C,UAAM,UAAU,MAAM,OAAO,MAAM,MAAM;AAAA,MACvC,QAAQ;AAAA,MACR,OAAO;AAAA,IACT,CAAC;AACD,gBAAY,KAAK,OAAO;AAGxB,kBAAc,KAAK;AAAA,MACjB,MAAM;AAAA,MACN,YAAY,OAAO,IAAI,CAAC,OAAO;AAAA,QAC7B,MAAM,EAAE;AAAA,QACR,MAAM,EAAE;AAAA,QACR,OAAO;AAAA,UACL,QAAQ,EAAE,MAAM;AAAA,UAChB,SAAS,EAAE,MAAM;AAAA,QACnB;AAAA,MACF,EAAE;AAAA,IACJ,CAAC;AAED;AAEA,QAAI,QAAQ,QAAQ;AAClB;AAAA,IACF,OAAO;AACL,UAAI,CAAC,OAAO;AACV,gBAAQ,IAAI,GAAG,IAAI,YAAO,OAAO,EAAE,CAAC;AACpC,YAAI,WAAW,WAAW;AACxB,qBAAW,UAAU,QAAQ,SAAS;AACpC,uBAAW,KAAK,OAAO,YAAY;AACjC,oBAAM,QAAQ,gBAAgB,IAAI,EAAE,IAAI,KAAK;AAC7C,8BAAgB,IAAI,EAAE,MAAM,QAAQ,CAAC;AACrC;AACA,sBAAQ;AAAA,gBACN,GAAG,IAAI,OAAO,EAAE,QAAQ,GAAG,IAC3B,GAAG,OAAO,EAAE,IAAI,IAChB,GAAG,IAAI,WAAM,EAAE,OAAO,EAAE;AAAA,cAC1B;AACA,kBAAI,EAAE,QAAQ;AACZ,wBAAQ,IAAI,GAAG,IAAI,YAAY,EAAE,MAAM,EAAE,CAAC;AAAA,cAC5C;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,QAAI,QAAQ,UAAU,CAAC,SAAS,WAAW,WAAW;AACpD,cAAQ,IAAI,GAAG,MAAM,YAAO,OAAO,EAAE,IAAI,GAAG,IAAI,KAAK,OAAO,MAAM,uBAAuB,QAAQ,KAAK,OAAO,CAAC;AAAA,IAChH;AAGA,QAAI,WAAW,UAAU,WAAW,SAAS;AAC3C,YAAM,SAAS,cAAc,SAAS,MAAM;AAC5C,cAAQ,IAAI,MAAM;AAAA,IACpB;AAAA,EACF;AAGA,QAAM,SAAS,uBAAuB,aAAa,eAAe,CAAC,CAAC;AAGpE,MAAI,CAAC,SAAS,WAAW,WAAW;AAClC,YAAQ,IAAI,GAAG,IAAI,sOAA6C,CAAC;AACjE,YAAQ,IAAI,qBAAqB,UAAU,EAAE;AAC7C,YAAQ,IAAI,qBAAqB,WAAW,IAAI,UAAU,EAAE;AAC5D,YAAQ,IAAI,qBAAqB,eAAe,EAAE;AAElD,QAAI,gBAAgB,OAAO,GAAG;AAC5B,cAAQ,IAAI,GAAG,IAAI,qBAAqB,CAAC;AACzC,YAAM,SAAS,CAAC,GAAG,gBAAgB,QAAQ,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;AACxE,iBAAW,CAAC,MAAM,KAAK,KAAK,OAAO,MAAM,GAAG,CAAC,GAAG;AAC9C,gBAAQ,IAAI,GAAG,IAAI,OAAO,KAAK,OAAI,IAAI,GAAG,OAAO,IAAI,CAAC;AAAA,MACxD;AAAA,IACF;AAGA,YAAQ,IAAI,GAAG,IAAI,uBAAuB,CAAC;AAC3C,YAAQ,IAAI,2BAA2B,OAAO,gBAAgB,MAAM,OAAO,oBAAoB,IAAI,OAAO,eAAe,GAAG;AAC5H,YAAQ,IAAI,2BAA2B,OAAO,iBAAiB,GAAG;AAElE,QAAI,OAAO,aAAa,SAAS,GAAG;AAClC,cAAQ,IAAI,GAAG,IAAI,2BAA2B,OAAO,aAAa,MAAM,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,GAAG,OAAO,aAAa,SAAS,IAAI,MAAM,OAAO,aAAa,SAAS,CAAC,WAAW,EAAE,EAAE,CAAC;AAAA,IAClL;AAEA,YAAQ,IAAI;AAEZ,QAAI,gBAAgB,YAAY;AAC9B,cAAQ,IAAI,GAAG,MAAM;AAAA,CAA0C,CAAC;AAAA,IAClE,OAAO;AACL,cAAQ;AAAA,QACN,GAAG,IAAI,YAAO,aAAa,WAAW;AAAA,CAAqC;AAAA,MAC7E;AAAA,IACF;AAAA,EACF;AAGA,MAAI,QAAQ,IAAI,qBAAqB,cAAc,SAAS,GAAG;AAC7D,QAAI;AACF,YAAM,SAAS,QAAQ,IAAI;AAC3B,YAAM,MAAM,QAAQ,IAAI,iBAAiB;AACzC,YAAM,MAAM,GAAG,GAAG,eAAe;AAAA,QAC/B,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,UAChB,eAAe,UAAU,MAAM;AAAA,QACjC;AAAA,QACA,MAAM,KAAK,UAAU;AAAA,UACnB,gBAAgB,KAAK,UAAU,aAAa;AAAA,UAC5C,iBAAiB,KAAK,UAAU,MAAM;AAAA,QACxC,CAAC;AAAA,MACH,CAAC;AAAA,IACH,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SAAO,EAAE,UAAU,gBAAgB,aAAa,IAAI,EAAE;AACxD;AAWA,eAAe,kBACb,SACA,YACA,OAC6B;AAC7B,MAAI;AAEF,QAAI;AACF,YAAM,EAAE,WAAW,IAAI,MAAM,OAAO,oBAAiB;AACrD,YAAM,EAAE,gBAAgB,IAAI,MAAM,OAAO,uBAAqB;AAE9D,YAAM,EAAE,QAAQ,UAAU,IAAI,MAAM,WAAW,UAAU;AACzD,UAAI,OAAO,QAAQ,SAAS,QAAQ;AAClC,cAAM,SAAS,MAAM,gBAAgB,OAAO,QAAQ,SAAS;AAC7D,YAAI,OAAO,OAAO,SAAS,GAAG;AAC5B,gBAAM,OAA+B,CAAC;AACtC,qBAAW,SAAS,OAAO,QAAQ;AACjC,iBAAK,MAAM,IAAI,IAAI,MAAM;AAAA,UAC3B;AACA,cAAI,CAAC,OAAO;AACV,oBAAQ;AAAA,cACN,GAAG,IAAI,eAAe,OAAO,OAAO,MAAM;AAAA,CAA4B;AAAA,YACxE;AAAA,UACF;AACA,iBAAO,KAAK,UAAU,IAAI;AAAA,QAC5B;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAER;AAGA,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,IACF,IAAI,MAAM,OAAO,gCAAgC;AAEjD,UAAM,eAAe,mBAAmB,OAAO;AAC/C,QAAI,cAAc;AAChB,YAAM,SAAS,MAAM,mBAAmB,YAAY;AACpD,UAAI,OAAO,SAAS,GAAG;AACrB,cAAM,OAA+B,CAAC;AACtC,mBAAW,SAAS,QAAQ;AAC1B,eAAK,MAAM,IAAI,IAAI,MAAM;AAAA,QAC3B;AACA,YAAI,CAAC,OAAO;AACV,kBAAQ;AAAA,YACN,GAAG,IAAI,eAAe,OAAO,MAAM;AAAA,CAAgC;AAAA,UACrE;AAAA,QACF;AACA,eAAO,KAAK,UAAU,IAAI;AAAA,MAC5B;AAAA,IACF;AAAA,EACF,SAAS,OAAO;AACd,QAAI,CAAC,OAAO;AACV,cAAQ;AAAA,QACN,GAAG;AAAA,UACD,+BAA+B,iBAAiB,QAAQ,MAAM,UAAU,eAAe;AAAA;AAAA,QACzF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAMA,eAAsB,YACpB,UAA8B,CAAC,GAChB;AACf,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI,MAAM,OAAO,uBAAuB;AAExC,QAAM,EAAE,SAAS,IAAI,MAAM,OAAO,uBAA+B;AACjE,QAAM,EAAE,aAAa,IAAI,MAAM,OAC7B,yBACF;AAEA,QAAM,QAAQ,QAAQ,SAAS;AAC/B,QAAM,aAAa,QAAQ,YAAY;AACvC,QAAM,SAAS,QAAQ,UAAU;AAGjC,UAAQ,IAAI,GAAG,KAAK;AAAA,EAAK,MAAM,IAAI;AAAA,CAAqB,CAAC;AAEzD,QAAM,EAAE,SAAS,IAAI,MAAM,WAAW,OAAO;AAC7C,MAAI,CAAC,OAAO;AACV,YAAQ;AAAA,MACN,GAAG,IAAI,kBAAkB,aAAa,IAAI,WAAW,2BAA2B;AAAA,CAAI;AAAA,IACtF;AAAA,EACF;AAGA,QAAM,UAAU,QAAQ,QAAQ,OAAO,cAAc,QAAQ,IAAI,CAAC,CAAC;AACnE,MAAI,SAAS,MAAM,WAAW,QAAQ,MAAM;AAC5C,MAAI,OAAO,KAAK,OAAO,KAAK,EAAE,WAAW,GAAG;AAC1C,aAAS,EAAE,GAAG,QAAQ,OAAO,mBAAmB;AAAA,EAClD;AACA,QAAM,WAAW,wBAAwB,OAAO,KAAK;AACrD,MAAI,CAAC,SAAS,KAAK,MAAM,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,mBAAmB;AAC9E,aAAS,KAAK,mBAAmB,CAAC;AAAA,EACpC;AACA,QAAM,SAAS,aAAa,QAAQ,QAAQ;AAG5C,UAAQ,IAAI,GAAG,IAAI,8CAA8C,CAAC;AAElE,QAAM,WAAW,MAAM,OAAO,UAAU;AAExC,QAAM,UAAU,SAAS;AAAA,IACvB,CAAC,YAAY,WAAW,YAAY,SAAS;AAAA,IAC7C;AAAA,MACE,KAAK;AAAA,MACL,eAAe;AAAA,MACf,SAAS;AAAA,QACP;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACA,kBAAkB,EAAE,oBAAoB,WAAW;AAAA,IACrD;AAAA,EACF;AAEA,QAAM,eAAe,OAAO,mBAA2B;AACrD,UAAM,eAAe,QAAQ,SAAS,cAAc;AAEpD,QAAI;AACF,YAAM,EAAE,OAAO,IAAI,MAAM,SAAS,YAAY;AAE9C,UAAI,OAAO,WAAW,GAAG;AACvB,YAAI,CAAC,OAAO;AACV,kBAAQ,IAAI,GAAG,IAAI,YAAO,cAAc,6BAAwB,CAAC;AAAA,QACnE;AACA;AAAA,MACF;AAEA,YAAM,OAAO,aAAa,QAAQ,cAAc,OAAO;AACvD,YAAM,UAAU,MAAM,OAAO,MAAM,MAAM;AAAA,QACvC,QAAQ;AAAA,QACR,OAAO;AAAA,MACT,CAAC;AAED,UAAI,QAAQ,QAAQ;AAClB,gBAAQ;AAAA,UACN,GAAG,MAAM,YAAO,cAAc,EAAE,IAChC,GAAG,IAAI,KAAK,OAAO,MAAM,uBAAuB,QAAQ,KAAK,OAAO;AAAA,QACtE;AAAA,MACF,OAAO;AACL,gBAAQ,IAAI,GAAG,IAAI,YAAO,cAAc,EAAE,CAAC;AAC3C,YAAI,WAAW,WAAW;AACxB,qBAAW,UAAU,QAAQ,SAAS;AACpC,uBAAW,KAAK,OAAO,YAAY;AACjC,sBAAQ;AAAA,gBACN,GAAG,IAAI,OAAO,EAAE,QAAQ,GAAG,IAC3B,GAAG,OAAO,EAAE,IAAI,IAChB,GAAG,IAAI,WAAM,EAAE,OAAO,EAAE;AAAA,cAC1B;AAAA,YACF;AAAA,UACF;AAAA,QACF,OAAO;AACL,kBAAQ,IAAI,cAAc,SAAS,MAAM,CAAC;AAAA,QAC5C;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AACd,UAAI,CAAC,OAAO;AACV,gBAAQ;AAAA,UACN,GAAG,IAAI,YAAO,cAAc,UAAK,IACjC,GAAG,OAAO,iBAAiB,QAAQ,MAAM,UAAU,aAAa;AAAA,QAClE;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,UAAQ,GAAG,UAAU,YAAY;AACjC,UAAQ,GAAG,OAAO,YAAY;AAG9B,QAAM,IAAI,QAAQ,MAAM;AAAA,EAAC,CAAC;AAC5B;","names":["existsSync"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fragments-sdk/cli",
3
- "version": "0.15.1",
3
+ "version": "0.15.3",
4
4
  "license": "FSL-1.1-MIT",
5
5
  "description": "CLI, MCP server, and dev tools for Fragments design system",
6
6
  "author": "Conan McNicholl",
@@ -82,12 +82,12 @@
82
82
  "vite-plugin-svgr": "^4.5.0",
83
83
  "zod": "^3.24.1",
84
84
  "@fragments-sdk/compiler": "0.2.0",
85
+ "@fragments-sdk/extract": "0.1.0",
85
86
  "@fragments-sdk/context": "0.6.0",
87
+ "@fragments-sdk/govern": "^0.3.1",
86
88
  "@fragments-sdk/core": "2.0.0",
87
89
  "@fragments-sdk/viewer": "0.2.8",
88
- "@fragments-sdk/webmcp": "3.0.0",
89
- "@fragments-sdk/extract": "0.1.0",
90
- "@fragments-sdk/govern": "^0.3.0"
90
+ "@fragments-sdk/webmcp": "3.0.0"
91
91
  },
92
92
  "devDependencies": {
93
93
  "@types/babel__generator": "^7.6.8",
package/src/bin.ts CHANGED
@@ -40,7 +40,9 @@ import { graph } from './commands/graph.js';
40
40
  import { inspect } from './commands/inspect.js';
41
41
  import { discover } from './commands/discover.js';
42
42
  import { perf } from './commands/perf.js';
43
- import { doctor } from './commands/doctor.js';
43
+ // doctor is imported lazily — it pulls @fragments-sdk/viewer/docs-data which
44
+ // ships as raw TS source and cannot be resolved by Node at startup.
45
+ type DoctorFn = typeof import('./commands/doctor.js')['doctor'];
44
46
  import { setup } from './commands/setup.js';
45
47
  import { sync } from './commands/sync.js';
46
48
  import { governCheck, governInit, governReport, governConnect } from './commands/govern.js';
@@ -1296,6 +1298,7 @@ program
1296
1298
  .option('--fix', 'Auto-fix issues where possible')
1297
1299
  .action(async (options) => {
1298
1300
  try {
1301
+ const { doctor } = await import('./commands/doctor.js');
1299
1302
  const result = await doctor({
1300
1303
  root: options.root,
1301
1304
  json: options.json,
@@ -37,6 +37,6 @@
37
37
  "source": "extracted",
38
38
  "verified": false,
39
39
  "frameworkSupport": "native",
40
- "extractedAt": "2026-03-22T23:45:29.606Z"
40
+ "extractedAt": "2026-03-25T22:09:03.672Z"
41
41
  }
42
42
  }
@@ -15,6 +15,6 @@
15
15
  "source": "extracted",
16
16
  "verified": false,
17
17
  "frameworkSupport": "native",
18
- "extractedAt": "2026-03-22T23:45:29.607Z"
18
+ "extractedAt": "2026-03-25T22:09:03.682Z"
19
19
  }
20
20
  }
@@ -13,10 +13,18 @@ describe("init", () => {
13
13
  });
14
14
 
15
15
  afterAll(async () => {
16
- await rm(tmpDir, { recursive: true, force: true });
16
+ // Retry cleanup CI runners can race with async handles
17
+ for (let i = 0; i < 3; i++) {
18
+ try {
19
+ await rm(tmpDir, { recursive: true, force: true });
20
+ break;
21
+ } catch {
22
+ await new Promise((r) => setTimeout(r, 200));
23
+ }
24
+ }
17
25
  });
18
26
 
19
- it("skips Fragments UI runtime injection for shadcn-style projects", { timeout: 10_000 }, async () => {
27
+ it("skips Fragments UI runtime injection for shadcn-style projects", async () => {
20
28
  const projectDir = join(tmpDir, "shadcn-app");
21
29
  await mkdir(join(projectDir, "src", "components", "ui"), { recursive: true });
22
30