@flink-app/flink 0.14.3 → 2.0.0-alpha.100

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 (280) hide show
  1. package/CHANGELOG.md +1051 -0
  2. package/SCHEMA_EXTRACTION_ANALYSIS.md +494 -0
  3. package/SIMPLE_AST_FEASIBILITY.md +570 -0
  4. package/bin/flink.ts +13 -2
  5. package/cli/build.ts +24 -44
  6. package/cli/clean.ts +13 -25
  7. package/cli/cli-utils.ts +190 -17
  8. package/cli/dev.ts +252 -0
  9. package/cli/loadEnvFiles.ts +116 -0
  10. package/cli/run.ts +45 -62
  11. package/dist/bin/flink.js +61 -2
  12. package/dist/cli/build.js +20 -25
  13. package/dist/cli/clean.js +12 -10
  14. package/dist/cli/cli-utils.d.ts +34 -3
  15. package/dist/cli/cli-utils.js +193 -12
  16. package/dist/cli/dev.d.ts +2 -0
  17. package/dist/cli/dev.js +279 -0
  18. package/dist/cli/loadEnvFiles.d.ts +30 -0
  19. package/dist/cli/loadEnvFiles.js +113 -0
  20. package/dist/cli/run.js +47 -46
  21. package/dist/src/DependencyTracker.d.ts +44 -0
  22. package/dist/src/DependencyTracker.js +239 -0
  23. package/dist/src/FlinkApp.d.ts +163 -10
  24. package/dist/src/FlinkApp.js +847 -184
  25. package/dist/src/FlinkContext.d.ts +41 -0
  26. package/dist/src/FlinkErrors.d.ts +19 -6
  27. package/dist/src/FlinkErrors.js +36 -42
  28. package/dist/src/FlinkHttpHandler.d.ts +219 -26
  29. package/dist/src/FlinkHttpHandler.js +37 -1
  30. package/dist/src/FlinkJob.d.ts +10 -0
  31. package/dist/src/FlinkLog.d.ts +82 -18
  32. package/dist/src/FlinkLog.js +165 -13
  33. package/dist/src/FlinkLogFactory.d.ts +288 -0
  34. package/dist/src/FlinkLogFactory.js +619 -0
  35. package/dist/src/FlinkRepo.d.ts +10 -2
  36. package/dist/src/FlinkRepo.js +11 -1
  37. package/dist/src/FlinkRequestContext.d.ts +63 -0
  38. package/dist/src/FlinkRequestContext.js +74 -0
  39. package/dist/src/FlinkResponse.d.ts +6 -0
  40. package/dist/src/FlinkService.d.ts +38 -0
  41. package/dist/src/FlinkService.js +46 -0
  42. package/dist/src/LeaderElection.d.ts +45 -0
  43. package/dist/src/LeaderElection.js +269 -0
  44. package/dist/src/SchemaCache.d.ts +84 -0
  45. package/dist/src/SchemaCache.js +289 -0
  46. package/dist/src/TypeScriptCompiler.d.ts +161 -51
  47. package/dist/src/TypeScriptCompiler.js +1253 -617
  48. package/dist/src/TypeScriptUtils.js +4 -0
  49. package/dist/src/ai/AgentRunner.d.ts +39 -0
  50. package/dist/src/ai/AgentRunner.js +760 -0
  51. package/dist/src/ai/ConversationAgent.d.ts +279 -0
  52. package/dist/src/ai/ConversationAgent.js +404 -0
  53. package/dist/src/ai/ConversationFlinkAgent.d.ts +278 -0
  54. package/dist/src/ai/ConversationFlinkAgent.js +404 -0
  55. package/dist/src/ai/FlinkAgent.d.ts +690 -0
  56. package/dist/src/ai/FlinkAgent.js +729 -0
  57. package/dist/src/ai/FlinkTool.d.ts +135 -0
  58. package/dist/src/ai/FlinkTool.js +2 -0
  59. package/dist/src/ai/InMemoryConversationAgent.d.ts +121 -0
  60. package/dist/src/ai/InMemoryConversationAgent.js +209 -0
  61. package/dist/src/ai/LLMAdapter.d.ts +148 -0
  62. package/dist/src/ai/LLMAdapter.js +2 -0
  63. package/dist/src/ai/PersistentFlinkAgent.d.ts +278 -0
  64. package/dist/src/ai/PersistentFlinkAgent.js +403 -0
  65. package/dist/src/ai/SubAgentExecutor.d.ts +38 -0
  66. package/dist/src/ai/SubAgentExecutor.js +223 -0
  67. package/dist/src/ai/ToolExecutor.d.ts +64 -0
  68. package/dist/src/ai/ToolExecutor.js +497 -0
  69. package/dist/src/ai/agentInstructions.d.ts +68 -0
  70. package/dist/src/ai/agentInstructions.js +286 -0
  71. package/dist/src/ai/index.d.ts +8 -0
  72. package/dist/src/ai/index.js +26 -0
  73. package/dist/src/ai/instructionFileLoader.d.ts +44 -0
  74. package/dist/src/ai/instructionFileLoader.js +179 -0
  75. package/dist/src/auth/FlinkAuthPlugin.d.ts +1 -1
  76. package/dist/src/handlers/StreamWriterFactory.d.ts +20 -0
  77. package/dist/src/handlers/StreamWriterFactory.js +83 -0
  78. package/dist/src/index.d.ts +14 -0
  79. package/dist/src/index.js +17 -0
  80. package/dist/src/loadPluginSchemas.d.ts +45 -0
  81. package/dist/src/loadPluginSchemas.js +143 -0
  82. package/dist/src/schema-extraction/ComplexTypeDetection.d.ts +40 -0
  83. package/dist/src/schema-extraction/ComplexTypeDetection.js +75 -0
  84. package/dist/src/schema-extraction/TypeScriptSourceParser.d.ts +321 -0
  85. package/dist/src/schema-extraction/TypeScriptSourceParser.js +925 -0
  86. package/dist/src/schema-extraction/TypeScriptSourceParser.spec.d.ts +1 -0
  87. package/dist/src/schema-extraction/TypeScriptSourceParser.spec.js +233 -0
  88. package/dist/src/schema-extraction/TypeScriptTokenizer.d.ts +57 -0
  89. package/dist/src/schema-extraction/TypeScriptTokenizer.js +177 -0
  90. package/dist/src/schema-extraction/index.d.ts +2 -0
  91. package/dist/src/schema-extraction/index.js +20 -0
  92. package/dist/src/schema-extraction/types.d.ts +31 -0
  93. package/dist/src/schema-extraction/types.js +2 -0
  94. package/dist/src/utils/loadFlinkConfig.d.ts +53 -0
  95. package/dist/src/utils/loadFlinkConfig.js +77 -0
  96. package/dist/src/utils.d.ts +30 -0
  97. package/dist/src/utils.js +52 -0
  98. package/dist/src/workers/SchemaGeneratorWorker.d.ts +1 -0
  99. package/dist/src/workers/SchemaGeneratorWorker.js +49 -0
  100. package/dist/src/workers/WorkerPool.d.ts +60 -0
  101. package/dist/src/workers/WorkerPool.js +306 -0
  102. package/examples/logging-hierarchical-example.ts +125 -0
  103. package/package.json +29 -4
  104. package/readme.md +499 -0
  105. package/spec/AgentDescendantDetection.spec.ts +335 -0
  106. package/spec/AgentDuplicateDetection.spec.ts +112 -0
  107. package/spec/AgentObserver.spec.ts +266 -0
  108. package/spec/AgentRunner.spec.ts +1062 -0
  109. package/spec/AsyncLocalStorageContext.spec.ts +223 -0
  110. package/spec/ConversationHooks.spec.ts +257 -0
  111. package/spec/FlinkAgent.spec.ts +681 -0
  112. package/spec/FlinkApp.htmlResponse.spec.ts +260 -0
  113. package/spec/FlinkApp.onError.invocation.spec.ts +151 -0
  114. package/spec/FlinkApp.onError.spec.ts +1 -2
  115. package/spec/FlinkApp.query.spec.ts +107 -0
  116. package/spec/FlinkApp.routeOrdering.spec.ts +61 -0
  117. package/spec/FlinkApp.undefinedResponse.spec.ts +123 -0
  118. package/spec/FlinkApp.validationMode.spec.ts +155 -0
  119. package/spec/FlinkJob.spec.ts +171 -0
  120. package/spec/FlinkLogFactory.spec.ts +337 -0
  121. package/spec/FlinkRepo.spec.ts +1 -1
  122. package/spec/LeaderElection.spec.ts +174 -0
  123. package/spec/StreamingIntegration.spec.ts +139 -0
  124. package/spec/ToolExecutor.spec.ts +465 -0
  125. package/spec/TypeScriptCompiler.spec.ts +1 -1
  126. package/spec/TypeScriptSourceParser.spec.ts +1215 -0
  127. package/spec/TypeScriptTokenizer.spec.ts +366 -0
  128. package/spec/ai/ContextCompaction.spec.ts +405 -0
  129. package/spec/ai/ConversationAgent.spec.ts +520 -0
  130. package/spec/ai/InMemoryConversationAgent.spec.ts +144 -0
  131. package/spec/ai/agentInstructions.spec.ts +358 -0
  132. package/spec/fixtures/agent-instructions/TestAgent.ts +24 -0
  133. package/spec/fixtures/agent-instructions/simple.md +3 -0
  134. package/spec/fixtures/agent-instructions/template.md +18 -0
  135. package/spec/fixtures/agent-instructions/yaml-format.yaml +9 -0
  136. package/spec/mock-project/dist/.tsbuildinfo +1 -0
  137. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar.js +56 -0
  138. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar2.js +58 -0
  139. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema.js +52 -0
  140. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema2.js +52 -0
  141. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema3.js +52 -0
  142. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema.js +54 -0
  143. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema2.js +54 -0
  144. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile.js +57 -0
  145. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile2.js +57 -0
  146. package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler.js +53 -0
  147. package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler2.js +55 -0
  148. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchCar.js +57 -0
  149. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOnboardingSession.js +75 -0
  150. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOrderWithComplexTypes.js +57 -0
  151. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchProductWithIntersection.js +58 -0
  152. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchUserWithUnion.js +58 -0
  153. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostCar.js +54 -0
  154. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogin.js +55 -0
  155. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogout.js +54 -0
  156. package/spec/mock-project/dist/spec/mock-project/src/handlers/PutCar.js +54 -0
  157. package/spec/mock-project/dist/spec/mock-project/src/index.js +83 -0
  158. package/spec/mock-project/dist/spec/mock-project/src/repos/CarRepo.js +26 -0
  159. package/spec/mock-project/dist/spec/mock-project/src/schemas/Car.js +2 -0
  160. package/spec/mock-project/dist/spec/mock-project/src/schemas/DefaultExportSchema.js +2 -0
  161. package/spec/mock-project/dist/spec/mock-project/src/schemas/FileWithTwoSchemas.js +2 -0
  162. package/spec/mock-project/dist/src/FlinkApp.js +1000 -0
  163. package/spec/mock-project/dist/src/FlinkContext.js +2 -0
  164. package/spec/mock-project/dist/src/FlinkErrors.js +143 -0
  165. package/spec/mock-project/dist/src/FlinkHttpHandler.js +47 -0
  166. package/spec/mock-project/dist/src/FlinkJob.js +2 -0
  167. package/spec/mock-project/dist/src/FlinkLog.js +119 -0
  168. package/spec/mock-project/dist/src/FlinkLogFactory.js +617 -0
  169. package/spec/mock-project/dist/src/FlinkPlugin.js +2 -0
  170. package/spec/mock-project/dist/src/FlinkRepo.js +224 -0
  171. package/spec/mock-project/dist/src/FlinkRequestContext.js +74 -0
  172. package/spec/mock-project/dist/src/FlinkResponse.js +2 -0
  173. package/spec/mock-project/dist/src/ai/AgentExecutor.js +279 -0
  174. package/spec/mock-project/dist/src/ai/AgentRunner.js +632 -0
  175. package/spec/mock-project/dist/src/ai/ConversationAgent.js +402 -0
  176. package/spec/mock-project/dist/src/ai/ConversationFlinkAgent.js +422 -0
  177. package/spec/mock-project/dist/src/ai/FlinkAgent.js +699 -0
  178. package/spec/mock-project/dist/src/ai/FlinkTool.js +2 -0
  179. package/spec/mock-project/dist/src/ai/InMemoryConversationAgent.js +209 -0
  180. package/spec/mock-project/dist/src/ai/LLMAdapter.js +2 -0
  181. package/spec/mock-project/dist/src/ai/SubAgentExecutor.js +223 -0
  182. package/spec/mock-project/dist/src/ai/ToolExecutor.js +412 -0
  183. package/spec/mock-project/dist/src/ai/agentInstructions.js +246 -0
  184. package/spec/mock-project/dist/src/auth/FlinkAuthPlugin.js +2 -0
  185. package/spec/mock-project/dist/src/auth/FlinkAuthUser.js +2 -0
  186. package/spec/mock-project/dist/src/handlers/GetCar.js +26 -52
  187. package/spec/mock-project/dist/src/handlers/GetCar.js.map +1 -0
  188. package/spec/mock-project/dist/src/handlers/GetCar2.js +32 -54
  189. package/spec/mock-project/dist/src/handlers/GetCar2.js.map +1 -0
  190. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema.js +26 -48
  191. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema.js.map +1 -0
  192. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema2.js +28 -48
  193. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema2.js.map +1 -0
  194. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema3.js +29 -48
  195. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema3.js.map +1 -0
  196. package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema.js +26 -50
  197. package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema.js.map +1 -0
  198. package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema2.js +28 -50
  199. package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema2.js.map +1 -0
  200. package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile.js +27 -53
  201. package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile.js.map +1 -0
  202. package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile2.js +29 -53
  203. package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile2.js.map +1 -0
  204. package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler.js +16 -49
  205. package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler.js.map +1 -0
  206. package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler2.js +25 -50
  207. package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler2.js.map +1 -0
  208. package/spec/mock-project/dist/src/handlers/PatchCar.js +27 -53
  209. package/spec/mock-project/dist/src/handlers/PatchCar.js.map +1 -0
  210. package/spec/mock-project/dist/src/handlers/PatchOnboardingSession.js +44 -70
  211. package/spec/mock-project/dist/src/handlers/PatchOnboardingSession.js.map +1 -0
  212. package/spec/mock-project/dist/src/handlers/PatchOrderWithComplexTypes.js +27 -53
  213. package/spec/mock-project/dist/src/handlers/PatchOrderWithComplexTypes.js.map +1 -0
  214. package/spec/mock-project/dist/src/handlers/PatchProductWithIntersection.js +28 -54
  215. package/spec/mock-project/dist/src/handlers/PatchProductWithIntersection.js.map +1 -0
  216. package/spec/mock-project/dist/src/handlers/PatchUserWithUnion.js +28 -54
  217. package/spec/mock-project/dist/src/handlers/PatchUserWithUnion.js.map +1 -0
  218. package/spec/mock-project/dist/src/handlers/PostCar.js +24 -50
  219. package/spec/mock-project/dist/src/handlers/PostCar.js.map +1 -0
  220. package/spec/mock-project/dist/src/handlers/PostLogin.js +25 -51
  221. package/spec/mock-project/dist/src/handlers/PostLogin.js.map +1 -0
  222. package/spec/mock-project/dist/src/handlers/PostLogout.js +24 -50
  223. package/spec/mock-project/dist/src/handlers/PostLogout.js.map +1 -0
  224. package/spec/mock-project/dist/src/handlers/PutCar.js +24 -50
  225. package/spec/mock-project/dist/src/handlers/PutCar.js.map +1 -0
  226. package/spec/mock-project/dist/src/handlers/StreamWriterFactory.js +83 -0
  227. package/spec/mock-project/dist/src/index.js +52 -76
  228. package/spec/mock-project/dist/src/index.js.map +1 -0
  229. package/spec/mock-project/dist/src/mock-data-generator.js +9 -0
  230. package/spec/mock-project/dist/src/repos/CarRepo.js +12 -24
  231. package/spec/mock-project/dist/src/repos/CarRepo.js.map +1 -0
  232. package/spec/mock-project/dist/src/schemas/Car.js +3 -1
  233. package/spec/mock-project/dist/src/schemas/Car.js.map +1 -0
  234. package/spec/mock-project/dist/src/schemas/DefaultExportSchema.js +3 -1
  235. package/spec/mock-project/dist/src/schemas/DefaultExportSchema.js.map +1 -0
  236. package/spec/mock-project/dist/src/schemas/FileWithTwoSchemas.js +3 -1
  237. package/spec/mock-project/dist/src/schemas/FileWithTwoSchemas.js.map +1 -0
  238. package/spec/mock-project/dist/src/utils.js +290 -0
  239. package/spec/mock-project/tsconfig.json +6 -1
  240. package/spec/schema-generation-nested-objects.spec.ts +97 -0
  241. package/spec/testHelpers.ts +49 -0
  242. package/spec/utils.caseConversion.spec.ts +78 -0
  243. package/spec/utils.spec.ts +13 -13
  244. package/src/DependencyTracker.ts +166 -0
  245. package/src/FlinkApp.ts +919 -155
  246. package/src/FlinkContext.ts +43 -0
  247. package/src/FlinkErrors.ts +32 -12
  248. package/src/FlinkHttpHandler.ts +246 -28
  249. package/src/FlinkJob.ts +11 -0
  250. package/src/FlinkLog.ts +119 -12
  251. package/src/FlinkLogFactory.ts +699 -0
  252. package/src/FlinkRepo.ts +10 -3
  253. package/src/FlinkRequestContext.ts +95 -0
  254. package/src/FlinkResponse.ts +6 -0
  255. package/src/FlinkService.ts +49 -0
  256. package/src/LeaderElection.ts +203 -0
  257. package/src/SchemaCache.ts +232 -0
  258. package/src/TypeScriptCompiler.ts +1347 -610
  259. package/src/TypeScriptUtils.ts +5 -0
  260. package/src/ai/AgentRunner.ts +646 -0
  261. package/src/ai/ConversationAgent.ts +413 -0
  262. package/src/ai/FlinkAgent.ts +1069 -0
  263. package/src/ai/FlinkTool.ts +165 -0
  264. package/src/ai/InMemoryConversationAgent.ts +149 -0
  265. package/src/ai/LLMAdapter.ts +126 -0
  266. package/src/ai/ToolExecutor.ts +485 -0
  267. package/src/ai/agentInstructions.ts +245 -0
  268. package/src/ai/index.ts +8 -0
  269. package/src/ai/instructionFileLoader.ts +156 -0
  270. package/src/auth/FlinkAuthPlugin.ts +2 -1
  271. package/src/handlers/StreamWriterFactory.ts +84 -0
  272. package/src/index.ts +14 -0
  273. package/src/loadPluginSchemas.ts +141 -0
  274. package/src/schema-extraction/TypeScriptSourceParser.ts +1058 -0
  275. package/src/schema-extraction/TypeScriptTokenizer.ts +205 -0
  276. package/src/schema-extraction/index.ts +2 -0
  277. package/src/schema-extraction/types.ts +34 -0
  278. package/src/utils/loadFlinkConfig.ts +89 -0
  279. package/src/utils.ts +52 -0
  280. package/tsconfig.json +6 -1
