wogiflow 1.0.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 (221) hide show
  1. package/.workflow/agents/reviewer.md +81 -0
  2. package/.workflow/agents/security.md +94 -0
  3. package/.workflow/agents/story-writer.md +58 -0
  4. package/.workflow/bridges/base-bridge.js +395 -0
  5. package/.workflow/bridges/claude-bridge.js +434 -0
  6. package/.workflow/bridges/index.js +130 -0
  7. package/.workflow/lib/assumption-detector.js +481 -0
  8. package/.workflow/lib/config-substitution.js +371 -0
  9. package/.workflow/lib/failure-categories.js +478 -0
  10. package/.workflow/state/app-map.md.template +15 -0
  11. package/.workflow/state/architecture.md.template +24 -0
  12. package/.workflow/state/component-index.json.template +5 -0
  13. package/.workflow/state/decisions.md.template +15 -0
  14. package/.workflow/state/feedback-patterns.md.template +9 -0
  15. package/.workflow/state/knowledge-sync.json.template +6 -0
  16. package/.workflow/state/progress.md.template +14 -0
  17. package/.workflow/state/ready.json.template +7 -0
  18. package/.workflow/state/request-log.md.template +14 -0
  19. package/.workflow/state/session-state.json.template +11 -0
  20. package/.workflow/state/stack.md.template +33 -0
  21. package/.workflow/state/testing.md.template +36 -0
  22. package/.workflow/templates/claude-md.hbs +257 -0
  23. package/.workflow/templates/correction-report.md +67 -0
  24. package/.workflow/templates/gemini-md.hbs +52 -0
  25. package/README.md +1802 -0
  26. package/bin/flow +205 -0
  27. package/lib/index.js +33 -0
  28. package/lib/installer.js +467 -0
  29. package/lib/release-channel.js +269 -0
  30. package/lib/skill-registry.js +526 -0
  31. package/lib/upgrader.js +401 -0
  32. package/lib/utils.js +305 -0
  33. package/package.json +64 -0
  34. package/scripts/flow +985 -0
  35. package/scripts/flow-adaptive-learning.js +1259 -0
  36. package/scripts/flow-aggregate.js +488 -0
  37. package/scripts/flow-archive +133 -0
  38. package/scripts/flow-auto-context.js +1015 -0
  39. package/scripts/flow-auto-learn.js +615 -0
  40. package/scripts/flow-bridge.js +223 -0
  41. package/scripts/flow-browser-suggest.js +316 -0
  42. package/scripts/flow-bug.js +247 -0
  43. package/scripts/flow-cascade.js +711 -0
  44. package/scripts/flow-changelog +85 -0
  45. package/scripts/flow-checkpoint.js +483 -0
  46. package/scripts/flow-cli.js +403 -0
  47. package/scripts/flow-code-intelligence.js +760 -0
  48. package/scripts/flow-complexity.js +502 -0
  49. package/scripts/flow-config-set.js +152 -0
  50. package/scripts/flow-constants.js +157 -0
  51. package/scripts/flow-context +152 -0
  52. package/scripts/flow-context-init.js +482 -0
  53. package/scripts/flow-context-monitor.js +384 -0
  54. package/scripts/flow-context-scoring.js +886 -0
  55. package/scripts/flow-correct.js +458 -0
  56. package/scripts/flow-damage-control.js +985 -0
  57. package/scripts/flow-deps +101 -0
  58. package/scripts/flow-diff.js +700 -0
  59. package/scripts/flow-done +151 -0
  60. package/scripts/flow-done.js +489 -0
  61. package/scripts/flow-durable-session.js +1541 -0
  62. package/scripts/flow-entropy-monitor.js +345 -0
  63. package/scripts/flow-export-profile +349 -0
  64. package/scripts/flow-export-scanner.js +1046 -0
  65. package/scripts/flow-figma-confirm.js +400 -0
  66. package/scripts/flow-figma-extract.js +496 -0
  67. package/scripts/flow-figma-generate.js +683 -0
  68. package/scripts/flow-figma-index.js +909 -0
  69. package/scripts/flow-figma-match.js +617 -0
  70. package/scripts/flow-figma-mcp-server.js +518 -0
  71. package/scripts/flow-figma-pipeline.js +414 -0
  72. package/scripts/flow-file-ops.js +301 -0
  73. package/scripts/flow-gate-confidence.js +825 -0
  74. package/scripts/flow-guided-edit.js +659 -0
  75. package/scripts/flow-health +185 -0
  76. package/scripts/flow-health.js +413 -0
  77. package/scripts/flow-hooks.js +556 -0
  78. package/scripts/flow-http-client.js +249 -0
  79. package/scripts/flow-hybrid-detect.js +167 -0
  80. package/scripts/flow-hybrid-interactive.js +591 -0
  81. package/scripts/flow-hybrid-test.js +152 -0
  82. package/scripts/flow-import-profile +439 -0
  83. package/scripts/flow-init +253 -0
  84. package/scripts/flow-instruction-richness.js +827 -0
  85. package/scripts/flow-jira-integration.js +579 -0
  86. package/scripts/flow-knowledge-router.js +522 -0
  87. package/scripts/flow-knowledge-sync.js +589 -0
  88. package/scripts/flow-linear-integration.js +631 -0
  89. package/scripts/flow-links.js +774 -0
  90. package/scripts/flow-log-manager.js +559 -0
  91. package/scripts/flow-loop-enforcer.js +1246 -0
  92. package/scripts/flow-loop-retry-learning.js +630 -0
  93. package/scripts/flow-lsp.js +923 -0
  94. package/scripts/flow-map-index +348 -0
  95. package/scripts/flow-map-sync +201 -0
  96. package/scripts/flow-memory-blocks.js +668 -0
  97. package/scripts/flow-memory-compactor.js +350 -0
  98. package/scripts/flow-memory-db.js +1110 -0
  99. package/scripts/flow-memory-sync.js +484 -0
  100. package/scripts/flow-metrics.js +353 -0
  101. package/scripts/flow-migrate-ids.js +370 -0
  102. package/scripts/flow-model-adapter.js +802 -0
  103. package/scripts/flow-model-router.js +884 -0
  104. package/scripts/flow-models.js +1231 -0
  105. package/scripts/flow-morning.js +517 -0
  106. package/scripts/flow-multi-approach.js +660 -0
  107. package/scripts/flow-new-feature +86 -0
  108. package/scripts/flow-onboard +1042 -0
  109. package/scripts/flow-orchestrate-llm.js +459 -0
  110. package/scripts/flow-orchestrate.js +3592 -0
  111. package/scripts/flow-output.js +123 -0
  112. package/scripts/flow-parallel-detector.js +399 -0
  113. package/scripts/flow-parallel-dispatch.js +987 -0
  114. package/scripts/flow-parallel.js +428 -0
  115. package/scripts/flow-pattern-enforcer.js +600 -0
  116. package/scripts/flow-prd-manager.js +282 -0
  117. package/scripts/flow-progress.js +323 -0
  118. package/scripts/flow-project-analyzer.js +975 -0
  119. package/scripts/flow-prompt-composer.js +487 -0
  120. package/scripts/flow-providers.js +1381 -0
  121. package/scripts/flow-queue.js +308 -0
  122. package/scripts/flow-ready +82 -0
  123. package/scripts/flow-ready.js +189 -0
  124. package/scripts/flow-regression.js +396 -0
  125. package/scripts/flow-response-parser.js +450 -0
  126. package/scripts/flow-resume.js +284 -0
  127. package/scripts/flow-rules-sync.js +439 -0
  128. package/scripts/flow-run-trace.js +718 -0
  129. package/scripts/flow-safety.js +587 -0
  130. package/scripts/flow-search +104 -0
  131. package/scripts/flow-security.js +481 -0
  132. package/scripts/flow-session-end +106 -0
  133. package/scripts/flow-session-end.js +437 -0
  134. package/scripts/flow-session-state.js +671 -0
  135. package/scripts/flow-setup-hooks +216 -0
  136. package/scripts/flow-setup-hooks.js +377 -0
  137. package/scripts/flow-skill-create.js +329 -0
  138. package/scripts/flow-skill-creator.js +572 -0
  139. package/scripts/flow-skill-generator.js +1046 -0
  140. package/scripts/flow-skill-learn.js +880 -0
  141. package/scripts/flow-skill-matcher.js +578 -0
  142. package/scripts/flow-spec-generator.js +820 -0
  143. package/scripts/flow-stack-wizard.js +895 -0
  144. package/scripts/flow-standup +162 -0
  145. package/scripts/flow-start +74 -0
  146. package/scripts/flow-start.js +235 -0
  147. package/scripts/flow-status +110 -0
  148. package/scripts/flow-status.js +301 -0
  149. package/scripts/flow-step-browser.js +83 -0
  150. package/scripts/flow-step-changelog.js +217 -0
  151. package/scripts/flow-step-comments.js +306 -0
  152. package/scripts/flow-step-complexity.js +234 -0
  153. package/scripts/flow-step-coverage.js +218 -0
  154. package/scripts/flow-step-knowledge.js +193 -0
  155. package/scripts/flow-step-pr-tests.js +364 -0
  156. package/scripts/flow-step-regression.js +89 -0
  157. package/scripts/flow-step-review.js +516 -0
  158. package/scripts/flow-step-security.js +162 -0
  159. package/scripts/flow-step-silent-failures.js +290 -0
  160. package/scripts/flow-step-simplifier.js +346 -0
  161. package/scripts/flow-story +105 -0
  162. package/scripts/flow-story.js +500 -0
  163. package/scripts/flow-suspend.js +252 -0
  164. package/scripts/flow-sync-daemon.js +654 -0
  165. package/scripts/flow-task-analyzer.js +606 -0
  166. package/scripts/flow-team-dashboard.js +748 -0
  167. package/scripts/flow-team-sync.js +752 -0
  168. package/scripts/flow-team.js +977 -0
  169. package/scripts/flow-tech-options.js +528 -0
  170. package/scripts/flow-templates.js +812 -0
  171. package/scripts/flow-tiered-learning.js +728 -0
  172. package/scripts/flow-trace +204 -0
  173. package/scripts/flow-transcript-chunking.js +1106 -0
  174. package/scripts/flow-transcript-digest.js +7918 -0
  175. package/scripts/flow-transcript-language.js +465 -0
  176. package/scripts/flow-transcript-parsing.js +1085 -0
  177. package/scripts/flow-transcript-stories.js +2194 -0
  178. package/scripts/flow-update-map +224 -0
  179. package/scripts/flow-utils.js +2242 -0
  180. package/scripts/flow-verification.js +644 -0
  181. package/scripts/flow-verify.js +1177 -0
  182. package/scripts/flow-voice-input.js +638 -0
  183. package/scripts/flow-watch +168 -0
  184. package/scripts/flow-workflow-steps.js +521 -0
  185. package/scripts/flow-workflow.js +1029 -0
  186. package/scripts/flow-worktree.js +489 -0
  187. package/scripts/hooks/adapters/base-adapter.js +102 -0
  188. package/scripts/hooks/adapters/claude-code.js +359 -0
  189. package/scripts/hooks/adapters/index.js +79 -0
  190. package/scripts/hooks/core/component-check.js +341 -0
  191. package/scripts/hooks/core/index.js +35 -0
  192. package/scripts/hooks/core/loop-check.js +241 -0
  193. package/scripts/hooks/core/session-context.js +294 -0
  194. package/scripts/hooks/core/task-gate.js +177 -0
  195. package/scripts/hooks/core/validation.js +230 -0
  196. package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
  197. package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
  198. package/scripts/hooks/entry/claude-code/session-end.js +87 -0
  199. package/scripts/hooks/entry/claude-code/session-start.js +46 -0
  200. package/scripts/hooks/entry/claude-code/stop.js +43 -0
  201. package/scripts/postinstall.js +139 -0
  202. package/templates/browser-test-flow.json +56 -0
  203. package/templates/bug-report.md +43 -0
  204. package/templates/component-detail.md +42 -0
  205. package/templates/component.stories.tsx +49 -0
  206. package/templates/context/constraints.md +83 -0
  207. package/templates/context/conventions.md +177 -0
  208. package/templates/context/stack.md +60 -0
  209. package/templates/correction-report.md +90 -0
  210. package/templates/feature-proposal.md +35 -0
  211. package/templates/hybrid/_base.md +254 -0
  212. package/templates/hybrid/_patterns.md +45 -0
  213. package/templates/hybrid/create-component.md +127 -0
  214. package/templates/hybrid/create-file.md +56 -0
  215. package/templates/hybrid/create-hook.md +145 -0
  216. package/templates/hybrid/create-service.md +70 -0
  217. package/templates/hybrid/fix-bug.md +33 -0
  218. package/templates/hybrid/modify-file.md +55 -0
  219. package/templates/story.md +68 -0
  220. package/templates/task.json +56 -0
  221. package/templates/trace.md +69 -0
