@fragments-sdk/cli 0.14.2 → 0.15.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.
Files changed (135) hide show
  1. package/README.md +0 -3
  2. package/dist/bin.js +4290 -3754
  3. package/dist/bin.js.map +1 -1
  4. package/dist/{chunk-TXFCEDOC.js → chunk-2WXKALIG.js} +2 -2
  5. package/dist/{chunk-I34BC3CU.js → chunk-32LIWN2P.js} +1006 -3
  6. package/dist/chunk-32LIWN2P.js.map +1 -0
  7. package/dist/{chunk-55KERLWL.js → chunk-65WSVDV5.js} +314 -89
  8. package/dist/chunk-65WSVDV5.js.map +1 -0
  9. package/dist/chunk-7DZC4YEV.js +294 -0
  10. package/dist/chunk-7DZC4YEV.js.map +1 -0
  11. package/dist/{chunk-LOYS64QS.js → chunk-7WHVW72L.js} +230 -19
  12. package/dist/chunk-7WHVW72L.js.map +1 -0
  13. package/dist/{chunk-PJT5IZ37.js → chunk-BJE3425I.js} +19 -52
  14. package/dist/{chunk-PJT5IZ37.js.map → chunk-BJE3425I.js.map} +1 -1
  15. package/dist/{chunk-5A6X2Y73.js → chunk-CZD3AD4Q.js} +12 -11
  16. package/dist/chunk-CZD3AD4Q.js.map +1 -0
  17. package/dist/{chunk-EYXVAMEX.js → chunk-MN3TJ3D5.js} +72 -3
  18. package/dist/chunk-MN3TJ3D5.js.map +1 -0
  19. package/dist/chunk-QCN35LJU.js +630 -0
  20. package/dist/chunk-QCN35LJU.js.map +1 -0
  21. package/dist/chunk-T47OLCSF.js +36 -0
  22. package/dist/chunk-T47OLCSF.js.map +1 -0
  23. package/dist/{chunk-APTQIBS5.js → chunk-XJQ5BIWI.js} +144 -1049
  24. package/dist/chunk-XJQ5BIWI.js.map +1 -0
  25. package/dist/codebase-scanner-VOTPXRYW.js +22 -0
  26. package/dist/converter-JLINP7CJ.js +34 -0
  27. package/dist/converter-JLINP7CJ.js.map +1 -0
  28. package/dist/core/index.js +43 -1
  29. package/dist/{generate-RYWIPDN2.js → generate-A4FP5426.js} +3 -4
  30. package/dist/{generate-RYWIPDN2.js.map → generate-A4FP5426.js.map} +1 -1
  31. package/dist/govern-scan-UCBZR6D6.js +280 -0
  32. package/dist/govern-scan-UCBZR6D6.js.map +1 -0
  33. package/dist/index.d.ts +2 -1
  34. package/dist/index.js +11 -11
  35. package/dist/{init-WRUSW7R5.js → init-HGSM35XA.js} +131 -128
  36. package/dist/init-HGSM35XA.js.map +1 -0
  37. package/dist/{init-cloud-REQ3XLHO.js → init-cloud-MQ6GRJAZ.js} +2 -2
  38. package/dist/mcp-bin.js +5 -36
  39. package/dist/mcp-bin.js.map +1 -1
  40. package/dist/scan-VNNKACG2.js +15 -0
  41. package/dist/{scan-generate-TFZVL3BT.js → scan-generate-TWRHNU5M.js} +335 -46
  42. package/dist/scan-generate-TWRHNU5M.js.map +1 -0
  43. package/dist/scanner-7LAZYPWZ.js +13 -0
  44. package/dist/{service-HKJ6B7P7.js → service-FHQU7YS7.js} +27 -23
  45. package/dist/{snapshot-C5DYIGIV.js → snapshot-KQEQ6XHL.js} +2 -2
  46. package/dist/{static-viewer-DUVC4UIM.js → static-viewer-63PG6FWY.js} +3 -3
  47. package/dist/static-viewer-63PG6FWY.js.map +1 -0
  48. package/dist/{test-JW7JIDFG.js → test-UQYUCZIS.js} +4 -6
  49. package/dist/{test-JW7JIDFG.js.map → test-UQYUCZIS.js.map} +1 -1
  50. package/dist/{tokens-KE73G5JC.js → tokens-6GYKDV6U.js} +6 -5
  51. package/dist/{tokens-KE73G5JC.js.map → tokens-6GYKDV6U.js.map} +1 -1
  52. package/dist/tokens-generate-VTZV5EEW.js +86 -0
  53. package/dist/tokens-generate-VTZV5EEW.js.map +1 -0
  54. package/package.json +6 -6
  55. package/src/bin.ts +210 -48
  56. package/src/build.ts +130 -6
  57. package/src/commands/__fixtures__/shadcn-label-wrapper/package.json +7 -0
  58. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +42 -0
  59. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.tsx +11 -0
  60. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +20 -0
  61. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.tsx +14 -0
  62. package/src/commands/__fixtures__/shadcn-label-wrapper/tsconfig.app.json +23 -0
  63. package/src/commands/__tests__/init.test.ts +113 -0
  64. package/src/commands/__tests__/scan-generate.test.ts +188 -69
  65. package/src/commands/__tests__/verify.test.ts +91 -0
  66. package/src/commands/discover.ts +151 -0
  67. package/src/commands/enhance.ts +3 -1
  68. package/src/commands/govern-scan.ts +386 -0
  69. package/src/commands/govern.ts +2 -2
  70. package/src/commands/init.ts +152 -28
  71. package/src/commands/inspect.ts +290 -0
  72. package/src/commands/migrate-contract.ts +85 -0
  73. package/src/commands/scan-generate.ts +438 -50
  74. package/src/commands/scan.ts +1 -0
  75. package/src/commands/setup.ts +27 -50
  76. package/src/commands/tokens-generate.ts +113 -0
  77. package/src/commands/verify.ts +195 -1
  78. package/src/core/__fixtures__/shadcn-input/input.tsx +7 -0
  79. package/src/core/__fixtures__/shadcn-input/tsconfig.json +14 -0
  80. package/src/core/__fixtures__/shadcn-label/label.tsx +11 -0
  81. package/src/core/__fixtures__/shadcn-label/primitive.tsx +14 -0
  82. package/src/core/__fixtures__/shadcn-label/tsconfig.json +14 -0
  83. package/src/core/__fixtures__/shadcn-radix-label/label.tsx +11 -0
  84. package/src/core/__fixtures__/shadcn-radix-label/node_modules/radix-ui/index.d.ts +12 -0
  85. package/src/core/__fixtures__/shadcn-radix-label/tsconfig.json +14 -0
  86. package/src/core/__tests__/contract-parity.test.ts +316 -0
  87. package/src/core/component-extractor.test.ts +39 -0
  88. package/src/core/component-extractor.ts +92 -1
  89. package/src/core/config.ts +2 -1
  90. package/src/core/discovery.ts +13 -2
  91. package/src/core/drift-verifier.ts +123 -0
  92. package/src/core/extractor-adapter.ts +80 -0
  93. package/src/mcp/__tests__/projectFields.test.ts +1 -1
  94. package/src/mcp/utils.ts +1 -50
  95. package/src/migrate/converter.ts +3 -3
  96. package/src/migrate/fragment-to-contract.ts +253 -0
  97. package/src/migrate/report.ts +1 -1
  98. package/src/scripts/token-benchmark.ts +121 -0
  99. package/src/service/__tests__/props-extractor.test.ts +94 -0
  100. package/src/service/__tests__/token-normalizer.test.ts +690 -0
  101. package/src/service/ast-utils.ts +4 -23
  102. package/src/service/babel-config.ts +23 -0
  103. package/src/service/enhance/converter.ts +61 -0
  104. package/src/service/enhance/props-extractor.ts +25 -8
  105. package/src/service/enhance/scanner.ts +5 -24
  106. package/src/service/snippet-validation.ts +9 -3
  107. package/src/service/token-normalizer.ts +510 -0
  108. package/src/shared/index.ts +1 -0
  109. package/src/shared/project-fields.ts +46 -0
  110. package/src/viewer/__tests__/viewer-integration.test.ts +8 -8
  111. package/src/viewer/preview-adapter.ts +116 -0
  112. package/src/viewer/style-utils.ts +27 -412
  113. package/src/viewer/vite-plugin.ts +2 -2
  114. package/dist/chunk-55KERLWL.js.map +0 -1
  115. package/dist/chunk-5A6X2Y73.js.map +0 -1
  116. package/dist/chunk-APTQIBS5.js.map +0 -1
  117. package/dist/chunk-EYXVAMEX.js.map +0 -1
  118. package/dist/chunk-I34BC3CU.js.map +0 -1
  119. package/dist/chunk-LOYS64QS.js.map +0 -1
  120. package/dist/chunk-ZKTFKHWN.js +0 -324
  121. package/dist/chunk-ZKTFKHWN.js.map +0 -1
  122. package/dist/discovery-VDANZAJ2.js +0 -28
  123. package/dist/init-WRUSW7R5.js.map +0 -1
  124. package/dist/scan-YJHQIRKG.js +0 -14
  125. package/dist/scan-generate-TFZVL3BT.js.map +0 -1
  126. package/dist/viewer-2TZS3NDL.js +0 -2730
  127. package/dist/viewer-2TZS3NDL.js.map +0 -1
  128. package/src/commands/dev.ts +0 -107
  129. /package/dist/{chunk-TXFCEDOC.js.map → chunk-2WXKALIG.js.map} +0 -0
  130. /package/dist/{discovery-VDANZAJ2.js.map → codebase-scanner-VOTPXRYW.js.map} +0 -0
  131. /package/dist/{init-cloud-REQ3XLHO.js.map → init-cloud-MQ6GRJAZ.js.map} +0 -0
  132. /package/dist/{scan-YJHQIRKG.js.map → scan-VNNKACG2.js.map} +0 -0
  133. /package/dist/{service-HKJ6B7P7.js.map → scanner-7LAZYPWZ.js.map} +0 -0
  134. /package/dist/{static-viewer-DUVC4UIM.js.map → service-FHQU7YS7.js.map} +0 -0
  135. /package/dist/{snapshot-C5DYIGIV.js.map → snapshot-KQEQ6XHL.js.map} +0 -0