@@ -0,0 +1,1058 @@
1
+ import { FlinkToolTypeArgs, PropertyMetadata, SchemaTypeDetection, TypeDefinition } from "./types";
2
+ import { TypeScriptTokenizer } from "./TypeScriptTokenizer";
3
+
4
+ /**
5
+ * Pure utility class for parsing TypeScript source code as text.
6
+ * All methods are static and side-effect free for easy testing.
7
+ *
8
+ * This class provides fast, regex-based parsing without using ts-morph
9
+ * or the TypeScript compiler, making it suitable for performance-critical
10
+ * schema extraction.
11
+ */
12
+ export class TypeScriptSourceParser {
13
+ /**
14
+ * Remove comments from source code to avoid false positives in schema detection.
15
+ * Handles both single-line (//) and multi-line (/* *\/) comments.
16
+ */
17
+ private static removeComments(sourceText: string): string {
18
+ // Remove multi-line comments: /* ... */
19
+ let text = sourceText.replace(/\/\*[\s\S]*?\*\//g, '');
20
+ // Remove single-line comments: // ...
21
+ text = text.replace(/\/\/.*$/gm, '');
22
+ return text;
23
+ }
24
+
25
+ /**
26
+ * Detect if a tool source file uses Zod or JSON schemas.
27
+ * If true, TypeScript schema extraction should be skipped.
28
+ *
29
+ * Note: Strips comments first to avoid false positives from commented-out schemas.
30
+ */
31
+ static detectSchemaType(sourceText: string): SchemaTypeDetection {
32
+ // Strip comments to avoid matching commented-out schema definitions
33
+ const textWithoutComments = this.removeComments(sourceText);
34
+
35
+ const hasZodSchemas = textWithoutComments.includes('inputSchema:') || textWithoutComments.includes('outputSchema:');
36
+ const hasJsonSchemas = textWithoutComments.includes('inputJsonSchema:') || textWithoutComments.includes('outputJsonSchema:');
37
+
38
+ return {
39
+ hasZodSchemas,
40
+ hasJsonSchemas,
41
+ shouldSkipTypeScriptExtraction: hasZodSchemas || hasJsonSchemas,
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Extract type arguments from Handler<Ctx, Req, Res, Params, Query> declaration.
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * parseHandlerTypeArgs('const handler: Handler<AppCtx, LoginReq, LoginRes>')
51
+ * // Returns: { contextType: 'AppCtx', reqType: 'LoginReq', resType: 'LoginRes' }
52
+ * ```
53
+ */
54
+ static parseHandlerTypeArgs(sourceText: string): { contextType: string; reqType?: string; resType?: string; paramsType?: string; queryType?: string } | null {
55
+ // Try to find Handler< or GetHandler< or PostHandler< etc.
56
+ const handlerMatch = sourceText.match(/\b(Get|Post|Put|Patch|Delete)?Handler\s*</);
57
+ if (!handlerMatch) {
58
+ return null;
59
+ }
60
+
61
+ const handlerType = handlerMatch[0].replace(/\s*<$/, '');
62
+ const startIdx = sourceText.indexOf(handlerMatch[0]);
63
+ if (startIdx === -1) {
64
+ return null;
65
+ }
66
+
67
+ // Find the matching closing > by counting bracket depth
68
+ let depth = 0;
69
+ let endIdx = -1;
70
+ for (let i = startIdx + handlerMatch[0].length; i < sourceText.length; i++) {
71
+ if (sourceText[i] === '<') {
72
+ depth++;
73
+ } else if (sourceText[i] === '>') {
74
+ if (depth === 0) {
75
+ endIdx = i;
76
+ break;
77
+ }
78
+ depth--;
79
+ }
80
+ }
81
+
82
+ if (endIdx === -1) {
83
+ return null;
84
+ }
85
+
86
+ const typeArgsText = sourceText.slice(startIdx + handlerMatch[0].length, endIdx);
87
+ const typeArgs = this.splitTopLevelCommas(typeArgsText);
88
+
89
+ if (typeArgs.length === 0) {
90
+ return null;
91
+ }
92
+
93
+ // Handler<Ctx, Req, Res, Params, Query>
94
+ // GetHandler<Ctx, Res, Params, Query> (no Req)
95
+ // PostHandler<Ctx, Req, Res, Params, Query>
96
+ // etc.
97
+
98
+ const isGetHandler = handlerType === 'GetHandler';
99
+
100
+ if (isGetHandler) {
101
+ // GetHandler<Ctx, Res, Params, Query>
102
+ return {
103
+ contextType: typeArgs[0]?.trim(),
104
+ resType: typeArgs[1]?.trim(),
105
+ paramsType: typeArgs[2]?.trim(),
106
+ queryType: typeArgs[3]?.trim(),
107
+ };
108
+ } else {
109
+ // Handler<Ctx, Req, Res, Params, Query>
110
+ return {
111
+ contextType: typeArgs[0]?.trim(),
112
+ reqType: typeArgs[1]?.trim(),
113
+ resType: typeArgs[2]?.trim(),
114
+ paramsType: typeArgs[3]?.trim(),
115
+ queryType: typeArgs[4]?.trim(),
116
+ };
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Extract type arguments from FlinkTool<Ctx, Input, Output> declaration.
122
+ *
123
+ * @example
124
+ * ```typescript
125
+ * parseFlinkToolTypeArgs('const handler: FlinkTool<Ctx, CreateDocInput, ToolResult<DocOutput>>')
126
+ * // Returns: { contextType: 'Ctx', inputType: 'CreateDocInput', outputType: 'ToolResult<DocOutput>' }
127
+ * ```
128
+ */
129
+ static parseFlinkToolTypeArgs(sourceText: string): FlinkToolTypeArgs | null {
130
+ // Find FlinkTool< and then manually parse to find the matching closing >
131
+ const startIdx = sourceText.indexOf('FlinkTool<');
132
+ if (startIdx === -1) {
133
+ return null;
134
+ }
135
+
136
+ // Find the matching closing > by counting bracket depth
137
+ let depth = 0;
138
+ let endIdx = -1;
139
+ for (let i = startIdx + 'FlinkTool<'.length; i < sourceText.length; i++) {
140
+ if (sourceText[i] === '<') {
141
+ depth++;
142
+ } else if (sourceText[i] === '>') {
143
+ if (depth === 0) {
144
+ endIdx = i;
145
+ break;
146
+ }
147
+ depth--;
148
+ }
149
+ }
150
+
151
+ if (endIdx === -1) {
152
+ return null;
153
+ }
154
+
155
+ // Extract the content between the brackets
156
+ const typeArgsStr = sourceText.substring(startIdx + 'FlinkTool<'.length, endIdx);
157
+
158
+ // Parse the type arguments by splitting on commas at the top level (not inside angle brackets)
159
+ const typeArgs = this.splitTopLevelCommas(typeArgsStr);
160
+
161
+ if (typeArgs.length < 3) {
162
+ return null;
163
+ }
164
+
165
+ return {
166
+ contextType: typeArgs[0].trim(),
167
+ inputType: typeArgs[1].trim(),
168
+ outputType: typeArgs[2].trim(),
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Split a string by commas, but only at the top level (not inside angle brackets).
174
+ * Helper for parsing generic type arguments like "Ctx, Input, ToolResult<Output>"
175
+ */
176
+ private static splitTopLevelCommas(str: string): string[] {
177
+ const result: string[] = [];
178
+ let current = '';
179
+ let depth = 0;
180
+
181
+ for (const char of str) {
182
+ if (char === '<') {
183
+ depth++;
184
+ current += char;
185
+ } else if (char === '>') {
186
+ depth--;
187
+ current += char;
188
+ } else if (char === ',' && depth === 0) {
189
+ result.push(current);
190
+ current = '';
191
+ } else {
192
+ current += char;
193
+ }
194
+ }
195
+
196
+ if (current) {
197
+ result.push(current);
198
+ }
199
+
200
+ return result;
201
+ }
202
+
203
+ /**
204
+ * Check if a type should generate a schema.
205
+ * Returns false for primitives and special types, true for named types and inline objects.
206
+ */
207
+ static shouldGenerateSchema(typeName: string): boolean {
208
+ // Skip 'any' and 'unknown'
209
+ if (typeName === 'any' || typeName === 'unknown') {
210
+ return false;
211
+ }
212
+
213
+ // Generate schemas for inline object types like {} or { foo: string }
214
+ if (typeName.startsWith('{')) {
215
+ return true;
216
+ }
217
+
218
+ // Skip primitive types
219
+ const primitives = ['string', 'number', 'boolean', 'void', 'undefined', 'null'];
220
+ if (primitives.includes(typeName.toLowerCase())) {
221
+ return false;
222
+ }
223
+
224
+ // Skip binary/non-serializable built-in types that can never have JSON schemas
225
+ const nonSerializableTypes = ['Buffer', 'ArrayBuffer', 'SharedArrayBuffer', 'Blob', 'ReadableStream', 'WritableStream', 'DataView', 'Uint8Array'];
226
+ if (nonSerializableTypes.includes(typeName)) {
227
+ return false;
228
+ }
229
+
230
+ // Must be a named type (starts with uppercase letter)
231
+ return /^[A-Z]/.test(typeName);
232
+ }
233
+
234
+ /**
235
+ * Check if a type is a built-in TypeScript type.
236
+ */
237
+ static isBuiltInType(typeName: string): boolean {
238
+ const builtIns = [
239
+ 'String', 'Number', 'Boolean', 'Date', 'Array', 'Object', 'Record',
240
+ 'Promise', 'Set', 'Map', 'Partial', 'Required', 'Pick', 'Omit',
241
+ 'Readonly', 'ReadonlyArray'
242
+ ];
243
+ return builtIns.includes(typeName);
244
+ }
245
+
246
+ /**
247
+ * Extract T from ToolResult<T> type name.
248
+ *
249
+ * @example
250
+ * ```typescript
251
+ * unwrapToolResultType('ToolResult<DocOutput>') // Returns: 'DocOutput'
252
+ * unwrapToolResultType('DocOutput') // Returns: 'DocOutput'
253
+ * ```
254
+ */
255
+ static unwrapToolResultType(typeName: string): string {
256
+ const match = typeName.match(/ToolResult<(.+)>/);
257
+ if (match) {
258
+ return match[1].trim();
259
+ }
260
+ return typeName;
261
+ }
262
+
263
+ /**
264
+ * Convert inline object type to interface definition.
265
+ *
266
+ * @example
267
+ * ```typescript
268
+ * convertInlineObjectToInterface('{}', 'MyInput')
269
+ * // Returns: 'export interface MyInput {}'
270
+ *
271
+ * convertInlineObjectToInterface('{ foo: string }', 'MyInput')
272
+ * // Returns: 'export interface MyInput {\n foo: string;\n}'
273
+ * ```
274
+ */
275
+ static convertInlineObjectToInterface(inlineType: string, interfaceName: string): string {
276
+ // For empty object, create empty interface
277
+ if (inlineType.trim() === '{}') {
278
+ return `export interface ${interfaceName} {}`;
279
+ }
280
+
281
+ // For non-empty inline object, convert to interface
282
+ // Remove outer braces and clean up
283
+ let body = inlineType.trim().slice(1, -1).trim();
284
+
285
+ // Ensure properties end with semicolons (convert commas to semicolons if needed)
286
+ body = body.replace(/,\s*$/g, ';').replace(/([^;])\s*$/g, '$1;');
287
+
288
+ return `export interface ${interfaceName} {\n ${body}\n}`;
289
+ }
290
+
291
+ /**
292
+ * Find an interface or type definition in source text.
293
+ *
294
+ * @example
295
+ * ```typescript
296
+ * const source = `
297
+ * interface User {
298
+ * name: string;
299
+ * }
300
+ * `;
301
+ * findTypeDefinition(source, 'User')
302
+ * // Returns: { name: 'User', definition: 'interface User { ... }', kind: 'interface' }
303
+ * ```
304
+ */
305
+ static findTypeDefinition(sourceText: string, typeName: string): TypeDefinition | null {
306
+ // Skip if it's an inline type
307
+ if (typeName.startsWith('{')) {
308
+ return null;
309
+ }
310
+
311
+ // Try to find interface definition first
312
+ // Pattern: (optional JSDoc) interface Name (optional generics) (optional extends) { ... }
313
+ const interfacePattern = new RegExp(
314
+ `((?:\\/\\*\\*[\\s\\S]*?\\*\\/\\s*)?)interface\\s+${typeName}\\s*`,
315
+ 'gm'
316
+ );
317
+
318
+ const interfaceMatch = interfacePattern.exec(sourceText);
319
+ if (interfaceMatch) {
320
+ const startIndex = interfaceMatch.index;
321
+ const jsDocAndInterface = interfaceMatch[0];
322
+
323
+ // Find the opening brace
324
+ const afterInterface = sourceText.slice(interfaceMatch.index + interfaceMatch[0].length);
325
+ const braceMatch = afterInterface.match(/^[^{]*\{/);
326
+
327
+ if (braceMatch) {
328
+ const braceStart = interfaceMatch.index + interfaceMatch[0].length + braceMatch[0].length - 1;
329
+
330
+ // Use tokenizer for robust bracket matching (handles strings and comments)
331
+ const tokenizer = new TypeScriptTokenizer(sourceText);
332
+ const braceEnd = tokenizer.findMatchingBracketAt(braceStart, '{');
333
+
334
+ if (braceEnd !== -1) {
335
+ const definition = sourceText.slice(startIndex, braceEnd + 1).trim();
336
+ return {
337
+ name: typeName,
338
+ definition,
339
+ kind: 'interface',
340
+ };
341
+ }
342
+ }
343
+ }
344
+
345
+ // Match type alias: type Name = ...;
346
+ const typePattern = new RegExp(
347
+ `((?:\\/\\*\\*[\\s\\S]*?\\*\\/\\s*)?)type\\s+${typeName}\\s*=\\s*`,
348
+ 'gm'
349
+ );
350
+
351
+ const typeMatch = typePattern.exec(sourceText);
352
+ if (typeMatch) {
353
+ const startIndex = typeMatch.index;
354
+ const afterEquals = typeMatch.index + typeMatch[0].length;
355
+
356
+ // Use tokenizer to find semicolon, properly handling strings and comments
357
+ const tokenizer = new TypeScriptTokenizer(sourceText);
358
+ let depth = 0;
359
+ let i = afterEquals;
360
+
361
+ while (i < sourceText.length) {
362
+ const token = tokenizer.getNextToken(i);
363
+
364
+ // Only process non-string, non-comment tokens
365
+ if (token.type !== 'string' && token.type !== 'comment') {
366
+ const char = token.value;
367
+
368
+ // Track depth
369
+ if (char === '{' || char === '[' || char === '(' || char === '<') {
370
+ depth++;
371
+ } else if (char === '}' || char === ']' || char === ')' || char === '>') {
372
+ depth--;
373
+
374
+ // Closing a bracket back to top level may terminate a
375
+ // semicolon-free type alias (e.g. `type Params = { ... }`
376
+ // with no trailing `;`). Only stop if the type does not
377
+ // continue with an operator such as `|` or `&`.
378
+ if (depth === 0 && !this.typeContinuesAfter(sourceText, token.endPos)) {
379
+ const definition = sourceText.slice(startIndex, token.endPos).trim();
380
+ return {
381
+ name: typeName,
382
+ definition,
383
+ kind: 'type',
384
+ };
385
+ }
386
+ }
387
+
388
+ // End of type at semicolon (at depth 0)
389
+ if (char === ';' && depth === 0) {
390
+ const definition = sourceText.slice(startIndex, i + 1).trim();
391
+ return {
392
+ name: typeName,
393
+ definition,
394
+ kind: 'type',
395
+ };
396
+ }
397
+ }
398
+
399
+ i = token.endPos;
400
+ }
401
+ }
402
+
403
+ return null;
404
+ }
405
+
406
+ /**
407
+ * Peek at the next non-whitespace, non-comment character starting at `fromPos`.
408
+ * Returns the character, or undefined if only whitespace/comments remain.
409
+ */
410
+ private static peekNextSignificantChar(sourceText: string, fromPos: number): string | undefined {
411
+ const tokenizer = new TypeScriptTokenizer(sourceText);
412
+ let i = fromPos;
413
+
414
+ while (i < sourceText.length) {
415
+ const token = tokenizer.getNextToken(i);
416
+
417
+ if (token.type === 'comment') {
418
+ i = token.endPos;
419
+ continue;
420
+ }
421
+
422
+ if (token.type === 'string') {
423
+ return token.value[0];
424
+ }
425
+
426
+ if (!/\s/.test(token.value)) {
427
+ return token.value;
428
+ }
429
+
430
+ i = token.endPos;
431
+ }
432
+
433
+ return undefined;
434
+ }
435
+
436
+ /**
437
+ * Determine whether a type expression continues after `fromPos`, i.e. whether
438
+ * the character that follows is a type-combinator/continuation operator
439
+ * (`|`, `&`, `[`, `.`, etc.) rather than the start of an unrelated statement.
440
+ *
441
+ * Used to decide if a top-level closing bracket terminates a semicolon-free
442
+ * type alias or whether the alias keeps going (e.g. a union of object types).
443
+ */
444
+ private static typeContinuesAfter(sourceText: string, fromPos: number): boolean {
445
+ const next = this.peekNextSignificantChar(sourceText, fromPos);
446
+ if (next === undefined) {
447
+ return false;
448
+ }
449
+ // Operators that mean the type expression continues across the boundary.
450
+ return '|&[.<'.includes(next);
451
+ }
452
+
453
+ /**
454
+ * Rename an interface or type definition.
455
+ *
456
+ * @example
457
+ * ```typescript
458
+ * renameTypeDefinition('interface User { name: string }', 'User', 'Person')
459
+ * // Returns: 'export interface Person { name: string }'
460
+ * ```
461
+ */
462
+ static renameTypeDefinition(definition: string, oldName: string, newName: string, exportIt = true): string {
463
+ const exportPrefix = exportIt ? 'export ' : '';
464
+
465
+ // Try interface rename
466
+ if (definition.includes('interface')) {
467
+ return definition.replace(
468
+ new RegExp(`interface\\s+${oldName}\\b`),
469
+ `${exportPrefix}interface ${newName}`
470
+ );
471
+ }
472
+
473
+ // Try type alias rename
474
+ if (definition.includes('type')) {
475
+ return definition.replace(
476
+ new RegExp(`type\\s+${oldName}\\b`),
477
+ `${exportPrefix}type ${newName}`
478
+ );
479
+ }
480
+
481
+ return definition;
482
+ }
483
+
484
+ /**
485
+ * Extract all type names referenced in a type definition.
486
+ * Excludes JSDoc comments and string literals.
487
+ *
488
+ * @example
489
+ * ```typescript
490
+ * extractReferencedTypes('interface Doc { author: User; tags: Tag[] }')
491
+ * // Returns: ['User', 'Tag']
492
+ * extractReferencedTypes('PaginatedResponse<Car>')
493
+ * // Returns: ['PaginatedResponse', 'Car']
494
+ * ```
495
+ */
496
+ static extractReferencedTypes(typeDefinition: string): string[] {
497
+ const types: string[] = [];
498
+
499
+ // Remove JSDoc comments to avoid extracting type names from documentation
500
+ const withoutJsDoc = typeDefinition.replace(/\/\*\*[\s\S]*?\*\//g, '');
501
+
502
+ // Match type references in actual code
503
+ // Types appear after: : < Array< extends implements | &
504
+ // Also match standalone types (e.g., PaginatedResponse in "PaginatedResponse<Car>")
505
+ // But exclude types after "interface" or "type" keywords (those are definitions, not references)
506
+ const typeContextRegex = /(?<!interface\s)(?<!type\s)(?::\s*|<|Array<|extends\s+|implements\s+|\|\s*|&\s*)?([A-Z][a-zA-Z0-9_]*)/g;
507
+ let match;
508
+
509
+ while ((match = typeContextRegex.exec(withoutJsDoc)) !== null) {
510
+ const typeName = match[1];
511
+
512
+ // Skip if this looks like it's part of "interface Name" or "type Name"
513
+ const beforeMatch = withoutJsDoc.substring(Math.max(0, match.index - 15), match.index);
514
+ if (/\b(interface|type)\s*$/.test(beforeMatch)) {
515
+ continue;
516
+ }
517
+
518
+ // Filter out built-in types and duplicates
519
+ if (!this.isBuiltInType(typeName) && !types.includes(typeName)) {
520
+ types.push(typeName);
521
+ }
522
+ }
523
+
524
+ return types;
525
+ }
526
+
527
+ /**
528
+ * Check if a type name is an inline object type.
529
+ */
530
+ static isInlineObjectType(typeName: string): boolean {
531
+ return typeName.trim().startsWith('{');
532
+ }
533
+
534
+ /**
535
+ * Parse JSDoc comment text to extract description.
536
+ * Strips JSDoc comment markers and leading asterisks.
537
+ *
538
+ * @example
539
+ * ```typescript
540
+ * parseJsDocDescription('/** Max cars to retrieve *\/')
541
+ * // Returns: "Max cars to retrieve"
542
+ *
543
+ * parseJsDocDescription('/**\n * Max cars to retrieve\n * from the database\n *\/')
544
+ * // Returns: "Max cars to retrieve from the database"
545
+ *
546
+ * parseJsDocDescription('')
547
+ * // Returns: ""
548
+ * ```
549
+ */
550
+ static parseJsDocDescription(jsDocText: string): string {
551
+ if (!jsDocText) {
552
+ return '';
553
+ }
554
+
555
+ // Remove /** and */ markers
556
+ let cleaned = jsDocText.replace(/^\/\*\*/, '').replace(/\*\/$/, '');
557
+
558
+ // Remove leading * from each line
559
+ cleaned = cleaned
560
+ .split('\n')
561
+ .map(line => line.trim().replace(/^\*\s?/, ''))
562
+ .filter(line => line.length > 0)
563
+ .join(' ');
564
+
565
+ // Collapse multiple spaces into single space
566
+ cleaned = cleaned.replace(/\s+/g, ' ');
567
+
568
+ return cleaned.trim();
569
+ }
570
+
571
+ /**
572
+ * Extract property metadata from an object type body.
573
+ * Parses properties with optional JSDoc comments.
574
+ * Only extracts top-level properties (not nested object properties).
575
+ *
576
+ * @param objectBody The content between { and } in an object type
577
+ * @returns Array of property metadata (name + description)
578
+ *
579
+ * @example
580
+ * ```typescript
581
+ * const body = `
582
+ * /** Max cars to retrieve *\/
583
+ * limit: string;
584
+ * page?: number;
585
+ * `;
586
+ * extractPropertiesFromObjectType(body)
587
+ * // Returns: [
588
+ * // { name: "limit", description: "Max cars to retrieve" },
589
+ * // { name: "page", description: "" }
590
+ * // ]
591
+ * ```
592
+ */
593
+ static extractPropertiesFromObjectType(objectBody: string): PropertyMetadata[] {
594
+ const properties: PropertyMetadata[] = [];
595
+ let i = 0;
596
+
597
+ while (i < objectBody.length) {
598
+ // Skip whitespace
599
+ while (i < objectBody.length && /\s/.test(objectBody[i])) {
600
+ i++;
601
+ }
602
+
603
+ if (i >= objectBody.length) break;
604
+
605
+ // Check for JSDoc comment
606
+ let jsDoc = '';
607
+ if (objectBody[i] === '/' && i + 1 < objectBody.length && objectBody[i + 1] === '*' && i + 2 < objectBody.length && objectBody[i + 2] === '*') {
608
+ const endIndex = objectBody.indexOf('*/', i + 3);
609
+ if (endIndex !== -1) {
610
+ jsDoc = objectBody.slice(i, endIndex + 2);
611
+ i = endIndex + 2;
612
+ } else {
613
+ break;
614
+ }
615
+ }
616
+
617
+ // Skip whitespace after JSDoc
618
+ while (i < objectBody.length && /\s/.test(objectBody[i])) {
619
+ i++;
620
+ }
621
+
622
+ if (i >= objectBody.length) break;
623
+
624
+ // Extract property name
625
+ const propertyNameMatch = objectBody.slice(i).match(/^(\w+)\??:/);
626
+ if (!propertyNameMatch) {
627
+ i++;
628
+ continue;
629
+ }
630
+
631
+ const propertyName = propertyNameMatch[1];
632
+ i += propertyNameMatch[0].length;
633
+
634
+ // Skip the type until we hit a delimiter at top level (;, ,, or closing })
635
+ // Use tokenizer to properly handle strings and comments
636
+ const tokenizer = new TypeScriptTokenizer(objectBody);
637
+ let depth = 0;
638
+
639
+ while (i < objectBody.length) {
640
+ const token = tokenizer.getNextToken(i);
641
+
642
+ // Only process non-string, non-comment tokens
643
+ if (token.type !== 'string' && token.type !== 'comment') {
644
+ const char = token.value;
645
+
646
+ // Track depth for nested objects/arrays
647
+ if (char === '{' || char === '[' || char === '(') {
648
+ depth++;
649
+ } else if (char === '}' || char === ']' || char === ')') {
650
+ depth--;
651
+ }
652
+
653
+ // End of property at top level
654
+ if (depth === 0 && (char === ';' || char === ',')) {
655
+ i = token.endPos; // Move past delimiter
656
+ break;
657
+ }
658
+
659
+ // End of property at a top-level newline when no `;`/`,`
660
+ // delimiter is used (semicolon-free style, issue #77). Only
661
+ // terminate when the next line starts a new member (an
662
+ // identifier, quoted key, or JSDoc) - not a continuation of
663
+ // the current type such as a leading `|` union arm.
664
+ if (depth === 0 && (char === '\n' || char === '\r')) {
665
+ const next = this.peekNextSignificantChar(objectBody, token.endPos);
666
+ if (next === undefined || /[A-Za-z_$"'/]/.test(next)) {
667
+ break; // Leave i at the newline; outer loop skips whitespace
668
+ }
669
+ }
670
+ }
671
+
672
+ i = token.endPos;
673
+ }
674
+
675
+ // Add property
676
+ const description = this.parseJsDocDescription(jsDoc);
677
+ properties.push({
678
+ name: propertyName,
679
+ description,
680
+ });
681
+ }
682
+
683
+ return properties;
684
+ }
685
+
686
+ /**
687
+ * Copy a type definition and recursively find all its dependencies.
688
+ * Returns structured data without side effects.
689
+ *
690
+ * @param sourceText Full source file text to search for types
691
+ * @param typeName Name of the type to copy
692
+ * @param newName New name for the type (use same name for dependencies)
693
+ * @param copiedTypes Set of already-copied type names to avoid duplication
694
+ * @returns Object containing the main definition, dependency definitions, and import statements
695
+ *
696
+ * @example
697
+ * ```typescript
698
+ * const source = `
699
+ * import { Address } from './Address';
700
+ * interface User {
701
+ * profile: Profile;
702
+ * address: Address;
703
+ * }
704
+ * interface Profile {
705
+ * name: string;
706
+ * }
707
+ * `;
708
+ * const result = copyTypeWithDependencies(source, 'User', 'UserSchema', new Set());
709
+ * // Returns: {
710
+ * // mainDefinition: 'export interface UserSchema { profile: Profile; address: Address; }',
711
+ * // dependencies: ['export interface Profile { name: string; }'],
712
+ * // imports: ['import { Address } from "./Address"'],
713
+ * // copiedTypes: Set(['User', 'Profile'])
714
+ * // }
715
+ * ```
716
+ */
717
+ static copyTypeWithDependencies(
718
+ sourceText: string,
719
+ typeName: string,
720
+ newName: string,
721
+ copiedTypes: Set<string>
722
+ ): {
723
+ mainDefinition: string | null;
724
+ dependencies: string[];
725
+ imports: string[];
726
+ copiedTypes: Set<string>;
727
+ } {
728
+ const dependencies: string[] = [];
729
+ const imports: string[] = [];
730
+
731
+ // Handle inline object types specially
732
+ let mainDefinition: string | null = null;
733
+
734
+ if (this.isInlineObjectType(typeName)) {
735
+ mainDefinition = this.convertInlineObjectToInterface(typeName, newName);
736
+ } else {
737
+ // Find and copy the main type from file
738
+ const found = this.findTypeDefinition(sourceText, typeName);
739
+ if (found) {
740
+ mainDefinition = this.renameTypeDefinition(found.definition, typeName, newName);
741
+ }
742
+ }
743
+
744
+ if (!mainDefinition) {
745
+ return { mainDefinition: null, dependencies: [], imports: [], copiedTypes };
746
+ }
747
+
748
+ // Mark this type as copied
749
+ copiedTypes.add(typeName);
750
+
751
+ // Find all referenced types in the definition
752
+ const referencedTypes = this.extractReferencedTypes(mainDefinition);
753
+
754
+ // Recursively copy each referenced type (if not already copied)
755
+ for (const refType of referencedTypes) {
756
+ if (!copiedTypes.has(refType) && !this.isBuiltInType(refType)) {
757
+ // Check if this type is defined in the current file
758
+ const typeDef = this.findTypeDefinition(sourceText, refType);
759
+
760
+ if (typeDef) {
761
+ // Type is defined in same file - recursively copy it
762
+ const result = this.copyTypeWithDependencies(sourceText, refType, refType, copiedTypes);
763
+
764
+ if (result.mainDefinition) {
765
+ dependencies.push(result.mainDefinition);
766
+ }
767
+ dependencies.push(...result.dependencies);
768
+ imports.push(...result.imports);
769
+ } else {
770
+ // Type is not in current file - must be imported
771
+ // Extract the import statement for it
772
+ const importStmt = this.extractImportForType(sourceText, refType);
773
+ if (importStmt && !imports.includes(importStmt)) {
774
+ imports.push(importStmt);
775
+ }
776
+ // Mark as copied to avoid trying to find it again
777
+ copiedTypes.add(refType);
778
+ }
779
+ }
780
+ }
781
+
782
+ return { mainDefinition, dependencies, imports, copiedTypes };
783
+ }
784
+
785
+ /**
786
+ * Extract import statement for a specific type from source text.
787
+ *
788
+ * @param sourceText Full source file text
789
+ * @param typeName Name of the type to find import for
790
+ * @returns Import statement or null if not found
791
+ *
792
+ * @example
793
+ * ```typescript
794
+ * const source = `
795
+ * import { User } from '../schemas/User';
796
+ * import type { Profile } from '../schemas/Profile';
797
+ * import Foo from '../schemas/Foo';
798
+ * `;
799
+ * extractImportForType(source, 'User')
800
+ * // Returns: "import { User } from '../schemas/User'"
801
+ *
802
+ * extractImportForType(source, 'Profile')
803
+ * // Returns: "import type { Profile } from '../schemas/Profile'"
804
+ *
805
+ * extractImportForType(source, 'Foo')
806
+ * // Returns: "import Foo from '../schemas/Foo'"
807
+ * ```
808
+ */
809
+ static extractImportForType(sourceText: string, typeName: string): string | null {
810
+ // Supports: import Foo from '...' OR import { Foo } from '...'
811
+ // Also supports: import type Foo from '...' OR import type { Foo } from '...'
812
+ const importRegex = new RegExp(
813
+ `import\\s+(?:type\\s+)?(?:{[^}]*\\b${typeName}\\b[^}]*}|${typeName})\\s+from\\s+(['"][^'"]+['"])`,
814
+ 'gm'
815
+ );
816
+ const match = importRegex.exec(sourceText);
817
+
818
+ if (match) {
819
+ return match[0];
820
+ }
821
+
822
+ return null;
823
+ }
824
+
825
+ /**
826
+ * Extract all imports from a source file.
827
+ * Used to get transitive dependencies when importing a type.
828
+ *
829
+ * @param sourceText Source code to extract imports from
830
+ * @returns Array of import statements
831
+ */
832
+ static extractAllImports(sourceText: string): string[] {
833
+ const imports: string[] = [];
834
+ // Match all import statements (including type imports, named imports, default imports, namespace imports)
835
+ const importRegex = /import\s+(?:type\s+)?(?:{[^}]+}|\w+|(?:\*\s+as\s+\w+))\s+from\s+['"][^'"]+['"]/gm;
836
+ let match;
837
+
838
+ while ((match = importRegex.exec(sourceText)) !== null) {
839
+ imports.push(match[0]);
840
+ }
841
+
842
+ return imports;
843
+ }
844
+
845
+ /**
846
+ * Adjust import path to be relative to .flink/schemas/ directory.
847
+ *
848
+ * @param importStatement Original import statement
849
+ * @returns Adjusted import statement with corrected path
850
+ *
851
+ * @example
852
+ * ```typescript
853
+ * adjustImportPathForSchemas('import { User } from "../schemas/User"')
854
+ * // Returns: 'import { User } from "../../src/schemas/User"'
855
+ *
856
+ * adjustImportPathForSchemas('import Profile from "../../schemas/Profile"')
857
+ * // Returns: 'import Profile from "../../src/schemas/Profile"'
858
+ *
859
+ * adjustImportPathForSchemas('import { Req } from "../clients/SupermetricsClient"')
860
+ * // Returns: 'import { Req } from "../../src/clients/SupermetricsClient"'
861
+ *
862
+ * adjustImportPathForSchemas('import { Foo } from "@company/shared"')
863
+ * // Returns: 'import { Foo } from "@company/shared"' (unchanged - not relative)
864
+ * ```
865
+ */
866
+ static adjustImportPathForSchemas(importStatement: string): string {
867
+ // schemas.ts is at: .flink/schemas/schemas.ts
868
+ // Handler imports can be from anywhere in src/ (handlers, schemas, clients, etc.)
869
+ // We need to adjust ALL relative imports to work from .flink/schemas/
870
+
871
+ // Extract the path from import statement
872
+ const pathMatch = importStatement.match(/from\s+(['"])([^'"]+)['"]/);
873
+ if (!pathMatch) {
874
+ return importStatement;
875
+ }
876
+
877
+ const quote = pathMatch[1];
878
+ const importPath = pathMatch[2];
879
+
880
+ // Skip non-relative imports (package imports like "@company/shared", "lodash")
881
+ if (!importPath.startsWith('./') && !importPath.startsWith('../')) {
882
+ return importStatement;
883
+ }
884
+
885
+ // For relative imports, we need to adjust the path to work from .flink/schemas/
886
+ // Original: from handler at src/handlers/foo/Bar.ts importing "../clients/Client"
887
+ // Resolves to: src/clients/Client
888
+ // From schemas.ts at .flink/schemas/schemas.ts, we need: "../../src/clients/Client"
889
+
890
+ // Extract the path after src/ directory
891
+ // Patterns we handle:
892
+ // - "../schemas/User" → "../../src/schemas/User"
893
+ // - "../../schemas/User" → "../../src/schemas/User"
894
+ // - "../clients/Client" → "../../src/clients/Client"
895
+ // - "../../types/Foo" → "../../src/types/Foo"
896
+
897
+ // Find the src directory marker in the path
898
+ let adjustedPath: string;
899
+
900
+ if (importPath.includes('/schemas/')) {
901
+ // Special case for schemas - extract everything after '/schemas/'
902
+ const schemasIndex = importPath.indexOf('/schemas/');
903
+ const pathAfterSchemas = importPath.substring(schemasIndex + '/schemas/'.length);
904
+ adjustedPath = `../../src/schemas/${pathAfterSchemas}`;
905
+ } else {
906
+ // General case: assume the import resolves to src/<something>
907
+ // Count the number of "../" to understand directory depth, then reconstruct
908
+ // For simplicity, we'll assume all imports from handlers resolve to src/*
909
+ // Pattern: any number of "../" optionally followed by "./" followed by a directory path
910
+ // Strip both "../" and "./" prefixes
911
+ const pathWithoutLeadingDots = importPath.replace(/^(\.\.\/)*/, '').replace(/^\.\//, '');
912
+ adjustedPath = `../../src/${pathWithoutLeadingDots}`;
913
+ }
914
+
915
+ return importStatement.replace(
916
+ `from ${quote}${importPath}${quote}`,
917
+ `from ${quote}${adjustedPath}${quote}`
918
+ );
919
+ }
920
+
921
+ /**
922
+ * Extract property metadata from a named type definition.
923
+ * Searches for the type definition and parses its properties.
924
+ *
925
+ * @param sourceText Full source file text
926
+ * @param typeName Name of the type to extract properties from
927
+ * @returns Array of property metadata, or null if type not found
928
+ *
929
+ * @example
930
+ * ```typescript
931
+ * const source = `
932
+ * interface Query {
933
+ * /** Max items *\/
934
+ * limit: string;
935
+ * page?: number;
936
+ * }
937
+ * `;
938
+ * extractPropertyMetadata(source, 'Query')
939
+ * // Returns: [
940
+ * // { name: "limit", description: "Max items" },
941
+ * // { name: "page", description: "" }
942
+ * // ]
943
+ * ```
944
+ */
945
+ static extractPropertyMetadata(sourceText: string, typeName: string): PropertyMetadata[] | null {
946
+ // Find the type definition
947
+ const typeDef = this.findTypeDefinition(sourceText, typeName);
948
+
949
+ if (!typeDef) {
950
+ return null;
951
+ }
952
+
953
+ // Extract the object body (content between { and })
954
+ const definition = typeDef.definition;
955
+
956
+ // Find the opening brace
957
+ const openBraceIndex = definition.indexOf('{');
958
+ if (openBraceIndex === -1) {
959
+ return null;
960
+ }
961
+
962
+ // Use tokenizer to find the matching closing brace (handles strings and comments)
963
+ const tokenizer = new TypeScriptTokenizer(definition);
964
+ const closeBraceIndex = tokenizer.findMatchingBracketAt(openBraceIndex, '{');
965
+
966
+ if (closeBraceIndex === -1) {
967
+ return null;
968
+ }
969
+
970
+ // Extract the object body (excluding the outer braces)
971
+ const objectBody = definition.slice(openBraceIndex + 1, closeBraceIndex);
972
+
973
+ return this.extractPropertiesFromObjectType(objectBody);
974
+ }
975
+
976
+ /**
977
+ * Consolidates duplicate imports from the same module.
978
+ * Merges named imports and preserves default imports.
979
+ *
980
+ * @param imports Array of import statements
981
+ * @returns Consolidated import statements with duplicates merged
982
+ *
983
+ * @example
984
+ * ```typescript
985
+ * const imports = [
986
+ * 'import { Ad } from "../../src/schemas/Ad"',
987
+ * 'import { AdStatus } from "../../src/schemas/Ad"',
988
+ * 'import { User } from "../../src/schemas/User"'
989
+ * ];
990
+ * consolidateImports(imports)
991
+ * // Returns: [
992
+ * // 'import { Ad, AdStatus } from "../../src/schemas/Ad"',
993
+ * // 'import { User } from "../../src/schemas/User"'
994
+ * // ]
995
+ * ```
996
+ */
997
+ static consolidateImports(imports: string[]): string[] {
998
+ // Map: module path -> { namedImports: Set<string>, defaultImport?: string }
999
+ const importMap = new Map<string, { namedImports: Set<string>; defaultImport?: string }>();
1000
+
1001
+ for (const importStmt of imports) {
1002
+ // Parse import statement to extract module path and imports
1003
+ // Patterns:
1004
+ // - import { Foo, Bar } from "path"
1005
+ // - import type { Foo, Bar } from "path"
1006
+ // - import Foo from "path"
1007
+ // - import type Foo from "path"
1008
+ const namedMatch = importStmt.match(/import\s+(?:type\s+)?\{([^}]+)\}\s+from\s+(['"])([^'"]+)['"]/);
1009
+ const defaultMatch = importStmt.match(/import\s+(?:type\s+)?(\w+)\s+from\s+(['"])([^'"]+)['"]/);
1010
+
1011
+ if (namedMatch) {
1012
+ const namedImports = namedMatch[1]
1013
+ .split(',')
1014
+ .map((s) => s.trim())
1015
+ .filter((s) => s.length > 0);
1016
+ const modulePath = namedMatch[3];
1017
+
1018
+ if (!importMap.has(modulePath)) {
1019
+ importMap.set(modulePath, { namedImports: new Set() });
1020
+ }
1021
+
1022
+ const entry = importMap.get(modulePath)!;
1023
+ namedImports.forEach((name) => entry.namedImports.add(name));
1024
+ } else if (defaultMatch) {
1025
+ const defaultImport = defaultMatch[1];
1026
+ const modulePath = defaultMatch[3];
1027
+
1028
+ if (!importMap.has(modulePath)) {
1029
+ importMap.set(modulePath, { namedImports: new Set() });
1030
+ }
1031
+
1032
+ const entry = importMap.get(modulePath)!;
1033
+ entry.defaultImport = defaultImport;
1034
+ }
1035
+ }
1036
+
1037
+ // Generate consolidated import statements
1038
+ const consolidated: string[] = [];
1039
+ const entries = Array.from(importMap.entries());
1040
+
1041
+ for (const [modulePath, { namedImports, defaultImport }] of entries) {
1042
+ if (defaultImport && namedImports.size > 0) {
1043
+ // Both default and named imports
1044
+ const named = Array.from(namedImports).sort().join(', ');
1045
+ consolidated.push(`import ${defaultImport}, { ${named} } from "${modulePath}"`);
1046
+ } else if (defaultImport) {
1047
+ // Default import only
1048
+ consolidated.push(`import ${defaultImport} from "${modulePath}"`);
1049
+ } else if (namedImports.size > 0) {
1050
+ // Named imports only
1051
+ const named = Array.from(namedImports).sort().join(', ');
1052
+ consolidated.push(`import { ${named} } from "${modulePath}"`);
1053
+ }
1054
+ }
1055
+
1056
+ return consolidated;
1057
+ }
1058
+ }