@@ -0,0 +1,1046 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Export Scanner
5
+ *
6
+ * Scans TypeScript/JavaScript files for exports to build an accurate
7
+ * import map for the local LLM. This ensures the LLM only uses imports
8
+ * that actually exist in the project.
9
+ *
10
+ * Usage:
11
+ * node flow-export-scanner.js [project-root]
12
+ * node flow-export-scanner.js --cache # Use cached export map if fresh
13
+ *
14
+ * When used as a module, call setProjectRoot() before other functions.
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const { getProjectRoot: getProjectRootFromUtils, getConfig } = require('./flow-utils');
20
+
21
+ // Default to getProjectRoot from utils, can be overridden via setProjectRoot() or CLI arg
22
+ let PROJECT_ROOT = getProjectRootFromUtils();
23
+ let CONFIG_PATH = path.join(PROJECT_ROOT, '.workflow/config.json');
24
+ let CACHE_PATH = path.join(PROJECT_ROOT, '.workflow/state/export-map.json');
25
+ const CACHE_MAX_AGE_MS = 5 * 60 * 1000; // 5 minutes
26
+
27
+ /**
28
+ * Set the project root directory.
29
+ * Must be called before using any other functions when used as a module.
30
+ * @param {string} root - Absolute path to project root
31
+ */
32
+ function setProjectRoot(root) {
33
+ PROJECT_ROOT = path.resolve(root);
34
+ CONFIG_PATH = path.join(PROJECT_ROOT, '.workflow/config.json');
35
+ CACHE_PATH = path.join(PROJECT_ROOT, '.workflow/state/export-map.json');
36
+ }
37
+
38
+ /**
39
+ * Get current project root
40
+ * @returns {string}
41
+ */
42
+ function getProjectRoot() {
43
+ return PROJECT_ROOT;
44
+ }
45
+
46
+ // Alias getConfig as loadConfig for minimal code changes
47
+ const loadConfig = getConfig;
48
+
49
+ // ============================================================
50
+ // Export Extraction
51
+ // ============================================================
52
+
53
+ /**
54
+ * Extract exports from a TypeScript/JavaScript file
55
+ * @param {string} filePath - Path to the file
56
+ * @returns {{ namedExports: string[], defaultExport: string|null, types: string[], arrayExports: string[] }}
57
+ */
58
+ function extractExports(filePath) {
59
+ const result = {
60
+ namedExports: [],
61
+ defaultExport: null,
62
+ types: [],
63
+ arrayExports: [] // Exports that are arrays (for variant detection)
64
+ };
65
+
66
+ if (!fs.existsSync(filePath)) return result;
67
+
68
+ try {
69
+ const content = fs.readFileSync(filePath, 'utf-8');
70
+
71
+ // Match export { X, Y, Z } and export { X as Y }
72
+ const reExportMatches = content.matchAll(/export\s+\{\s*([^}]+)\s*\}/g);
73
+ for (const match of reExportMatches) {
74
+ const exports = match[1].split(',').map(e => {
75
+ const parts = e.trim().split(/\s+as\s+/);
76
+ return parts[parts.length - 1].trim(); // Use the aliased name if exists
77
+ }).filter(e => e && !e.startsWith('type '));
78
+ result.namedExports.push(...exports);
79
+ }
80
+
81
+ // Match export type { X, Y }
82
+ const typeExportMatches = content.matchAll(/export\s+type\s+\{\s*([^}]+)\s*\}/g);
83
+ for (const match of typeExportMatches) {
84
+ const types = match[1].split(',').map(e => e.trim().split(/\s+as\s+/).pop().trim());
85
+ result.types.push(...types);
86
+ }
87
+
88
+ // Match export const/function/class X and detect arrays
89
+ const namedExportMatches = content.matchAll(/export\s+(?:const|let|var)\s+(\w+)\s*(?::\s*\w+(?:\[\])?)?\s*=\s*(\[|\{|[^;]+)/g);
90
+ for (const match of namedExportMatches) {
91
+ const exportName = match[1];
92
+ const valueStart = match[2].trim();
93
+
94
+ if (!result.namedExports.includes(exportName)) {
95
+ result.namedExports.push(exportName);
96
+ }
97
+
98
+ // Detect if this is an array export (common for variants)
99
+ if (valueStart === '[' ||
100
+ exportName.includes('Variants') ||
101
+ exportName.includes('Sizes') ||
102
+ exportName.includes('Statuses') ||
103
+ exportName.includes('Options')) {
104
+ result.arrayExports.push(exportName);
105
+ }
106
+ }
107
+
108
+ // Match export function/class
109
+ const funcExportMatches = content.matchAll(/export\s+(?:function|class)\s+(\w+)/g);
110
+ for (const match of funcExportMatches) {
111
+ if (!result.namedExports.includes(match[1])) {
112
+ result.namedExports.push(match[1]);
113
+ }
114
+ }
115
+
116
+ // Match export type/interface X
117
+ const typeDefMatches = content.matchAll(/export\s+(?:type|interface)\s+(\w+)/g);
118
+ for (const match of typeDefMatches) {
119
+ if (!result.types.includes(match[1])) {
120
+ result.types.push(match[1]);
121
+ }
122
+ }
123
+
124
+ // Match export default X or export default function X
125
+ const defaultMatch = content.match(/export\s+default\s+(?:function\s+)?(\w+)/);
126
+ if (defaultMatch) {
127
+ result.defaultExport = defaultMatch[1];
128
+ }
129
+
130
+ // Also check for "export default" at end of file (common pattern)
131
+ const defaultAtEnd = content.match(/export\s+default\s+(\w+)\s*;?\s*$/m);
132
+ if (defaultAtEnd && !result.defaultExport) {
133
+ result.defaultExport = defaultAtEnd[1];
134
+ }
135
+
136
+ } catch (err) {
137
+ // Ignore read errors
138
+ }
139
+
140
+ return result;
141
+ }
142
+
143
+ /**
144
+ * Extract props interface and type aliases from a component file
145
+ * @param {string} filePath - Path to the component file
146
+ * @returns {{ props: Object, typeAliases: Object, usageExample: Object|null, enums: Object, genericTypes: Object }}
147
+ */
148
+ function extractComponentDetails(filePath) {
149
+ const result = {
150
+ props: {},
151
+ typeAliases: {},
152
+ usageExample: null,
153
+ enums: {},
154
+ genericTypes: {}
155
+ };
156
+
157
+ if (!fs.existsSync(filePath)) return result;
158
+
159
+ try {
160
+ const content = fs.readFileSync(filePath, 'utf-8');
161
+
162
+ // Extract enums (e.g., enum Status { Active = 'active', Inactive = 'inactive' })
163
+ const enumMatches = content.matchAll(/enum\s+(\w+)\s*\{([^}]+)\}/g);
164
+ for (const match of enumMatches) {
165
+ const enumName = match[1];
166
+ const enumBody = match[2];
167
+
168
+ // Extract enum values
169
+ const valueMatches = enumBody.matchAll(/(\w+)\s*=\s*['"]([^'"]+)['"]/g);
170
+ const values = [];
171
+ for (const vm of valueMatches) {
172
+ values.push(vm[2]);
173
+ }
174
+
175
+ // Also handle simple enums without explicit values
176
+ if (values.length === 0) {
177
+ const simpleValues = enumBody.match(/\b(\w+)\b(?=\s*[,}])/g);
178
+ if (simpleValues) {
179
+ values.push(...simpleValues.filter(v => v !== 'const'));
180
+ }
181
+ }
182
+
183
+ if (values.length > 0) {
184
+ result.enums[enumName] = values;
185
+ result.typeAliases[enumName] = values; // Also expose as type alias
186
+ }
187
+ }
188
+
189
+ // Extract type aliases - handle multiple patterns
190
+ // Pattern 1: type X = 'a' | 'b' | 'c' (string literal union)
191
+ const stringUnionMatches = content.matchAll(/type\s+(\w+)\s*=\s*(['"][^'"]+['"](?:\s*\|\s*['"][^'"]+['"])*)/g);
192
+ for (const match of stringUnionMatches) {
193
+ const typeName = match[1];
194
+ const typeValue = match[2];
195
+ const literalMatches = typeValue.match(/['"]([^'"]+)['"]/g);
196
+ if (literalMatches) {
197
+ result.typeAliases[typeName] = literalMatches.map(v => v.replace(/['"]/g, ''));
198
+ }
199
+ }
200
+
201
+ // Pattern 2: type X = number | string | boolean (primitive union)
202
+ const primitiveUnionMatches = content.matchAll(/type\s+(\w+)\s*=\s*((?:string|number|boolean|null|undefined)(?:\s*\|\s*(?:string|number|boolean|null|undefined))*)/g);
203
+ for (const match of primitiveUnionMatches) {
204
+ result.typeAliases[match[1]] = [match[2]]; // Store as single value representing the union
205
+ }
206
+
207
+ // Pattern 3: type X = typeof Y[number] (indexed access types)
208
+ const indexedAccessMatches = content.matchAll(/type\s+(\w+)\s*=\s*typeof\s+(\w+)\[(?:number|'[^']+')?\]/g);
209
+ for (const match of indexedAccessMatches) {
210
+ const typeName = match[1];
211
+ const sourceArray = match[2];
212
+ // Link to the array type alias if we found it
213
+ if (result.typeAliases[`_array_${sourceArray}`]) {
214
+ result.typeAliases[typeName] = result.typeAliases[`_array_${sourceArray}`];
215
+ }
216
+ }
217
+
218
+ // Pattern 4: type X<T> = ... (generic type definitions)
219
+ const genericTypeMatches = content.matchAll(/type\s+(\w+)<([^>]+)>\s*=\s*([^;\n]+)/g);
220
+ for (const match of genericTypeMatches) {
221
+ result.genericTypes[match[1]] = {
222
+ params: match[2].split(',').map(p => p.trim()),
223
+ definition: match[3].trim()
224
+ };
225
+ }
226
+
227
+ // Pattern 5: type Props = { ... } (object type alias - treat like interface)
228
+ const typeObjectMatches = content.matchAll(/type\s+(\w+Props)\s*=\s*\{/g);
229
+ for (const match of typeObjectMatches) {
230
+ const typeName = match[1];
231
+ const startIndex = match.index + match[0].length;
232
+
233
+ let braceCount = 1;
234
+ let endIndex = startIndex;
235
+ while (braceCount > 0 && endIndex < content.length) {
236
+ if (content[endIndex] === '{') braceCount++;
237
+ if (content[endIndex] === '}') braceCount--;
238
+ endIndex++;
239
+ }
240
+
241
+ const propsBody = content.slice(startIndex, endIndex - 1);
242
+ extractPropsFromBody(propsBody, result.props);
243
+ }
244
+
245
+ // Also check for "as const" arrays that define variants
246
+ // e.g., const buttonVariants = ['primary', 'secondary'] as const
247
+ const constArrayMatches = content.matchAll(/(?:export\s+)?const\s+(\w+)\s*=\s*\[([^\]]+)\]\s*(?:as\s+const)?/g);
248
+ for (const match of constArrayMatches) {
249
+ const constName = match[1];
250
+ const arrayContent = match[2];
251
+
252
+ const literalMatches = arrayContent.match(/['"]([^'"]+)['"]/g);
253
+ if (literalMatches) {
254
+ const values = literalMatches.map(v => v.replace(/['"]/g, ''));
255
+ // Store as a pseudo-type for reference
256
+ result.typeAliases[`_array_${constName}`] = values;
257
+ }
258
+ }
259
+
260
+ // Extract props interfaces - handle nested braces with balanced matching
261
+ // Match: interface XxxProps { ... } or interface XxxProps extends YYY { ... }
262
+ const propsInterfaceRegex = /interface\s+(\w+Props)\s*(?:<[^>]+>)?\s*(?:extends[^{]+)?\{/g;
263
+ let propsMatch;
264
+ while ((propsMatch = propsInterfaceRegex.exec(content)) !== null) {
265
+ const startIndex = propsMatch.index + propsMatch[0].length;
266
+
267
+ // Find matching closing brace with balanced brace counting
268
+ let braceCount = 1;
269
+ let endIndex = startIndex;
270
+ while (braceCount > 0 && endIndex < content.length) {
271
+ if (content[endIndex] === '{') braceCount++;
272
+ if (content[endIndex] === '}') braceCount--;
273
+ endIndex++;
274
+ }
275
+
276
+ const propsBody = content.slice(startIndex, endIndex - 1);
277
+ extractPropsFromBody(propsBody, result.props);
278
+ }
279
+
280
+ // Extract React.FC<Props> or FC<Props> style component definitions
281
+ const fcPropsMatches = content.matchAll(/(?:React\.)?FC<(\w+)>/g);
282
+ for (const match of fcPropsMatches) {
283
+ const propsTypeName = match[1];
284
+ // Mark that this type is used as component props
285
+ result.typeAliases[`_fcProps_${propsTypeName}`] = propsTypeName;
286
+ }
287
+
288
+ } catch (err) {
289
+ // Ignore read errors
290
+ }
291
+
292
+ return result;
293
+ }
294
+
295
+ /**
296
+ * Extract props from an interface/type body
297
+ * @param {string} propsBody - The body content between braces
298
+ * @param {Object} propsTarget - Target object to store extracted props
299
+ */
300
+ function extractPropsFromBody(propsBody, propsTarget) {
301
+ // Parse each prop line - handle multi-line types
302
+ const lines = propsBody.split('\n');
303
+ let currentProp = '';
304
+
305
+ for (const line of lines) {
306
+ const trimmed = line.trim();
307
+
308
+ // Skip comments
309
+ if (trimmed.startsWith('//') || trimmed.startsWith('/*')) continue;
310
+
311
+ currentProp += ' ' + trimmed;
312
+
313
+ // Check if we have a complete property (ends with ; or has balanced brackets)
314
+ const hasComplete = trimmed.endsWith(';') ||
315
+ (currentProp.split('{').length === currentProp.split('}').length &&
316
+ currentProp.split('<').length === currentProp.split('>').length);
317
+
318
+ if (hasComplete && currentProp.includes(':')) {
319
+ // Parse the accumulated property
320
+ const propMatch = currentProp.match(/^\s*(\w+)(\?)?:\s*(.+?)(?:;|$)/);
321
+ if (propMatch) {
322
+ const propName = propMatch[1];
323
+ const isOptional = !!propMatch[2];
324
+ let propType = propMatch[3].trim();
325
+
326
+ // Skip internal props (starting with $ or _)
327
+ if (!propName.startsWith('$') && !propName.startsWith('_')) {
328
+ // Clean up type (remove comments, trailing semicolons)
329
+ propType = propType.replace(/\/\*.*?\*\//g, '').replace(/;$/, '').trim();
330
+
331
+ propsTarget[propName] = {
332
+ type: propType,
333
+ optional: isOptional
334
+ };
335
+ }
336
+ }
337
+ currentProp = '';
338
+ }
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Generate a usage example for a component
344
+ * @param {string} componentName - Name of the component
345
+ * @param {Object} props - Extracted props
346
+ * @param {Object} typeAliases - Type aliases for string literal unions
347
+ * @returns {{ jsx: string, propsInfo: string[] }}
348
+ */
349
+ function generateUsageExample(componentName, props, typeAliases) {
350
+ let example = `<${componentName}`;
351
+ const propsInfo = [];
352
+
353
+ // Important props to show in examples
354
+ const importantProps = ['variant', 'size', 'type', 'status', 'color', 'kind'];
355
+
356
+ for (const propName of importantProps) {
357
+ if (props[propName]) {
358
+ const propType = props[propName].type;
359
+
360
+ // Look up the type in our aliases
361
+ let values = typeAliases[propType];
362
+
363
+ // Also check for array-based variants
364
+ if (!values) {
365
+ // Try to find matching array (e.g., variant -> buttonVariants)
366
+ for (const [aliasName, aliasValues] of Object.entries(typeAliases)) {
367
+ if (aliasName.startsWith('_array_') &&
368
+ aliasName.toLowerCase().includes(propName.toLowerCase())) {
369
+ values = aliasValues;
370
+ break;
371
+ }
372
+ }
373
+ }
374
+
375
+ if (values && values.length > 0) {
376
+ const defaultValue = values[0];
377
+ example += ` ${propName}="${defaultValue}"`;
378
+ propsInfo.push(`${propName}="${values.join('" | "')}"`);
379
+ }
380
+ }
381
+ }
382
+
383
+ example += `>{children}</${componentName}>`;
384
+
385
+ return {
386
+ jsx: example,
387
+ propsInfo
388
+ };
389
+ }
390
+
391
+ /**
392
+ * Scan a component/module directory for exports and resolve import path
393
+ * @param {string} dirPath - Full path to the component directory
394
+ * @param {string} baseImportPath - Base import path (e.g., '@/components')
395
+ * @param {boolean} includeDetails - Whether to extract props and usage examples
396
+ * @returns {{ exports: string[], types: string[], importPath: string, defaultExport: string|null, arrayExports: string[], props: Object, usageExample: Object|null }|null}
397
+ */
398
+ function scanModuleExports(dirPath, baseImportPath, includeDetails = false) {
399
+ const dirName = path.basename(dirPath);
400
+
401
+ // Check for index file first
402
+ const indexFiles = ['index.ts', 'index.tsx', 'index.js', 'index.jsx'];
403
+ let mainFile = null;
404
+ let componentFile = null;
405
+
406
+ for (const indexFile of indexFiles) {
407
+ const indexPath = path.join(dirPath, indexFile);
408
+ if (fs.existsSync(indexPath)) {
409
+ mainFile = indexPath;
410
+ break;
411
+ }
412
+ }
413
+
414
+ // Also find the main component file for props extraction
415
+ const componentFiles = [`${dirName}.tsx`, `${dirName}.ts`, `${dirName}.jsx`, `${dirName}.js`];
416
+ for (const compFile of componentFiles) {
417
+ const compPath = path.join(dirPath, compFile);
418
+ if (fs.existsSync(compPath)) {
419
+ componentFile = compPath;
420
+ if (!mainFile) mainFile = compPath;
421
+ break;
422
+ }
423
+ }
424
+
425
+ if (!mainFile) return null;
426
+
427
+ const result = extractExports(mainFile);
428
+
429
+ const moduleResult = {
430
+ exports: [...new Set(result.namedExports)],
431
+ types: [...new Set(result.types)],
432
+ defaultExport: result.defaultExport,
433
+ arrayExports: [...new Set(result.arrayExports)],
434
+ importPath: `${baseImportPath}/${dirName}`
435
+ };
436
+
437
+ // Extract props and generate usage example if requested
438
+ if (includeDetails && componentFile) {
439
+ const details = extractComponentDetails(componentFile);
440
+ moduleResult.props = details.props;
441
+ moduleResult.typeAliases = details.typeAliases;
442
+
443
+ // Generate usage example
444
+ if (Object.keys(details.props).length > 0) {
445
+ moduleResult.usageExample = generateUsageExample(dirName, details.props, details.typeAliases);
446
+ }
447
+ }
448
+
449
+ return moduleResult;
450
+ }
451
+
452
+ /**
453
+ * Scan a single file (not a directory) for exports
454
+ * @param {string} filePath - Full path to the file
455
+ * @param {string} baseImportPath - Base import path
456
+ * @returns {{ exports: string[], types: string[], importPath: string, defaultExport: string|null }|null}
457
+ */
458
+ function scanFileExports(filePath, baseImportPath) {
459
+ if (!fs.existsSync(filePath)) return null;
460
+
461
+ const fileName = path.basename(filePath);
462
+ const fileNameWithoutExt = fileName.replace(/\.(tsx?|jsx?)$/, '');
463
+
464
+ const result = extractExports(filePath);
465
+
466
+ return {
467
+ exports: [...new Set(result.namedExports)],
468
+ types: [...new Set(result.types)],
469
+ defaultExport: result.defaultExport,
470
+ importPath: `${baseImportPath}/${fileNameWithoutExt}`
471
+ };
472
+ }
473
+
474
+ // ============================================================
475
+ // Export Map Building
476
+ // ============================================================
477
+
478
+ /**
479
+ * Build export map for all configured directories
480
+ * @param {object} config - Project config
481
+ * @returns {object} Export map with components, hooks, types, etc.
482
+ */
483
+ function buildExportMap(config) {
484
+ const projectContext = config.hybrid?.projectContext || {};
485
+ const exportMap = {
486
+ components: {},
487
+ hooks: {},
488
+ services: {},
489
+ types: {},
490
+ utils: {},
491
+ _meta: {
492
+ generatedAt: new Date().toISOString(),
493
+ projectRoot: PROJECT_ROOT
494
+ }
495
+ };
496
+
497
+ // Scan component directories (with details for usage examples)
498
+ const componentDirs = projectContext.componentDirs || ['src/components'];
499
+ for (const dir of componentDirs) {
500
+ const fullDir = path.join(PROJECT_ROOT, dir);
501
+ if (!fs.existsSync(fullDir)) continue;
502
+
503
+ // Include details (props, usage examples) for components
504
+ scanDirectory(fullDir, '@/components', exportMap.components, true);
505
+ }
506
+
507
+ // Scan hooks directory (use config if available, with glob support)
508
+ const hooksDirs = projectContext.hookDirs || ['src/hooks', 'hooks'];
509
+ for (const dir of hooksDirs) {
510
+ // Convert directory path to import path
511
+ // apps/web/src/features/auth/hooks -> @/features/auth/hooks
512
+ // src/hooks -> @/hooks
513
+ const importBase = dir
514
+ .replace(/^apps\/\w+\/src\//, '@/') // apps/web/src/ -> @/
515
+ .replace(/^src\//, '@/'); // src/ -> @/
516
+
517
+ // Handle glob patterns like src/hooks/*.ts
518
+ if (dir.includes('*')) {
519
+ const baseDir = dir.split('*')[0].replace(/\/$/, '');
520
+ const fullDir = path.join(PROJECT_ROOT, baseDir);
521
+ if (!fs.existsSync(fullDir)) continue;
522
+
523
+ scanDirectoryFlat(fullDir, importBase.split('*')[0].replace(/\/$/, ''), exportMap.hooks);
524
+ } else {
525
+ const fullDir = path.join(PROJECT_ROOT, dir);
526
+ if (!fs.existsSync(fullDir)) continue;
527
+
528
+ // Hooks can be individual files or directories
529
+ scanDirectoryFlat(fullDir, importBase, exportMap.hooks);
530
+ }
531
+ }
532
+
533
+ // Scan services directory
534
+ const servicesDirs = ['src/services', 'services', 'src/lib'];
535
+ for (const dir of servicesDirs) {
536
+ const fullDir = path.join(PROJECT_ROOT, dir);
537
+ if (!fs.existsSync(fullDir)) continue;
538
+
539
+ scanDirectoryFlat(fullDir, dir.startsWith('src/') ? `@/${dir.replace('src/', '')}` : `@/${dir}`, exportMap.services);
540
+ }
541
+
542
+ // Scan type directories
543
+ const typeDirs = projectContext.typeDirs || ['src/types'];
544
+ for (const dir of typeDirs) {
545
+ // Handle glob patterns like src/types/*.ts
546
+ if (dir.includes('*')) {
547
+ const baseDir = dir.split('*')[0].replace(/\/$/, '');
548
+ const fullDir = path.join(PROJECT_ROOT, baseDir);
549
+ if (!fs.existsSync(fullDir)) continue;
550
+
551
+ scanDirectoryFlat(fullDir, '@/types', exportMap.types, true);
552
+ } else {
553
+ const fullDir = path.join(PROJECT_ROOT, dir);
554
+ if (!fs.existsSync(fullDir)) continue;
555
+
556
+ scanDirectoryFlat(fullDir, '@/types', exportMap.types, true);
557
+ }
558
+ }
559
+
560
+ // Scan utils directory
561
+ const utilsDirs = ['src/utils', 'src/lib/utils', 'utils'];
562
+ for (const dir of utilsDirs) {
563
+ const fullDir = path.join(PROJECT_ROOT, dir);
564
+ if (!fs.existsSync(fullDir)) continue;
565
+
566
+ scanDirectoryFlat(fullDir, dir.startsWith('src/') ? `@/${dir.replace('src/', '')}` : `@/${dir}`, exportMap.utils);
567
+ }
568
+
569
+ return exportMap;
570
+ }
571
+
572
+ /**
573
+ * Scan a directory containing subdirectories (like src/components/)
574
+ * @param {boolean} includeDetails - Whether to extract props and usage examples
575
+ */
576
+ function scanDirectory(dirPath, baseImportPath, target, includeDetails = false) {
577
+ try {
578
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
579
+
580
+ for (const entry of entries) {
581
+ if (!entry.isDirectory()) continue;
582
+
583
+ // Skip common excluded directories
584
+ if (['__tests__', '__mocks__', 'node_modules', '.git'].includes(entry.name)) continue;
585
+
586
+ const modulePath = path.join(dirPath, entry.name);
587
+ const result = scanModuleExports(modulePath, baseImportPath, includeDetails);
588
+
589
+ if (result && (result.exports.length > 0 || result.defaultExport)) {
590
+ target[entry.name] = result;
591
+ }
592
+ }
593
+ } catch (err) {
594
+ // Ignore scan errors
595
+ }
596
+ }
597
+
598
+ /**
599
+ * Scan a directory containing files (like src/hooks/)
600
+ */
601
+ function scanDirectoryFlat(dirPath, baseImportPath, target, typesOnly = false) {
602
+ try {
603
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
604
+
605
+ for (const entry of entries) {
606
+ // Skip test files and common excludes
607
+ if (entry.name.includes('.test.') || entry.name.includes('.spec.')) continue;
608
+ if (entry.name.includes('.stories.')) continue;
609
+ if (entry.name === 'index.ts' || entry.name === 'index.js') continue;
610
+
611
+ const entryPath = path.join(dirPath, entry.name);
612
+
613
+ if (entry.isDirectory()) {
614
+ // Recurse into subdirectory
615
+ const result = scanModuleExports(entryPath, baseImportPath);
616
+ if (result && (result.exports.length > 0 || result.defaultExport || result.types.length > 0)) {
617
+ target[entry.name] = result;
618
+ }
619
+ } else if (entry.isFile() && /\.(tsx?|jsx?)$/.test(entry.name)) {
620
+ const result = scanFileExports(entryPath, baseImportPath);
621
+ if (result) {
622
+ const key = entry.name.replace(/\.(tsx?|jsx?)$/, '');
623
+ if (typesOnly) {
624
+ if (result.types.length > 0) {
625
+ target[key] = result;
626
+ }
627
+ } else if (result.exports.length > 0 || result.defaultExport) {
628
+ target[key] = result;
629
+ }
630
+ }
631
+ }
632
+ }
633
+ } catch (err) {
634
+ // Ignore scan errors
635
+ }
636
+ }
637
+
638
+ // ============================================================
639
+ // Caching
640
+ // ============================================================
641
+
642
+ /**
643
+ * Load cached export map if it's fresh
644
+ * @returns {object|null} Cached export map or null
645
+ */
646
+ function loadCachedExportMap() {
647
+ try {
648
+ if (!fs.existsSync(CACHE_PATH)) return null;
649
+
650
+ const stat = fs.statSync(CACHE_PATH);
651
+ const age = Date.now() - stat.mtimeMs;
652
+
653
+ if (age > CACHE_MAX_AGE_MS) return null;
654
+
655
+ return JSON.parse(fs.readFileSync(CACHE_PATH, 'utf-8'));
656
+ } catch {
657
+ return null;
658
+ }
659
+ }
660
+
661
+ /**
662
+ * Save export map to cache
663
+ * @param {object} exportMap - Export map to cache
664
+ */
665
+ function saveExportMapCache(exportMap) {
666
+ try {
667
+ const stateDir = path.dirname(CACHE_PATH);
668
+ if (!fs.existsSync(stateDir)) {
669
+ fs.mkdirSync(stateDir, { recursive: true });
670
+ }
671
+ fs.writeFileSync(CACHE_PATH, JSON.stringify(exportMap, null, 2));
672
+ } catch (err) {
673
+ console.error(`Warning: Could not cache export map: ${err.message}`);
674
+ }
675
+ }
676
+
677
+ /**
678
+ * Clear the export map cache
679
+ */
680
+ function clearCache() {
681
+ try {
682
+ if (fs.existsSync(CACHE_PATH)) {
683
+ fs.unlinkSync(CACHE_PATH);
684
+ console.log('✓ Cleared export map cache');
685
+ }
686
+ } catch (err) {
687
+ console.error(`Warning: Could not clear cache: ${err.message}`);
688
+ }
689
+ }
690
+
691
+ // ============================================================
692
+ // Formatting for Templates
693
+ // ============================================================
694
+
695
+ /**
696
+ * Format export map as markdown for templates
697
+ * @param {object} exportMap - Export map
698
+ * @returns {string} Markdown-formatted export list
699
+ */
700
+ function formatExportMapForTemplate(exportMap) {
701
+ const lines = [];
702
+
703
+ // Components
704
+ if (Object.keys(exportMap.components).length > 0) {
705
+ lines.push('#### Components');
706
+ for (const [name, info] of Object.entries(exportMap.components)) {
707
+ const exports = info.exports.join(', ') || (info.defaultExport ? `default: ${info.defaultExport}` : '');
708
+ if (exports) {
709
+ lines.push(`- \`import { ${info.exports.join(', ')} } from '${info.importPath}'\``);
710
+ }
711
+ }
712
+ lines.push('');
713
+ }
714
+
715
+ // Hooks
716
+ if (Object.keys(exportMap.hooks).length > 0) {
717
+ lines.push('#### Hooks');
718
+ for (const [name, info] of Object.entries(exportMap.hooks)) {
719
+ const exports = info.exports.join(', ');
720
+ if (exports) {
721
+ lines.push(`- \`import { ${exports} } from '${info.importPath}'\``);
722
+ }
723
+ }
724
+ lines.push('');
725
+ }
726
+
727
+ // Services
728
+ if (Object.keys(exportMap.services).length > 0) {
729
+ lines.push('#### Services');
730
+ for (const [name, info] of Object.entries(exportMap.services)) {
731
+ const exports = info.exports.join(', ');
732
+ if (exports) {
733
+ lines.push(`- \`import { ${exports} } from '${info.importPath}'\``);
734
+ }
735
+ }
736
+ lines.push('');
737
+ }
738
+
739
+ // Types
740
+ if (Object.keys(exportMap.types).length > 0) {
741
+ lines.push('#### Types');
742
+ for (const [name, info] of Object.entries(exportMap.types)) {
743
+ const types = info.types.join(', ');
744
+ if (types) {
745
+ lines.push(`- \`import type { ${types} } from '${info.importPath}'\``);
746
+ }
747
+ }
748
+ lines.push('');
749
+ }
750
+
751
+ // Utils
752
+ if (Object.keys(exportMap.utils).length > 0) {
753
+ lines.push('#### Utilities');
754
+ for (const [name, info] of Object.entries(exportMap.utils)) {
755
+ const exports = info.exports.join(', ');
756
+ if (exports) {
757
+ lines.push(`- \`import { ${exports} } from '${info.importPath}'\``);
758
+ }
759
+ }
760
+ lines.push('');
761
+ }
762
+
763
+ return lines.join('\n');
764
+ }
765
+
766
+ // ============================================================
767
+ // Component Usage Validation
768
+ // ============================================================
769
+
770
+ /**
771
+ * Validate component usage patterns in generated code
772
+ * @param {string} code - Generated code to validate
773
+ * @param {object} exportMap - Export map with array export info
774
+ * @returns {{ valid: boolean, errors: string[], warnings: string[] }}
775
+ */
776
+ function validateComponentUsage(code, exportMap = null) {
777
+ const errors = [];
778
+ const warnings = [];
779
+
780
+ // Load export map if not provided
781
+ if (!exportMap) {
782
+ exportMap = loadCachedExportMap();
783
+ if (!exportMap) {
784
+ return { valid: true, errors: [], warnings: ['No export map available for validation'] };
785
+ }
786
+ }
787
+
788
+ // Collect all array exports from components
789
+ const arrayExports = new Set();
790
+ for (const [name, info] of Object.entries(exportMap.components || {})) {
791
+ if (info.arrayExports) {
792
+ info.arrayExports.forEach(e => arrayExports.add(e));
793
+ }
794
+ }
795
+
796
+ // Check for array-as-object access patterns
797
+ // e.g., cardVariants.default, buttonVariants.primary
798
+ const arrayAccessPattern = /(\w+(?:Variants|Sizes|Statuses|Options))\.(\w+)/g;
799
+ const matches = code.matchAll(arrayAccessPattern);
800
+
801
+ for (const match of matches) {
802
+ const exportName = match[1];
803
+ const accessedProp = match[2];
804
+
805
+ // If this is a known array export, it's wrong to access it as an object
806
+ if (arrayExports.has(exportName)) {
807
+ errors.push(
808
+ `Invalid usage: "${match[0]}" - ${exportName} is an ARRAY, not an object. ` +
809
+ `Use string literal: "${accessedProp}" instead of ${exportName}.${accessedProp}`
810
+ );
811
+ } else {
812
+ // Even if not in our export map, warn about common patterns
813
+ warnings.push(
814
+ `Suspicious pattern: "${match[0]}" - ${exportName} is likely an array. ` +
815
+ `Consider using string literal: "${accessedProp}"`
816
+ );
817
+ }
818
+ }
819
+
820
+ // Check for variant/size/type props using object access instead of string literals
821
+ // e.g., variant={buttonVariants.primary} instead of variant="primary"
822
+ const propObjectPattern = /(?:variant|size|type|status)=\{(\w+(?:Variants|Sizes|Types|Statuses))\.(\w+)\}/g;
823
+ const propMatches = code.matchAll(propObjectPattern);
824
+
825
+ for (const match of propMatches) {
826
+ const exportName = match[1];
827
+ const value = match[2];
828
+ errors.push(
829
+ `Invalid prop usage: "${match[0]}" - Use string literal instead: ` +
830
+ `variant="${value}" (NOT {${exportName}.${value}})`
831
+ );
832
+ }
833
+
834
+ // Check for hook file name vs export name mismatches
835
+ // Common pattern: use-auth-store.ts exports useAuthState, not useAuthStore
836
+ const hookPatterns = [
837
+ { pattern: /useAuthStore\(\)/g, suggestion: 'useAuthState()' },
838
+ { pattern: /useUserStore\(\)/g, suggestion: 'useUserState()' },
839
+ { pattern: /useCartStore\(\)/g, suggestion: 'useCartState()' },
840
+ ];
841
+
842
+ for (const { pattern, suggestion } of hookPatterns) {
843
+ if (pattern.test(code)) {
844
+ // Check if the actual export exists
845
+ const wrongName = pattern.source.replace(/\\/g, '').replace(/\(\)/g, '');
846
+ let found = false;
847
+ for (const [name, info] of Object.entries(exportMap.hooks || {})) {
848
+ if (info.exports?.includes(wrongName)) {
849
+ found = true;
850
+ break;
851
+ }
852
+ }
853
+ if (!found) {
854
+ warnings.push(
855
+ `Possible hook name mistake: Check if "${wrongName}" is the correct export name. ` +
856
+ `File names often differ from export names (e.g., use-auth-store.ts might export ${suggestion.replace('()', '')})`
857
+ );
858
+ }
859
+ }
860
+ }
861
+
862
+ return {
863
+ valid: errors.length === 0,
864
+ errors,
865
+ warnings
866
+ };
867
+ }
868
+
869
+ /**
870
+ * Format component with usage example for context
871
+ * @param {string} name - Component name
872
+ * @param {object} info - Component info from export map
873
+ * @returns {string} Formatted markdown
874
+ */
875
+ function formatComponentWithUsage(name, info) {
876
+ let output = `#### ${name}\n\n`;
877
+ output += '```typescript\n';
878
+ if (info.exports.length > 0) {
879
+ output += `import { ${info.exports.join(', ')} } from '${info.importPath}';\n`;
880
+ } else if (info.defaultExport) {
881
+ output += `import ${info.defaultExport} from '${info.importPath}';\n`;
882
+ }
883
+ output += '```\n\n';
884
+
885
+ // Show props table if available
886
+ if (info.props && Object.keys(info.props).length > 0) {
887
+ output += '**Props:**\n';
888
+
889
+ // Important props to show first (styling/behavior related)
890
+ const importantProps = ['variant', 'size', 'padding', 'status', 'type', 'color', 'disabled', 'checked', 'onChange', 'onClick', 'children'];
891
+ const shownProps = new Set();
892
+
893
+ // Show important props first
894
+ for (const propName of importantProps) {
895
+ if (info.props[propName]) {
896
+ const propInfo = info.props[propName];
897
+ const optional = propInfo.optional ? '?' : '';
898
+ let typeDisplay = propInfo.type;
899
+
900
+ // Resolve type alias to actual values if available
901
+ if (info.typeAliases && info.typeAliases[propInfo.type]) {
902
+ typeDisplay = `"${info.typeAliases[propInfo.type].join('" | "')}"`;
903
+ }
904
+
905
+ output += `- \`${propName}${optional}\`: ${typeDisplay}\n`;
906
+ shownProps.add(propName);
907
+ }
908
+ }
909
+
910
+ // Show remaining props (up to 3 more non-event, non-internal props)
911
+ let extraCount = 0;
912
+ for (const [propName, propInfo] of Object.entries(info.props)) {
913
+ if (shownProps.has(propName)) continue;
914
+ if (propName.startsWith('on') && propName !== 'onChange' && propName !== 'onClick') continue;
915
+ if (extraCount >= 3) break;
916
+
917
+ const optional = propInfo.optional ? '?' : '';
918
+ let typeDisplay = propInfo.type;
919
+
920
+ if (info.typeAliases && info.typeAliases[propInfo.type]) {
921
+ typeDisplay = `"${info.typeAliases[propInfo.type].join('" | "')}"`;
922
+ }
923
+
924
+ output += `- \`${propName}${optional}\`: ${typeDisplay}\n`;
925
+ extraCount++;
926
+ }
927
+
928
+ output += '\n';
929
+ }
930
+
931
+ // Add usage example
932
+ if (info.usageExample) {
933
+ output += '**Usage:**\n```tsx\n';
934
+ output += info.usageExample.jsx + '\n';
935
+ output += '```\n\n';
936
+ }
937
+
938
+ // Add warning about array exports
939
+ if (info.arrayExports && info.arrayExports.length > 0) {
940
+ output += `⚠️ \`${info.arrayExports.join('`, `')}\` are arrays for iteration, NOT objects.\n\n`;
941
+ }
942
+
943
+ return output;
944
+ }
945
+
946
+ // ============================================================
947
+ // CLI
948
+ // ============================================================
949
+
950
+ function printUsage() {
951
+ console.log(`
952
+ Wogi Flow - Export Scanner
953
+
954
+ Scans your project for TypeScript/JavaScript exports to build an accurate
955
+ import map for the local LLM. This ensures generated code uses only valid imports.
956
+
957
+ Usage:
958
+ node flow-export-scanner.js [project-root]
959
+ node flow-export-scanner.js --cache # Use cached map if fresh (5 min)
960
+ node flow-export-scanner.js --clear # Clear the cache
961
+ node flow-export-scanner.js --format # Output formatted for templates
962
+
963
+ Output is saved to: .workflow/state/export-map.json
964
+ `);
965
+ }
966
+
967
+ // Main - CLI execution
968
+ if (require.main === module) {
969
+ // Set project root from CLI arg (only when running directly as CLI)
970
+ const cliRoot = process.argv[2] && !process.argv[2].startsWith('--')
971
+ ? path.resolve(process.argv[2])
972
+ : process.cwd();
973
+ setProjectRoot(cliRoot);
974
+
975
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
976
+ printUsage();
977
+ process.exit(0);
978
+ }
979
+
980
+ if (process.argv.includes('--clear')) {
981
+ clearCache();
982
+ process.exit(0);
983
+ }
984
+
985
+ const useCache = process.argv.includes('--cache');
986
+ const formatOutput = process.argv.includes('--format');
987
+
988
+ let exportMap;
989
+
990
+ if (useCache) {
991
+ exportMap = loadCachedExportMap();
992
+ if (exportMap) {
993
+ console.log('Using cached export map');
994
+ }
995
+ }
996
+
997
+ if (!exportMap) {
998
+ console.log('Scanning project exports...\n');
999
+ const config = loadConfig();
1000
+ exportMap = buildExportMap(config);
1001
+ saveExportMapCache(exportMap);
1002
+ }
1003
+
1004
+ // Report findings
1005
+ const componentCount = Object.keys(exportMap.components).length;
1006
+ const hookCount = Object.keys(exportMap.hooks).length;
1007
+ const serviceCount = Object.keys(exportMap.services).length;
1008
+ const typeCount = Object.keys(exportMap.types).length;
1009
+ const utilCount = Object.keys(exportMap.utils).length;
1010
+
1011
+ console.log(`Found exports:`);
1012
+ console.log(` Components: ${componentCount}`);
1013
+ console.log(` Hooks: ${hookCount}`);
1014
+ console.log(` Services: ${serviceCount}`);
1015
+ console.log(` Types: ${typeCount}`);
1016
+ console.log(` Utils: ${utilCount}`);
1017
+
1018
+ if (formatOutput) {
1019
+ console.log('\n--- Template Format ---\n');
1020
+ console.log(formatExportMapForTemplate(exportMap));
1021
+ }
1022
+
1023
+ console.log(`\n✓ Export map saved to ${CACHE_PATH}`);
1024
+ }
1025
+
1026
+ module.exports = {
1027
+ // Core scanning functions
1028
+ extractExports,
1029
+ extractComponentDetails,
1030
+ generateUsageExample,
1031
+ scanModuleExports,
1032
+ scanFileExports,
1033
+ buildExportMap,
1034
+ // Cache functions
1035
+ loadCachedExportMap,
1036
+ saveExportMapCache,
1037
+ clearCache,
1038
+ // Formatting functions
1039
+ formatExportMapForTemplate,
1040
+ validateComponentUsage,
1041
+ formatComponentWithUsage,
1042
+ // Configuration functions (for use as module)
1043
+ setProjectRoot,
1044
+ getProjectRoot,
1045
+ loadConfig
1046
+ };