@@ -0,0 +1,630 @@
1
+ import { createRequire as __banner_createRequire } from 'module'; const require = __banner_createRequire(import.meta.url);
2
+ import {
3
+ scanFile
4
+ } from "./chunk-7DZC4YEV.js";
5
+
6
+ // src/service/enhance/codebase-scanner.ts
7
+ import fg from "fast-glob";
8
+ import { readFile as readFile2 } from "fs/promises";
9
+ import { relative, resolve, dirname as dirname2 } from "path";
10
+
11
+ // src/service/enhance/aggregator.ts
12
+ function aggregateComponentUsages(componentName, sourceFile, usages, imports) {
13
+ const uniqueFiles = new Set(usages.map((u) => u.filePath));
14
+ const patternMap = /* @__PURE__ */ new Map();
15
+ for (const usage of usages) {
16
+ const propKey = createPropKey(usage.props.static);
17
+ const existing = patternMap.get(propKey);
18
+ if (existing) {
19
+ existing.count++;
20
+ if (!existing.files.includes(usage.filePath)) {
21
+ existing.files.push(usage.filePath);
22
+ }
23
+ if (existing.sampleContexts.length < 3) {
24
+ existing.sampleContexts.push(usage.context);
25
+ }
26
+ } else {
27
+ patternMap.set(propKey, {
28
+ props: { ...usage.props.static },
29
+ count: 1,
30
+ files: [usage.filePath],
31
+ sampleContexts: [usage.context]
32
+ });
33
+ }
34
+ }
35
+ const patterns = Array.from(patternMap.values()).sort(
36
+ (a, b) => b.count - a.count
37
+ );
38
+ const contextMap = /* @__PURE__ */ new Map();
39
+ for (const usage of usages) {
40
+ const context = classifyFileContext(usage.filePath, usage.context);
41
+ const existing = contextMap.get(context);
42
+ if (existing) {
43
+ existing.count++;
44
+ if (!existing.files.includes(usage.filePath)) {
45
+ existing.files.push(usage.filePath);
46
+ }
47
+ } else {
48
+ contextMap.set(context, { count: 1, files: [usage.filePath] });
49
+ }
50
+ }
51
+ const contexts = Array.from(contextMap.entries()).map(([type, data]) => ({ type, ...data })).sort((a, b) => b.count - a.count);
52
+ return {
53
+ name: componentName,
54
+ sourceFile,
55
+ totalUsages: usages.length,
56
+ uniqueFiles: uniqueFiles.size,
57
+ patterns,
58
+ contexts,
59
+ usages,
60
+ imports
61
+ };
62
+ }
63
+ function aggregateAllUsages(usages, imports, componentSources) {
64
+ const usagesByComponent = /* @__PURE__ */ new Map();
65
+ for (const usage of usages) {
66
+ const existing = usagesByComponent.get(usage.componentName) ?? [];
67
+ existing.push(usage);
68
+ usagesByComponent.set(usage.componentName, existing);
69
+ }
70
+ const importsByComponent = /* @__PURE__ */ new Map();
71
+ for (const imp of imports) {
72
+ const existing = importsByComponent.get(imp.componentName) ?? [];
73
+ existing.push(imp);
74
+ importsByComponent.set(imp.componentName, existing);
75
+ }
76
+ const components = {};
77
+ const allComponentNames = /* @__PURE__ */ new Set([
78
+ ...usagesByComponent.keys(),
79
+ ...importsByComponent.keys()
80
+ ]);
81
+ for (const componentName of allComponentNames) {
82
+ const componentUsages = usagesByComponent.get(componentName) ?? [];
83
+ const componentImports = importsByComponent.get(componentName) ?? [];
84
+ const sourceFile = componentSources.get(componentName) ?? "unknown";
85
+ components[componentName] = aggregateComponentUsages(
86
+ componentName,
87
+ sourceFile,
88
+ componentUsages,
89
+ componentImports
90
+ );
91
+ }
92
+ const allFiles = /* @__PURE__ */ new Set();
93
+ for (const usage of usages) {
94
+ allFiles.add(usage.filePath);
95
+ }
96
+ for (const imp of imports) {
97
+ allFiles.add(imp.filePath);
98
+ }
99
+ return {
100
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
101
+ rootDir: "",
102
+ // Will be set by caller
103
+ totalFiles: allFiles.size,
104
+ totalComponents: Object.keys(components).length,
105
+ components,
106
+ errorFiles: []
107
+ // Will be populated by scanner
108
+ };
109
+ }
110
+ function createPropKey(props) {
111
+ const entries = Object.entries(props).sort(([a], [b]) => a.localeCompare(b));
112
+ return entries.map(([k, v]) => `${k}=${JSON.stringify(v)}`).join("|");
113
+ }
114
+ function classifyFileContext(filePath, context) {
115
+ const lowerPath = filePath.toLowerCase();
116
+ const lowerContext = context.toLowerCase();
117
+ if (lowerPath.includes("/modal") || lowerPath.includes("modal.") || lowerContext.includes("<modal")) {
118
+ return "modal";
119
+ }
120
+ if (lowerPath.includes("/dialog") || lowerPath.includes("dialog.") || lowerContext.includes("<dialog")) {
121
+ return "dialog";
122
+ }
123
+ if (lowerPath.includes("/form") || lowerPath.includes("form.") || lowerContext.includes("<form") || lowerContext.includes("onsubmit")) {
124
+ return "form";
125
+ }
126
+ if (lowerPath.includes("/page") || lowerPath.includes("pages/") || lowerPath.includes("/routes/")) {
127
+ return "page";
128
+ }
129
+ if (lowerPath.includes("/layout") || lowerPath.includes("layout.") || lowerPath.includes("/layouts/")) {
130
+ return "layout";
131
+ }
132
+ if (lowerPath.includes("/card") || lowerPath.includes("card.") || lowerContext.includes("<card")) {
133
+ return "card";
134
+ }
135
+ if (lowerPath.includes("/table") || lowerPath.includes("table.") || lowerContext.includes("<table") || lowerContext.includes("<tr")) {
136
+ return "table";
137
+ }
138
+ if (lowerPath.includes("/list") || lowerPath.includes("list.") || lowerContext.includes("<ul") || lowerContext.includes("<ol") || lowerContext.includes("<li")) {
139
+ return "list";
140
+ }
141
+ if (lowerPath.includes("/nav") || lowerPath.includes("navigation") || lowerContext.includes("<nav")) {
142
+ return "navigation";
143
+ }
144
+ if (lowerPath.includes("/header") || lowerPath.includes("header.") || lowerContext.includes("<header")) {
145
+ return "header";
146
+ }
147
+ if (lowerPath.includes("/footer") || lowerPath.includes("footer.") || lowerContext.includes("<footer")) {
148
+ return "footer";
149
+ }
150
+ if (lowerPath.includes("/sidebar") || lowerPath.includes("sidebar.") || lowerContext.includes("<aside")) {
151
+ return "sidebar";
152
+ }
153
+ return "unknown";
154
+ }
155
+ function findCommonPropCombinations(usages, minOccurrences = 3) {
156
+ const combinations = /* @__PURE__ */ new Map();
157
+ for (const usage of usages) {
158
+ const allProps = [
159
+ ...Object.keys(usage.props.static),
160
+ ...usage.props.dynamic
161
+ ].sort();
162
+ for (let size = 2; size <= Math.min(allProps.length, 4); size++) {
163
+ for (const combo of combinations_k(allProps, size)) {
164
+ const key = combo.join(",");
165
+ combinations.set(key, (combinations.get(key) ?? 0) + 1);
166
+ }
167
+ }
168
+ }
169
+ return Array.from(combinations.entries()).filter(([, count]) => count >= minOccurrences).map(([key, count]) => ({ props: key.split(","), count })).sort((a, b) => b.count - a.count);
170
+ }
171
+ function* combinations_k(arr, k) {
172
+ if (k === 0) {
173
+ yield [];
174
+ return;
175
+ }
176
+ if (arr.length < k) return;
177
+ for (let i = 0; i <= arr.length - k; i++) {
178
+ for (const combo of combinations_k(arr.slice(i + 1), k - 1)) {
179
+ yield [arr[i], ...combo];
180
+ }
181
+ }
182
+ }
183
+ function inferRelations(componentName, analysis, allAnalyses) {
184
+ const relations = [];
185
+ const parentCounts = /* @__PURE__ */ new Map();
186
+ const siblingCounts = /* @__PURE__ */ new Map();
187
+ for (const usage of analysis.usages) {
188
+ if (usage.parentElement && usage.parentElement !== "div" && usage.parentElement !== "span") {
189
+ const parent = usage.parentElement;
190
+ const existing = parentCounts.get(parent);
191
+ if (existing) {
192
+ existing.count++;
193
+ if (!existing.files.includes(usage.filePath)) {
194
+ existing.files.push(usage.filePath);
195
+ }
196
+ } else {
197
+ parentCounts.set(parent, { count: 1, files: [usage.filePath] });
198
+ }
199
+ }
200
+ }
201
+ const filesWithThisComponent = new Set(analysis.usages.map((u) => u.filePath));
202
+ for (const [otherName, otherAnalysis] of Object.entries(allAnalyses)) {
203
+ if (otherName === componentName) continue;
204
+ let sharedFiles = 0;
205
+ const sharedFileList = [];
206
+ for (const usage of otherAnalysis.usages) {
207
+ if (filesWithThisComponent.has(usage.filePath)) {
208
+ if (!sharedFileList.includes(usage.filePath)) {
209
+ sharedFiles++;
210
+ sharedFileList.push(usage.filePath);
211
+ }
212
+ }
213
+ }
214
+ if (sharedFiles >= 3) {
215
+ siblingCounts.set(otherName, {
216
+ count: sharedFiles,
217
+ files: sharedFileList.slice(0, 5)
218
+ });
219
+ }
220
+ }
221
+ for (const [parent, data] of parentCounts) {
222
+ if (data.count >= 2) {
223
+ relations.push({
224
+ component: parent,
225
+ relationship: "parent",
226
+ frequency: data.count,
227
+ sampleFiles: data.files.slice(0, 3)
228
+ });
229
+ }
230
+ }
231
+ for (const [sibling, data] of siblingCounts) {
232
+ relations.push({
233
+ component: sibling,
234
+ relationship: "sibling",
235
+ frequency: data.count,
236
+ sampleFiles: data.files.slice(0, 3)
237
+ });
238
+ }
239
+ relations.sort((a, b) => b.frequency - a.frequency);
240
+ return relations;
241
+ }
242
+ function inferAllRelations(analysis) {
243
+ const allRelations = /* @__PURE__ */ new Map();
244
+ for (const [componentName, componentAnalysis] of Object.entries(analysis.components)) {
245
+ const relations = inferRelations(componentName, componentAnalysis, analysis.components);
246
+ if (relations.length > 0) {
247
+ allRelations.set(componentName, relations);
248
+ }
249
+ }
250
+ return allRelations;
251
+ }
252
+ function summarizePatternsForPrompt(analysis, maxPatterns = 10, maxContexts = 5) {
253
+ const lines = [];
254
+ lines.push(`## ${analysis.name}`);
255
+ lines.push(`Total usages: ${analysis.totalUsages} across ${analysis.uniqueFiles} files`);
256
+ lines.push("");
257
+ lines.push("### Usage Patterns (by frequency)");
258
+ const topPatterns = analysis.patterns.slice(0, maxPatterns);
259
+ for (const pattern of topPatterns) {
260
+ const propsStr = Object.entries(pattern.props).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
261
+ lines.push(`- ${propsStr || "(no props)"}: ${pattern.count} usages in ${pattern.files.length} files`);
262
+ }
263
+ lines.push("");
264
+ lines.push("### File Contexts");
265
+ const topContexts = analysis.contexts.slice(0, maxContexts);
266
+ for (const ctx of topContexts) {
267
+ lines.push(`- ${ctx.type}: ${ctx.count} usages`);
268
+ }
269
+ lines.push("");
270
+ lines.push("### Sample Usages");
271
+ for (const pattern of topPatterns.slice(0, 3)) {
272
+ if (pattern.sampleContexts[0]) {
273
+ lines.push("```tsx");
274
+ lines.push(pattern.sampleContexts[0].trim());
275
+ lines.push("```");
276
+ lines.push("");
277
+ }
278
+ }
279
+ return lines.join("\n");
280
+ }
281
+
282
+ // src/service/enhance/cache.ts
283
+ import { readFile, writeFile } from "fs/promises";
284
+ import { existsSync } from "fs";
285
+ import { createHash } from "crypto";
286
+ import { join, dirname } from "path";
287
+ import { mkdir } from "fs/promises";
288
+ var CURRENT_CACHE_VERSION = 1;
289
+ var CACHE_FILENAME = "analysis-cache.json";
290
+ function getCachePath(rootDir) {
291
+ return join(rootDir, ".fragments", CACHE_FILENAME);
292
+ }
293
+ async function loadCache(rootDir) {
294
+ const cachePath = getCachePath(rootDir);
295
+ if (!existsSync(cachePath)) {
296
+ return null;
297
+ }
298
+ try {
299
+ const content = await readFile(cachePath, "utf-8");
300
+ const cache = JSON.parse(content);
301
+ if (cache.version !== CURRENT_CACHE_VERSION) {
302
+ console.warn(
303
+ `Cache version mismatch: expected ${CURRENT_CACHE_VERSION}, got ${cache.version}. Invalidating cache.`
304
+ );
305
+ return null;
306
+ }
307
+ if (cache.rootDir !== rootDir) {
308
+ console.warn(`Cache root mismatch. Invalidating cache.`);
309
+ return null;
310
+ }
311
+ return cache;
312
+ } catch (error) {
313
+ console.warn(`Failed to load cache: ${error.message}`);
314
+ return null;
315
+ }
316
+ }
317
+ async function saveCache(rootDir, cache) {
318
+ const cachePath = getCachePath(rootDir);
319
+ const cacheDir = dirname(cachePath);
320
+ if (!existsSync(cacheDir)) {
321
+ await mkdir(cacheDir, { recursive: true });
322
+ }
323
+ cache.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
324
+ await writeFile(cachePath, JSON.stringify(cache, null, 2));
325
+ }
326
+ function createEmptyCache(rootDir) {
327
+ const now = (/* @__PURE__ */ new Date()).toISOString();
328
+ return {
329
+ version: CURRENT_CACHE_VERSION,
330
+ createdAt: now,
331
+ updatedAt: now,
332
+ rootDir,
333
+ files: {}
334
+ };
335
+ }
336
+ function computeFileHash(content) {
337
+ return createHash("md5").update(content).digest("hex");
338
+ }
339
+ function getCachedFile(cache, filePath) {
340
+ return cache.files[filePath] ?? null;
341
+ }
342
+ function updateCacheFile(cache, filePath, hash, imports, usages) {
343
+ cache.files[filePath] = {
344
+ hash,
345
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
346
+ imports,
347
+ usages
348
+ };
349
+ }
350
+ function removeCacheFile(cache, filePath) {
351
+ delete cache.files[filePath];
352
+ }
353
+ async function detectFileChanges(cache, currentFiles, getFileHash) {
354
+ const changes = {
355
+ added: [],
356
+ modified: [],
357
+ deleted: [],
358
+ unchanged: []
359
+ };
360
+ const currentFileSet = new Set(currentFiles);
361
+ const cachedFileSet = new Set(Object.keys(cache.files));
362
+ for (const filePath of currentFiles) {
363
+ const entry = cache.files[filePath];
364
+ if (!entry) {
365
+ changes.added.push(filePath);
366
+ } else {
367
+ const currentHash = await getFileHash(filePath);
368
+ if (currentHash !== entry.hash) {
369
+ changes.modified.push(filePath);
370
+ } else {
371
+ changes.unchanged.push(filePath);
372
+ }
373
+ }
374
+ }
375
+ for (const cachedFile of cachedFileSet) {
376
+ if (!currentFileSet.has(cachedFile)) {
377
+ changes.deleted.push(cachedFile);
378
+ }
379
+ }
380
+ return changes;
381
+ }
382
+ function getCacheStats(cache) {
383
+ let totalImports = 0;
384
+ let totalUsages = 0;
385
+ for (const entry of Object.values(cache.files)) {
386
+ totalImports += entry.imports.length;
387
+ totalUsages += entry.usages.length;
388
+ }
389
+ const ageMs = Date.now() - new Date(cache.updatedAt).getTime();
390
+ const ageMinutes = Math.round(ageMs / 6e4);
391
+ const cacheAge = ageMinutes < 60 ? `${ageMinutes} minutes ago` : ageMinutes < 1440 ? `${Math.round(ageMinutes / 60)} hours ago` : `${Math.round(ageMinutes / 1440)} days ago`;
392
+ return {
393
+ totalFiles: Object.keys(cache.files).length,
394
+ totalImports,
395
+ totalUsages,
396
+ cacheAge
397
+ };
398
+ }
399
+
400
+ // src/service/enhance/codebase-scanner.ts
401
+ var DEFAULT_INCLUDE = [
402
+ "**/*.tsx",
403
+ "**/*.ts",
404
+ "**/*.jsx",
405
+ "**/*.js"
406
+ ];
407
+ var DEFAULT_EXCLUDE = [
408
+ "**/node_modules/**",
409
+ "**/dist/**",
410
+ "**/build/**",
411
+ "**/.next/**",
412
+ "**/coverage/**",
413
+ "**/__tests__/**",
414
+ "**/*.test.*",
415
+ "**/*.spec.*",
416
+ "**/*.stories.*",
417
+ "**/*.fragment.*",
418
+ "**/storybook-static/**"
419
+ ];
420
+ async function scanCodebase(options) {
421
+ const {
422
+ rootDir,
423
+ include = DEFAULT_INCLUDE,
424
+ exclude = DEFAULT_EXCLUDE,
425
+ componentNames,
426
+ useCache = true,
427
+ onProgress
428
+ } = options;
429
+ const absoluteRoot = resolve(rootDir);
430
+ let cache = null;
431
+ if (useCache) {
432
+ cache = await loadCache(absoluteRoot);
433
+ }
434
+ if (!cache) {
435
+ cache = createEmptyCache(absoluteRoot);
436
+ }
437
+ onProgress?.({
438
+ current: 0,
439
+ total: 0,
440
+ currentFile: "",
441
+ phase: "discovering"
442
+ });
443
+ const files = await fg(include, {
444
+ cwd: absoluteRoot,
445
+ ignore: exclude,
446
+ absolute: true,
447
+ onlyFiles: true
448
+ });
449
+ const totalFiles = files.length;
450
+ const trackedComponents = componentNames ? new Set(componentNames) : void 0;
451
+ const allImports = [];
452
+ const allUsages = [];
453
+ const errorFiles = [];
454
+ const componentSources = /* @__PURE__ */ new Map();
455
+ for (let i = 0; i < files.length; i++) {
456
+ const filePath = files[i];
457
+ const relativePath = relative(absoluteRoot, filePath);
458
+ onProgress?.({
459
+ current: i + 1,
460
+ total: totalFiles,
461
+ currentFile: relativePath,
462
+ phase: "scanning"
463
+ });
464
+ try {
465
+ const content = await readFile2(filePath, "utf-8");
466
+ const fileHash = computeFileHash(content);
467
+ const cachedEntry = getCachedFile(cache, filePath);
468
+ if (cachedEntry && cachedEntry.hash === fileHash) {
469
+ allImports.push(...cachedEntry.imports);
470
+ allUsages.push(...cachedEntry.usages);
471
+ for (const imp of cachedEntry.imports) {
472
+ if (!componentSources.has(imp.componentName)) {
473
+ const sourceFile = resolveImportSource(filePath, imp.source);
474
+ if (sourceFile) {
475
+ componentSources.set(imp.componentName, sourceFile);
476
+ }
477
+ }
478
+ }
479
+ continue;
480
+ }
481
+ const { imports, usages } = await scanFile(filePath, trackedComponents);
482
+ updateCacheFile(cache, filePath, fileHash, imports, usages);
483
+ allImports.push(...imports);
484
+ allUsages.push(...usages);
485
+ for (const imp of imports) {
486
+ if (!componentSources.has(imp.componentName)) {
487
+ const sourceFile = resolveImportSource(filePath, imp.source);
488
+ if (sourceFile) {
489
+ componentSources.set(imp.componentName, sourceFile);
490
+ }
491
+ }
492
+ }
493
+ } catch (error) {
494
+ errorFiles.push(relativePath);
495
+ console.warn(`Error scanning ${relativePath}:`, error.message);
496
+ }
497
+ }
498
+ if (useCache) {
499
+ await saveCache(absoluteRoot, cache);
500
+ }
501
+ onProgress?.({
502
+ current: totalFiles,
503
+ total: totalFiles,
504
+ currentFile: "",
505
+ phase: "aggregating"
506
+ });
507
+ const analysis = aggregateAllUsages(allUsages, allImports, componentSources);
508
+ analysis.rootDir = absoluteRoot;
509
+ analysis.totalFiles = totalFiles;
510
+ analysis.errorFiles = errorFiles;
511
+ return analysis;
512
+ }
513
+ async function incrementalScan(rootDir, changes, existingCache, onProgress) {
514
+ const absoluteRoot = resolve(rootDir);
515
+ const cache = { ...existingCache, files: { ...existingCache.files } };
516
+ for (const filePath of changes.deleted) {
517
+ removeCacheFile(cache, filePath);
518
+ }
519
+ const filesToScan = [...changes.added, ...changes.modified];
520
+ const totalFiles = filesToScan.length;
521
+ const allImports = [];
522
+ const allUsages = [];
523
+ const errorFiles = [];
524
+ const componentSources = /* @__PURE__ */ new Map();
525
+ for (const filePath of changes.unchanged) {
526
+ const cachedEntry = getCachedFile(cache, filePath);
527
+ if (cachedEntry) {
528
+ allImports.push(...cachedEntry.imports);
529
+ allUsages.push(...cachedEntry.usages);
530
+ for (const imp of cachedEntry.imports) {
531
+ if (!componentSources.has(imp.componentName)) {
532
+ const sourceFile = resolveImportSource(filePath, imp.source);
533
+ if (sourceFile) {
534
+ componentSources.set(imp.componentName, sourceFile);
535
+ }
536
+ }
537
+ }
538
+ }
539
+ }
540
+ for (let i = 0; i < filesToScan.length; i++) {
541
+ const filePath = filesToScan[i];
542
+ const relativePath = relative(absoluteRoot, filePath);
543
+ onProgress?.({
544
+ current: i + 1,
545
+ total: totalFiles,
546
+ currentFile: relativePath,
547
+ phase: "scanning"
548
+ });
549
+ try {
550
+ const content = await readFile2(filePath, "utf-8");
551
+ const fileHash = computeFileHash(content);
552
+ const { imports, usages } = await scanFile(filePath);
553
+ updateCacheFile(cache, filePath, fileHash, imports, usages);
554
+ allImports.push(...imports);
555
+ allUsages.push(...usages);
556
+ for (const imp of imports) {
557
+ if (!componentSources.has(imp.componentName)) {
558
+ const sourceFile = resolveImportSource(filePath, imp.source);
559
+ if (sourceFile) {
560
+ componentSources.set(imp.componentName, sourceFile);
561
+ }
562
+ }
563
+ }
564
+ } catch (error) {
565
+ errorFiles.push(relativePath);
566
+ }
567
+ }
568
+ await saveCache(absoluteRoot, cache);
569
+ const analysis = aggregateAllUsages(allUsages, allImports, componentSources);
570
+ analysis.rootDir = absoluteRoot;
571
+ analysis.totalFiles = changes.unchanged.length + filesToScan.length;
572
+ analysis.errorFiles = errorFiles;
573
+ return { analysis, cache };
574
+ }
575
+ function resolveImportSource(importingFile, source) {
576
+ if (!source.startsWith(".") && !source.startsWith("/")) {
577
+ return null;
578
+ }
579
+ const importDir = dirname2(importingFile);
580
+ const extensions = ["", ".tsx", ".ts", ".jsx", ".js", "/index.tsx", "/index.ts"];
581
+ for (const ext of extensions) {
582
+ const fullPath = resolve(importDir, source + ext);
583
+ if (ext === "" && source.endsWith(".tsx")) {
584
+ return fullPath;
585
+ }
586
+ if (ext) {
587
+ return resolve(importDir, source) + ext;
588
+ }
589
+ }
590
+ return resolve(importDir, source);
591
+ }
592
+ function getScanStats(analysis) {
593
+ const totalUsages = Object.values(analysis.components).reduce(
594
+ (sum, c) => sum + c.totalUsages,
595
+ 0
596
+ );
597
+ const topComponents = Object.values(analysis.components).map((c) => ({ name: c.name, usages: c.totalUsages })).sort((a, b) => b.usages - a.usages).slice(0, 10);
598
+ return {
599
+ totalFiles: analysis.totalFiles,
600
+ totalComponents: analysis.totalComponents,
601
+ totalUsages,
602
+ topComponents
603
+ };
604
+ }
605
+ async function hasCachedAnalysis(rootDir) {
606
+ const cache = await loadCache(resolve(rootDir));
607
+ if (!cache) return false;
608
+ const stats = getCacheStats(cache);
609
+ return stats.totalFiles > 0;
610
+ }
611
+
612
+ export {
613
+ aggregateComponentUsages,
614
+ aggregateAllUsages,
615
+ findCommonPropCombinations,
616
+ inferRelations,
617
+ inferAllRelations,
618
+ summarizePatternsForPrompt,
619
+ loadCache,
620
+ saveCache,
621
+ createEmptyCache,
622
+ computeFileHash,
623
+ detectFileChanges,
624
+ getCacheStats,
625
+ scanCodebase,
626
+ incrementalScan,
627
+ getScanStats,
628
+ hasCachedAnalysis
629
+ };
630
+ //# sourceMappingURL=chunk-QCN35LJU.js.map