@agentuity/cli 1.0.59 → 2.0.0-beta.1

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 (189) hide show
  1. package/bin/cli.ts +2 -3
  2. package/dist/cmd/build/app-config-extractor.d.ts +27 -0
  3. package/dist/cmd/build/app-config-extractor.d.ts.map +1 -0
  4. package/dist/cmd/build/app-config-extractor.js +152 -0
  5. package/dist/cmd/build/app-config-extractor.js.map +1 -0
  6. package/dist/cmd/build/app-router-detector.d.ts +2 -5
  7. package/dist/cmd/build/app-router-detector.d.ts.map +1 -1
  8. package/dist/cmd/build/app-router-detector.js +130 -154
  9. package/dist/cmd/build/app-router-detector.js.map +1 -1
  10. package/dist/cmd/build/ci.d.ts.map +1 -1
  11. package/dist/cmd/build/ci.js +5 -21
  12. package/dist/cmd/build/ci.js.map +1 -1
  13. package/dist/cmd/build/ids.d.ts +11 -0
  14. package/dist/cmd/build/ids.d.ts.map +1 -0
  15. package/dist/cmd/build/ids.js +18 -0
  16. package/dist/cmd/build/ids.js.map +1 -0
  17. package/dist/cmd/build/index.d.ts.map +1 -1
  18. package/dist/cmd/build/index.js +8 -0
  19. package/dist/cmd/build/index.js.map +1 -1
  20. package/dist/cmd/build/vite/agent-discovery.d.ts +8 -4
  21. package/dist/cmd/build/vite/agent-discovery.d.ts.map +1 -1
  22. package/dist/cmd/build/vite/agent-discovery.js +166 -487
  23. package/dist/cmd/build/vite/agent-discovery.js.map +1 -1
  24. package/dist/cmd/build/vite/bun-dev-server.d.ts +43 -14
  25. package/dist/cmd/build/vite/bun-dev-server.d.ts.map +1 -1
  26. package/dist/cmd/build/vite/bun-dev-server.js +290 -129
  27. package/dist/cmd/build/vite/bun-dev-server.js.map +1 -1
  28. package/dist/cmd/build/vite/config-loader.d.ts +15 -20
  29. package/dist/cmd/build/vite/config-loader.d.ts.map +1 -1
  30. package/dist/cmd/build/vite/config-loader.js +41 -74
  31. package/dist/cmd/build/vite/config-loader.js.map +1 -1
  32. package/dist/cmd/build/vite/docs-generator.d.ts.map +1 -1
  33. package/dist/cmd/build/vite/docs-generator.js +0 -2
  34. package/dist/cmd/build/vite/docs-generator.js.map +1 -1
  35. package/dist/cmd/build/vite/index.d.ts.map +1 -1
  36. package/dist/cmd/build/vite/index.js +0 -36
  37. package/dist/cmd/build/vite/index.js.map +1 -1
  38. package/dist/cmd/build/vite/lifecycle-generator.d.ts +10 -2
  39. package/dist/cmd/build/vite/lifecycle-generator.d.ts.map +1 -1
  40. package/dist/cmd/build/vite/lifecycle-generator.js +302 -23
  41. package/dist/cmd/build/vite/lifecycle-generator.js.map +1 -1
  42. package/dist/cmd/build/vite/route-discovery.d.ts +11 -38
  43. package/dist/cmd/build/vite/route-discovery.d.ts.map +1 -1
  44. package/dist/cmd/build/vite/route-discovery.js +97 -177
  45. package/dist/cmd/build/vite/route-discovery.js.map +1 -1
  46. package/dist/cmd/build/vite/server-bundler.js +1 -1
  47. package/dist/cmd/build/vite/server-bundler.js.map +1 -1
  48. package/dist/cmd/build/vite/static-renderer.d.ts +0 -2
  49. package/dist/cmd/build/vite/static-renderer.d.ts.map +1 -1
  50. package/dist/cmd/build/vite/static-renderer.js +19 -13
  51. package/dist/cmd/build/vite/static-renderer.js.map +1 -1
  52. package/dist/cmd/build/vite/vite-asset-server-config.d.ts +6 -3
  53. package/dist/cmd/build/vite/vite-asset-server-config.d.ts.map +1 -1
  54. package/dist/cmd/build/vite/vite-asset-server-config.js +175 -69
  55. package/dist/cmd/build/vite/vite-asset-server-config.js.map +1 -1
  56. package/dist/cmd/build/vite/vite-asset-server.d.ts +8 -3
  57. package/dist/cmd/build/vite/vite-asset-server.d.ts.map +1 -1
  58. package/dist/cmd/build/vite/vite-asset-server.js +14 -13
  59. package/dist/cmd/build/vite/vite-asset-server.js.map +1 -1
  60. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  61. package/dist/cmd/build/vite/vite-builder.js +42 -190
  62. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  63. package/dist/cmd/build/vite/ws-proxy.d.ts +53 -0
  64. package/dist/cmd/build/vite/ws-proxy.d.ts.map +1 -0
  65. package/dist/cmd/build/vite/ws-proxy.js +95 -0
  66. package/dist/cmd/build/vite/ws-proxy.js.map +1 -0
  67. package/dist/cmd/build/vite-bundler.d.ts.map +1 -1
  68. package/dist/cmd/build/vite-bundler.js +0 -3
  69. package/dist/cmd/build/vite-bundler.js.map +1 -1
  70. package/dist/cmd/cloud/deploy-fork.d.ts.map +1 -1
  71. package/dist/cmd/cloud/deploy-fork.js +15 -36
  72. package/dist/cmd/cloud/deploy-fork.js.map +1 -1
  73. package/dist/cmd/cloud/sandbox/exec.d.ts.map +1 -1
  74. package/dist/cmd/cloud/sandbox/exec.js +28 -86
  75. package/dist/cmd/cloud/sandbox/exec.js.map +1 -1
  76. package/dist/cmd/cloud/sandbox/run.d.ts.map +1 -1
  77. package/dist/cmd/cloud/sandbox/run.js +2 -9
  78. package/dist/cmd/cloud/sandbox/run.js.map +1 -1
  79. package/dist/cmd/cloud/sandbox/snapshot/build.js +2 -2
  80. package/dist/cmd/cloud/sandbox/snapshot/build.js.map +1 -1
  81. package/dist/cmd/coder/hub-url.d.ts.map +1 -1
  82. package/dist/cmd/coder/hub-url.js +1 -3
  83. package/dist/cmd/coder/hub-url.js.map +1 -1
  84. package/dist/cmd/coder/start.js +6 -6
  85. package/dist/cmd/coder/start.js.map +1 -1
  86. package/dist/cmd/coder/tui-init.d.ts +2 -2
  87. package/dist/cmd/coder/tui-init.js +2 -2
  88. package/dist/cmd/coder/tui-init.js.map +1 -1
  89. package/dist/cmd/dev/file-watcher.d.ts.map +1 -1
  90. package/dist/cmd/dev/file-watcher.js +2 -8
  91. package/dist/cmd/dev/file-watcher.js.map +1 -1
  92. package/dist/cmd/dev/index.d.ts.map +1 -1
  93. package/dist/cmd/dev/index.js +432 -752
  94. package/dist/cmd/dev/index.js.map +1 -1
  95. package/dist/cmd/dev/process-manager.d.ts +104 -0
  96. package/dist/cmd/dev/process-manager.d.ts.map +1 -0
  97. package/dist/cmd/dev/process-manager.js +204 -0
  98. package/dist/cmd/dev/process-manager.js.map +1 -0
  99. package/dist/errors.d.ts +10 -24
  100. package/dist/errors.d.ts.map +1 -1
  101. package/dist/errors.js +12 -42
  102. package/dist/errors.js.map +1 -1
  103. package/dist/schema-generator.d.ts.map +1 -1
  104. package/dist/schema-generator.js +12 -2
  105. package/dist/schema-generator.js.map +1 -1
  106. package/dist/tui.d.ts.map +1 -1
  107. package/dist/tui.js +5 -19
  108. package/dist/tui.js.map +1 -1
  109. package/dist/utils/version-mismatch.d.ts +39 -0
  110. package/dist/utils/version-mismatch.d.ts.map +1 -0
  111. package/dist/utils/version-mismatch.js +161 -0
  112. package/dist/utils/version-mismatch.js.map +1 -0
  113. package/package.json +6 -6
  114. package/src/cmd/ai/prompt/agent.md +0 -1
  115. package/src/cmd/ai/prompt/api.md +0 -7
  116. package/src/cmd/ai/prompt/web.md +51 -213
  117. package/src/cmd/build/app-config-extractor.ts +186 -0
  118. package/src/cmd/build/app-router-detector.ts +152 -182
  119. package/src/cmd/build/ci.ts +5 -21
  120. package/src/cmd/build/ids.ts +19 -0
  121. package/src/cmd/build/index.ts +10 -0
  122. package/src/cmd/build/vite/agent-discovery.ts +208 -679
  123. package/src/cmd/build/vite/bun-dev-server.ts +383 -146
  124. package/src/cmd/build/vite/config-loader.ts +45 -77
  125. package/src/cmd/build/vite/docs-generator.ts +0 -2
  126. package/src/cmd/build/vite/index.ts +1 -42
  127. package/src/cmd/build/vite/lifecycle-generator.ts +345 -21
  128. package/src/cmd/build/vite/route-discovery.ts +116 -274
  129. package/src/cmd/build/vite/server-bundler.ts +1 -1
  130. package/src/cmd/build/vite/static-renderer.ts +23 -15
  131. package/src/cmd/build/vite/vite-asset-server-config.ts +200 -70
  132. package/src/cmd/build/vite/vite-asset-server.ts +25 -15
  133. package/src/cmd/build/vite/vite-builder.ts +49 -220
  134. package/src/cmd/build/vite/ws-proxy.ts +126 -0
  135. package/src/cmd/build/vite-bundler.ts +0 -4
  136. package/src/cmd/cloud/deploy-fork.ts +16 -39
  137. package/src/cmd/cloud/sandbox/exec.ts +23 -130
  138. package/src/cmd/cloud/sandbox/run.ts +2 -9
  139. package/src/cmd/cloud/sandbox/snapshot/build.ts +2 -2
  140. package/src/cmd/coder/hub-url.ts +1 -3
  141. package/src/cmd/coder/start.ts +6 -6
  142. package/src/cmd/coder/tui-init.ts +4 -4
  143. package/src/cmd/dev/file-watcher.ts +2 -9
  144. package/src/cmd/dev/index.ts +476 -859
  145. package/src/cmd/dev/process-manager.ts +261 -0
  146. package/src/errors.ts +12 -44
  147. package/src/schema-generator.ts +12 -2
  148. package/src/tui.ts +5 -18
  149. package/src/utils/version-mismatch.ts +204 -0
  150. package/dist/cmd/build/ast.d.ts +0 -78
  151. package/dist/cmd/build/ast.d.ts.map +0 -1
  152. package/dist/cmd/build/ast.js +0 -2703
  153. package/dist/cmd/build/ast.js.map +0 -1
  154. package/dist/cmd/build/entry-generator.d.ts +0 -25
  155. package/dist/cmd/build/entry-generator.d.ts.map +0 -1
  156. package/dist/cmd/build/entry-generator.js +0 -695
  157. package/dist/cmd/build/entry-generator.js.map +0 -1
  158. package/dist/cmd/build/vite/api-mount-path.d.ts +0 -61
  159. package/dist/cmd/build/vite/api-mount-path.d.ts.map +0 -1
  160. package/dist/cmd/build/vite/api-mount-path.js +0 -83
  161. package/dist/cmd/build/vite/api-mount-path.js.map +0 -1
  162. package/dist/cmd/build/vite/registry-generator.d.ts +0 -19
  163. package/dist/cmd/build/vite/registry-generator.d.ts.map +0 -1
  164. package/dist/cmd/build/vite/registry-generator.js +0 -1108
  165. package/dist/cmd/build/vite/registry-generator.js.map +0 -1
  166. package/dist/cmd/build/webanalytics-generator.d.ts +0 -16
  167. package/dist/cmd/build/webanalytics-generator.d.ts.map +0 -1
  168. package/dist/cmd/build/webanalytics-generator.js +0 -178
  169. package/dist/cmd/build/webanalytics-generator.js.map +0 -1
  170. package/dist/cmd/build/workbench.d.ts +0 -7
  171. package/dist/cmd/build/workbench.d.ts.map +0 -1
  172. package/dist/cmd/build/workbench.js +0 -55
  173. package/dist/cmd/build/workbench.js.map +0 -1
  174. package/dist/utils/route-migration.d.ts +0 -62
  175. package/dist/utils/route-migration.d.ts.map +0 -1
  176. package/dist/utils/route-migration.js +0 -630
  177. package/dist/utils/route-migration.js.map +0 -1
  178. package/dist/utils/stream-capture.d.ts +0 -9
  179. package/dist/utils/stream-capture.d.ts.map +0 -1
  180. package/dist/utils/stream-capture.js +0 -34
  181. package/dist/utils/stream-capture.js.map +0 -1
  182. package/src/cmd/build/ast.ts +0 -3529
  183. package/src/cmd/build/entry-generator.ts +0 -760
  184. package/src/cmd/build/vite/api-mount-path.ts +0 -87
  185. package/src/cmd/build/vite/registry-generator.ts +0 -1267
  186. package/src/cmd/build/webanalytics-generator.ts +0 -197
  187. package/src/cmd/build/workbench.ts +0 -58
  188. package/src/utils/route-migration.ts +0 -757
  189. package/src/utils/stream-capture.ts +0 -39
@@ -1,3529 +0,0 @@
1
- import * as acornLoose from 'acorn-loose';
2
- import { dirname, relative, join, basename, resolve } from 'node:path';
3
- import { parse as parseCronExpression } from '@datasert/cronjs-parser';
4
- import { generate } from 'astring';
5
- import type { BuildMetadata } from '../../types';
6
- import { createLogger } from '@agentuity/server';
7
- import * as ts from 'typescript';
8
- import { StructuredError, type WorkbenchConfig } from '@agentuity/core';
9
- import type { LogLevel } from '../../types';
10
-
11
- import { existsSync, mkdirSync, statSync } from 'node:fs';
12
- import { parseJSONC } from '../../utils/jsonc';
13
- import { formatSchemaCode } from './format-schema';
14
- import { toForwardSlash } from '../../utils/normalize-path';
15
- import {
16
- computeApiMountPath,
17
- joinMountAndRoute,
18
- extractRelativeApiPath,
19
- } from './vite/api-mount-path';
20
-
21
- const logger = createLogger((process.env.AGENTUITY_LOG_LEVEL || 'info') as LogLevel);
22
-
23
- interface ASTProgram extends ASTNode {
24
- body: ASTNode[];
25
- }
26
-
27
- interface ASTNode {
28
- type: string;
29
- start?: number;
30
- end?: number;
31
- }
32
-
33
- interface ASTNodeIdentifier extends ASTNode {
34
- name: string;
35
- }
36
-
37
- interface ASTCallExpression extends ASTNode {
38
- arguments: unknown[];
39
- callee: ASTMemberExpression;
40
- }
41
-
42
- interface ASTPropertyNode {
43
- type: string;
44
- kind: string;
45
- key: ASTNodeIdentifier;
46
- value: ASTNode;
47
- shorthand?: boolean;
48
- method?: boolean;
49
- computed?: boolean;
50
- }
51
-
52
- interface ASTObjectExpression extends ASTNode {
53
- properties: ASTPropertyNode[];
54
- }
55
-
56
- interface ASTLiteral extends ASTNode {
57
- value: string | number | boolean | null;
58
- raw?: string;
59
- }
60
-
61
- interface ASTMemberExpression extends ASTNode {
62
- object: ASTNode;
63
- property: ASTNode;
64
- computed: boolean;
65
- optional: boolean;
66
- name?: string;
67
- }
68
-
69
- interface ASTExpressionStatement extends ASTNode {
70
- expression: ASTCallExpression;
71
- }
72
-
73
- interface ASTVariableDeclarator extends ASTNode {
74
- id: ASTNode;
75
- init?: ASTNode;
76
- }
77
-
78
- function parseObjectExpressionToMap(expr: ASTObjectExpression): Map<string, string> {
79
- const result = new Map<string, string>();
80
- for (const prop of expr.properties) {
81
- switch (prop.value.type) {
82
- case 'Literal': {
83
- const value = prop.value as unknown as ASTLiteral;
84
- result.set(prop.key.name, String(value.value));
85
- break;
86
- }
87
- default: {
88
- console.warn(
89
- 'AST value type %s of metadata key: %s not supported',
90
- prop.value.type,
91
- prop.key.name
92
- );
93
- }
94
- }
95
- }
96
- return result;
97
- }
98
-
99
- function createObjectPropertyNode(key: string, value: string) {
100
- return {
101
- type: 'Property',
102
- kind: 'init',
103
- key: {
104
- type: 'Identifier',
105
- name: key,
106
- },
107
- value: {
108
- type: 'Literal',
109
- value,
110
- },
111
- };
112
- }
113
-
114
- function createNewMetadataNode() {
115
- return {
116
- type: 'Property',
117
- kind: 'init',
118
- key: {
119
- type: 'Identifier',
120
- name: 'metadata',
121
- },
122
- value: {
123
- type: 'ObjectExpression',
124
- properties: [] as ASTPropertyNode[],
125
- },
126
- };
127
- }
128
-
129
- function hash(...val: string[]): string {
130
- const hasher = new Bun.CryptoHasher('sha256');
131
- val.map((val) => hasher.update(val));
132
- return hasher.digest().toHex();
133
- }
134
-
135
- function hashSHA1(...val: string[]): string {
136
- const hasher = new Bun.CryptoHasher('sha1');
137
- val.map((val) => hasher.update(val));
138
- return hasher.digest().toHex();
139
- }
140
-
141
- export function getDevmodeDeploymentId(projectId: string, endpointId: string): string {
142
- return `devmode_${hashSHA1(projectId, endpointId)}`;
143
- }
144
-
145
- // getAgentId generates the deployment-specific agent ID (becomes database PK agent.id)
146
- // This ID changes with each deployment and uses the agentid_ prefix
147
- // Hash includes deploymentId so it's unique per deployment
148
- function getAgentId(
149
- projectId: string,
150
- deploymentId: string,
151
- filename: string,
152
- version: string
153
- ): string {
154
- return `agentid_${hashSHA1(projectId, deploymentId, filename, version)}`;
155
- }
156
-
157
- function getEvalId(
158
- projectId: string,
159
- deploymentId: string,
160
- filename: string,
161
- name: string,
162
- version: string
163
- ): string {
164
- return `evalid_${hashSHA1(projectId, deploymentId, filename, name, version)}`;
165
- }
166
-
167
- function generateRouteId(
168
- projectId: string,
169
- deploymentId: string,
170
- type: string,
171
- method: string,
172
- filename: string,
173
- path: string,
174
- version: string
175
- ): string {
176
- return `route_${hashSHA1(projectId, deploymentId, type, method, filename, path, version)}`;
177
- }
178
-
179
- // generateStableAgentId generates the stable identifier (becomes database agent.identifier)
180
- // This uses the agent_ prefix and is the same across all deployments
181
- // Hash only includes projectId + name, no deploymentId
182
- function generateStableAgentId(projectId: string, name: string): string {
183
- return `agent_${hashSHA1(projectId, name)}`.substring(0, 64);
184
- }
185
-
186
- function generateStableEvalId(projectId: string, agentId: string, name: string): string {
187
- return `eval_${hashSHA1(projectId, agentId, name)}`.substring(0, 64);
188
- }
189
-
190
- /**
191
- * Type guard to check if an AST node is an ObjectExpression
192
- */
193
- function isObjectExpression(node: unknown): node is ASTObjectExpression {
194
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
195
- return typeof node === 'object' && node !== null && (node as any).type === 'ObjectExpression';
196
- }
197
-
198
- /**
199
- * Extract schema code from createAgent call arguments
200
- * Returns input and output schema code as strings
201
- */
202
- function extractSchemaCode(callargexp: ASTObjectExpression): {
203
- inputSchemaCode?: string;
204
- outputSchemaCode?: string;
205
- } {
206
- let schemaObj: ASTObjectExpression | undefined;
207
-
208
- // Find the schema property
209
- for (const prop of callargexp.properties) {
210
- if (prop.key.type === 'Identifier' && prop.key.name === 'schema') {
211
- if (prop.value.type === 'ObjectExpression') {
212
- schemaObj = prop.value as ASTObjectExpression;
213
- break;
214
- }
215
- }
216
- }
217
-
218
- if (!schemaObj) {
219
- return {};
220
- }
221
-
222
- let inputSchemaCode: string | undefined;
223
- let outputSchemaCode: string | undefined;
224
-
225
- // Extract input and output schema code
226
- for (const prop of schemaObj.properties) {
227
- if (prop.key.type === 'Identifier') {
228
- if (prop.key.name === 'input' && prop.value) {
229
- // Generate source code from AST node and format it
230
- inputSchemaCode = formatSchemaCode(generate(prop.value));
231
- } else if (prop.key.name === 'output' && prop.value) {
232
- // Generate source code from AST node and format it
233
- outputSchemaCode = formatSchemaCode(generate(prop.value));
234
- }
235
- }
236
- }
237
-
238
- return { inputSchemaCode, outputSchemaCode };
239
- }
240
-
241
- type AcornParseResultType = ReturnType<typeof acornLoose.parse>;
242
-
243
- const MetadataError = StructuredError('MetatadataNameMissingError')<{
244
- filename: string;
245
- line?: number;
246
- }>();
247
-
248
- function augmentAgentMetadataNode(
249
- projectId: string,
250
- id: string,
251
- rel: string,
252
- version: string,
253
- ast: AcornParseResultType,
254
- propvalue: ASTObjectExpression,
255
- filename: string,
256
- inputSchemaCode?: string,
257
- outputSchemaCode?: string
258
- ): [string, Map<string, string>] {
259
- const metadata = parseObjectExpressionToMap(propvalue);
260
- if (!metadata.has('name')) {
261
- const location = ast.loc?.start?.line ? ` on line ${ast.loc.start.line}` : '';
262
- throw new MetadataError({
263
- filename,
264
- line: ast.loc?.start?.line,
265
- message: `missing required metadata.name in ${filename}${location}. This Agent should have a unique and human readable name for this project.`,
266
- });
267
- }
268
- const name = metadata.get('name')!;
269
- const descriptionNode = propvalue.properties.find((x) => x.key.name === 'description')?.value;
270
- const descriptionValue = descriptionNode ? (descriptionNode as ASTLiteral).value : '';
271
- const description = typeof descriptionValue === 'string' ? descriptionValue : '';
272
- const agentId = generateStableAgentId(projectId, name);
273
- metadata.set('version', version);
274
- metadata.set('filename', rel);
275
- metadata.set('id', id);
276
- metadata.set('agentId', agentId);
277
- metadata.set('description', description);
278
- if (inputSchemaCode) {
279
- metadata.set('inputSchemaCode', inputSchemaCode);
280
- }
281
- if (outputSchemaCode) {
282
- metadata.set('outputSchemaCode', outputSchemaCode);
283
- }
284
- propvalue.properties.push(
285
- createObjectPropertyNode('id', id),
286
- createObjectPropertyNode('agentId', agentId),
287
- createObjectPropertyNode('version', version),
288
- createObjectPropertyNode('filename', rel),
289
- createObjectPropertyNode('description', description)
290
- );
291
- if (inputSchemaCode) {
292
- propvalue.properties.push(createObjectPropertyNode('inputSchemaCode', inputSchemaCode));
293
- }
294
- if (outputSchemaCode) {
295
- propvalue.properties.push(createObjectPropertyNode('outputSchemaCode', outputSchemaCode));
296
- }
297
-
298
- const newsource = generate(ast);
299
-
300
- // Evals imports are now handled in registry.generated.ts
301
- return [newsource, metadata];
302
- }
303
-
304
- function createAgentMetadataNode(
305
- id: string,
306
- name: string,
307
- rel: string,
308
- version: string,
309
- ast: AcornParseResultType,
310
- callargexp: ASTObjectExpression,
311
- _filename: string,
312
- projectId: string,
313
- inputSchemaCode?: string,
314
- outputSchemaCode?: string
315
- ): [string, Map<string, string>] {
316
- const newmetadata = createNewMetadataNode();
317
- const agentId = generateStableAgentId(projectId, name);
318
- const md = new Map<string, string>();
319
- md.set('id', id);
320
- md.set('agentId', agentId);
321
- md.set('version', version);
322
- md.set('name', name);
323
- md.set('filename', rel);
324
- if (inputSchemaCode) {
325
- md.set('inputSchemaCode', inputSchemaCode);
326
- }
327
- if (outputSchemaCode) {
328
- md.set('outputSchemaCode', outputSchemaCode);
329
- }
330
- for (const [key, value] of md) {
331
- newmetadata.value.properties.push(createObjectPropertyNode(key, value));
332
- }
333
- callargexp.properties.push(newmetadata);
334
-
335
- const newsource = generate(ast);
336
-
337
- // Evals imports are now handled in registry.generated.ts
338
- return [newsource, md];
339
- }
340
-
341
- const DuplicateNameError = StructuredError('DuplicateNameError')<{ filename: string }>();
342
-
343
- function injectEvalMetadata(
344
- configObj: ASTObjectExpression,
345
- evalId: string,
346
- stableEvalId: string,
347
- version: string,
348
- filename: string,
349
- agentId?: string
350
- ): void {
351
- // Create metadata object with eval IDs and version
352
- const properties = [
353
- createObjectPropertyNode('id', evalId),
354
- createObjectPropertyNode('evalId', stableEvalId),
355
- createObjectPropertyNode('version', version),
356
- createObjectPropertyNode('filename', filename),
357
- ];
358
-
359
- // Add agentId if available
360
- if (agentId) {
361
- properties.push(createObjectPropertyNode('agentId', agentId));
362
- }
363
-
364
- const metadataObj: ASTPropertyNode = {
365
- type: 'Property',
366
- kind: 'init',
367
- key: {
368
- type: 'Identifier',
369
- name: 'metadata',
370
- },
371
- value: {
372
- type: 'ObjectExpression',
373
- properties,
374
- } as ASTObjectExpression,
375
- };
376
-
377
- // Add metadata to the config object
378
- configObj.properties.push(metadataObj);
379
- }
380
-
381
- function findAgentVariableAndImport(
382
- ast: ASTProgram
383
- ): { varName: string; importPath: string } | undefined {
384
- // First, find what variable is being used in agent.createEval() calls
385
- let agentVarName: string | undefined;
386
-
387
- for (const node of ast.body) {
388
- if (node.type === 'ExportNamedDeclaration') {
389
- const exportDecl = node as {
390
- declaration?: { type: string; declarations?: Array<ASTVariableDeclarator> };
391
- };
392
- if (exportDecl.declaration?.type === 'VariableDeclaration') {
393
- const variableDeclaration = exportDecl.declaration as {
394
- declarations: Array<ASTVariableDeclarator>;
395
- };
396
-
397
- for (const vardecl of variableDeclaration.declarations) {
398
- if (
399
- vardecl.type === 'VariableDeclarator' &&
400
- vardecl.init?.type === 'CallExpression'
401
- ) {
402
- const call = vardecl.init as ASTCallExpression;
403
- if (call.callee.type === 'MemberExpression') {
404
- const memberExpr = call.callee as ASTMemberExpression;
405
- const object = memberExpr.object as ASTNodeIdentifier;
406
- const property = memberExpr.property as ASTNodeIdentifier;
407
- if (
408
- object.type === 'Identifier' &&
409
- property.type === 'Identifier' &&
410
- property.name === 'createEval'
411
- ) {
412
- agentVarName = object.name;
413
- break;
414
- }
415
- }
416
- }
417
- }
418
- if (agentVarName) break;
419
- }
420
- }
421
- }
422
-
423
- if (!agentVarName) return undefined;
424
-
425
- // Now find the import for this variable
426
- for (const node of ast.body) {
427
- if (node.type === 'ImportDeclaration') {
428
- const importDecl = node as unknown as {
429
- source: ASTLiteral;
430
- specifiers: Array<{
431
- type: string;
432
- local: ASTNodeIdentifier;
433
- }>;
434
- };
435
-
436
- // Find default import specifier that matches our variable
437
- for (const spec of importDecl.specifiers) {
438
- if (spec.type === 'ImportDefaultSpecifier' && spec.local.name === agentVarName) {
439
- const importPath = importDecl.source.value;
440
- if (typeof importPath === 'string') {
441
- return { varName: agentVarName, importPath };
442
- }
443
- }
444
- }
445
- }
446
- }
447
-
448
- return undefined;
449
- }
450
-
451
- export async function parseEvalMetadata(
452
- rootDir: string,
453
- filename: string,
454
- contents: string,
455
- projectId: string,
456
- deploymentId: string,
457
- agentId?: string,
458
- agentMetadata?: Map<string, Map<string, string>>
459
- ): Promise<
460
- [
461
- string,
462
- Array<{
463
- filename: string;
464
- id: string;
465
- version: string;
466
- name: string;
467
- evalId: string;
468
- description?: string;
469
- }>,
470
- ]
471
- > {
472
- const logLevel = (process.env.AGENTUITY_LOG_LEVEL || 'info') as
473
- | 'trace'
474
- | 'debug'
475
- | 'info'
476
- | 'warn'
477
- | 'error';
478
- const logger = createLogger(logLevel);
479
- logger.trace(`Parsing evals from ${filename}`);
480
-
481
- // Quick string search optimization - skip AST parsing if no createEval call
482
- if (!contents.includes('createEval')) {
483
- logger.trace(`Skipping ${filename}: no createEval found`);
484
- return [contents, []];
485
- }
486
-
487
- const ast = acornLoose.parse(contents, {
488
- locations: true,
489
- ecmaVersion: 'latest',
490
- sourceType: 'module',
491
- });
492
- const rel = toForwardSlash(relative(rootDir, filename));
493
- const version = hash(contents);
494
- const evals: Array<{
495
- filename: string;
496
- id: string;
497
- version: string;
498
- name: string;
499
- evalId: string;
500
- description?: string;
501
- }> = [];
502
-
503
- // Try to find the corresponding agent to get the agentId
504
- let resolvedAgentId = agentId;
505
- if (!resolvedAgentId && agentMetadata) {
506
- const agentInfo = findAgentVariableAndImport(ast);
507
- if (agentInfo) {
508
- logger.trace(
509
- `[EVAL METADATA] Found agent variable '${agentInfo.varName}' imported from '${agentInfo.importPath}'`
510
- );
511
-
512
- // Resolve the import path to actual file path
513
- let resolvedPath = agentInfo.importPath;
514
- if (resolvedPath.startsWith('./') || resolvedPath.startsWith('../')) {
515
- // Convert relative path to match the format in agentMetadata
516
- const baseDir = dirname(filename);
517
- resolvedPath = join(baseDir, resolvedPath);
518
- // Normalize and ensure .ts extension
519
- if (!resolvedPath.endsWith('.ts')) {
520
- resolvedPath += '.ts';
521
- }
522
- }
523
-
524
- // Find the agent metadata from the passed agentMetadata map
525
- for (const [agentFile, metadata] of agentMetadata) {
526
- // Check if this agent file matches the resolved import path
527
- if (agentFile.includes(basename(resolvedPath)) && metadata.has('agentId')) {
528
- resolvedAgentId = metadata.get('agentId');
529
- logger.trace(
530
- `[EVAL METADATA] Resolved agentId from agent metadata: ${resolvedAgentId} (file: ${agentFile})`
531
- );
532
- break;
533
- }
534
- }
535
-
536
- if (!resolvedAgentId) {
537
- logger.warn(
538
- `[EVAL METADATA] Could not find agent metadata for import path: ${resolvedPath}`
539
- );
540
- }
541
- }
542
- }
543
-
544
- // Find all exported agent.createEval() calls
545
- for (const body of ast.body) {
546
- let variableDeclaration: { declarations: Array<ASTVariableDeclarator> } | undefined;
547
-
548
- // Only process exported VariableDeclarations
549
- if (body.type === 'ExportNamedDeclaration') {
550
- const exportDecl = body as {
551
- declaration?: { type: string; declarations?: Array<ASTVariableDeclarator> };
552
- };
553
- if (exportDecl.declaration?.type === 'VariableDeclaration') {
554
- variableDeclaration = exportDecl.declaration as {
555
- declarations: Array<ASTVariableDeclarator>;
556
- };
557
- }
558
- }
559
-
560
- if (variableDeclaration) {
561
- for (const vardecl of variableDeclaration.declarations) {
562
- if (vardecl.type === 'VariableDeclarator' && vardecl.init?.type === 'CallExpression') {
563
- const call = vardecl.init as ASTCallExpression;
564
- if (call.callee.type === 'MemberExpression') {
565
- const memberExpr = call.callee as ASTMemberExpression;
566
- const object = memberExpr.object as ASTNodeIdentifier;
567
- const property = memberExpr.property as ASTNodeIdentifier;
568
- if (
569
- object.type === 'Identifier' &&
570
- object.name === 'agent' &&
571
- property.type === 'Identifier' &&
572
- property.name === 'createEval'
573
- ) {
574
- // Found agent.createEval() call
575
- // New signature: agent.createEval(name, { description?, handler })
576
- if (call.arguments.length >= 2) {
577
- const firstArg = call.arguments[0] as ASTNode;
578
- const secondArg = call.arguments[1] as ASTNode;
579
-
580
- let evalName: string | undefined;
581
- let evalDescription: string | undefined;
582
- let configObj: ASTObjectExpression | undefined;
583
-
584
- // First argument should be a string literal (the name)
585
- if (
586
- firstArg.type === 'Literal' &&
587
- typeof (firstArg as ASTLiteral).value === 'string'
588
- ) {
589
- evalName = (firstArg as ASTLiteral).value as string;
590
- } else {
591
- throw new MetadataError({
592
- filename,
593
- line: body.loc?.start?.line,
594
- message:
595
- 'agent.createEval() first argument must be a string literal name.',
596
- });
597
- }
598
-
599
- // Second argument should be the config object
600
- if (secondArg.type === 'ObjectExpression') {
601
- configObj = secondArg as ASTObjectExpression;
602
-
603
- // Extract description from config object
604
- for (const prop of configObj.properties) {
605
- if (
606
- prop.key.type === 'Identifier' &&
607
- prop.key.name === 'description'
608
- ) {
609
- if (prop.value.type === 'Literal') {
610
- const literalValue = (prop.value as ASTLiteral).value;
611
- evalDescription =
612
- typeof literalValue === 'string' ? literalValue : undefined;
613
- }
614
- }
615
- }
616
- }
617
-
618
- const finalName = evalName;
619
-
620
- logger.trace(
621
- `Found eval: ${finalName}${evalDescription ? ` - ${evalDescription}` : ''}`
622
- );
623
- const evalId = getEvalId(projectId, deploymentId, rel, finalName, version);
624
-
625
- // Generate stable evalId using resolved agentId
626
- const effectiveAgentId = resolvedAgentId || '';
627
- const stableEvalId = generateStableEvalId(
628
- projectId,
629
- effectiveAgentId,
630
- finalName
631
- );
632
-
633
- // Inject eval metadata into the AST (same pattern as agents)
634
- if (configObj) {
635
- injectEvalMetadata(
636
- configObj,
637
- evalId,
638
- stableEvalId,
639
- version,
640
- rel,
641
- resolvedAgentId
642
- );
643
- }
644
-
645
- evals.push({
646
- filename: rel,
647
- id: evalId,
648
- version,
649
- name: finalName,
650
- evalId: stableEvalId,
651
- description: evalDescription,
652
- });
653
- }
654
- }
655
- }
656
- }
657
- }
658
- }
659
- }
660
-
661
- // Check for duplicate eval names in the same file
662
- // This prevents hash collisions when projectId/deploymentId are empty
663
- const seenNames = new Map<string, number>();
664
- for (const evalItem of evals) {
665
- const count = seenNames.get(evalItem.name) || 0;
666
- seenNames.set(evalItem.name, count + 1);
667
- }
668
-
669
- const duplicates: string[] = [];
670
- for (const [name, count] of seenNames.entries()) {
671
- if (count > 1) {
672
- duplicates.push(name);
673
- }
674
- }
675
-
676
- if (duplicates.length > 0) {
677
- throw new DuplicateNameError({
678
- filename,
679
- message:
680
- `Duplicate eval names found in ${rel}: ${duplicates.join(', ')}. ` +
681
- 'Eval names must be unique within the same file to prevent ID collisions.',
682
- });
683
- }
684
-
685
- const newsource = generate(ast);
686
- logger.trace(`Parsed ${evals.length} eval(s) from ${filename}`);
687
- return [newsource, evals];
688
- }
689
-
690
- const InvalidExportError = StructuredError('InvalidExportError')<{ filename: string }>();
691
-
692
- export async function parseAgentMetadata(
693
- rootDir: string,
694
- filename: string,
695
- contents: string,
696
- projectId: string,
697
- deploymentId: string
698
- ): Promise<[string, Map<string, string>] | undefined> {
699
- // Quick string search optimization - skip AST parsing if no createAgent call
700
- if (!contents.includes('createAgent')) {
701
- return undefined;
702
- }
703
-
704
- const ast = acornLoose.parse(contents, {
705
- locations: true,
706
- ecmaVersion: 'latest',
707
- sourceType: 'module',
708
- });
709
- let exportName: string | undefined;
710
- const rel = toForwardSlash(relative(rootDir, filename));
711
- let name: string | undefined; // Will be set from createAgent identifier
712
- const version = hash(contents);
713
- const id = getAgentId(projectId, deploymentId, rel, version);
714
-
715
- let result: [string, Map<string, string>] | undefined;
716
- let schemaCodeExtracted = false;
717
-
718
- for (const body of ast.body) {
719
- if (body.type === 'ExportDefaultDeclaration') {
720
- if (body.declaration?.type === 'CallExpression') {
721
- const call = body.declaration as ASTCallExpression;
722
- if (call.callee.name === 'createAgent') {
723
- // Enforce new API: createAgent('name', {config})
724
- if (call.arguments.length < 2) {
725
- throw new Error(
726
- `createAgent requires 2 arguments: createAgent('name', config) in ${filename}`
727
- );
728
- }
729
-
730
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
731
- const nameArg = call.arguments[0] as any;
732
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
733
- const configArg = call.arguments[1] as any;
734
-
735
- if (!nameArg || nameArg.type !== 'Literal' || typeof nameArg.value !== 'string') {
736
- throw new Error(
737
- `createAgent first argument must be a string literal in ${filename}`
738
- );
739
- }
740
-
741
- if (!isObjectExpression(configArg)) {
742
- throw new Error(
743
- `createAgent second argument must be a config object in ${filename}`
744
- );
745
- }
746
-
747
- // Extract agent identifier from createAgent first argument
748
- name = nameArg.value;
749
-
750
- const callargexp = configArg;
751
-
752
- // Extract schema code before processing metadata
753
- let inputSchemaCode: string | undefined;
754
- let outputSchemaCode: string | undefined;
755
- if (!schemaCodeExtracted) {
756
- const schemaCode = extractSchemaCode(callargexp);
757
- inputSchemaCode = schemaCode.inputSchemaCode;
758
- outputSchemaCode = schemaCode.outputSchemaCode;
759
- schemaCodeExtracted = true;
760
- }
761
-
762
- for (const prop of callargexp.properties) {
763
- if (prop.key.type === 'Identifier' && prop.key.name === 'metadata') {
764
- result = augmentAgentMetadataNode(
765
- projectId,
766
- id,
767
- rel,
768
- version,
769
- ast,
770
- prop.value as ASTObjectExpression,
771
- filename,
772
- inputSchemaCode,
773
- outputSchemaCode
774
- );
775
- break;
776
- }
777
- }
778
- if (!result && name) {
779
- result = createAgentMetadataNode(
780
- id,
781
- name,
782
- rel,
783
- version,
784
- ast,
785
- callargexp,
786
- filename,
787
- projectId,
788
- inputSchemaCode,
789
- outputSchemaCode
790
- );
791
- }
792
- break;
793
- }
794
- }
795
- }
796
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
797
- if (!result && (body as any).declaration?.type === 'Identifier') {
798
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
799
- const identifier = (body as any).declaration as ASTNodeIdentifier;
800
- exportName = identifier.name;
801
- break;
802
- }
803
- }
804
- // If no default export or createAgent found, skip this file (it's not an agent)
805
- if (!result && !exportName) {
806
- return undefined;
807
- }
808
- if (!result) {
809
- for (const body of ast.body) {
810
- if (body.type === 'VariableDeclaration') {
811
- for (const vardecl of body.declarations) {
812
- if (vardecl.type === 'VariableDeclarator' && vardecl.id.type === 'Identifier') {
813
- const identifier = vardecl.id as ASTNodeIdentifier;
814
- if (identifier.name === exportName) {
815
- if (vardecl.init?.type === 'CallExpression') {
816
- const call = vardecl.init as ASTCallExpression;
817
- if (call.callee.name === 'createAgent') {
818
- // Enforce new API: createAgent('name', {config})
819
- if (call.arguments.length < 2) {
820
- throw new Error(
821
- `createAgent requires 2 arguments: createAgent('name', config) in ${filename}`
822
- );
823
- }
824
-
825
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
826
- const nameArg = call.arguments[0] as any;
827
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
828
- const configArg = call.arguments[1] as any;
829
-
830
- if (
831
- !nameArg ||
832
- nameArg.type !== 'Literal' ||
833
- typeof nameArg.value !== 'string'
834
- ) {
835
- throw new Error(
836
- `createAgent first argument must be a string literal in ${filename}`
837
- );
838
- }
839
-
840
- if (!isObjectExpression(configArg)) {
841
- throw new Error(
842
- `createAgent second argument must be a config object in ${filename}`
843
- );
844
- }
845
-
846
- // Extract agent identifier from createAgent first argument
847
- name = nameArg.value;
848
-
849
- const callargexp = configArg;
850
-
851
- // Extract schema code before processing metadata
852
- let inputSchemaCode: string | undefined;
853
- let outputSchemaCode: string | undefined;
854
- if (!schemaCodeExtracted) {
855
- const schemaCode = extractSchemaCode(callargexp);
856
- inputSchemaCode = schemaCode.inputSchemaCode;
857
- outputSchemaCode = schemaCode.outputSchemaCode;
858
- schemaCodeExtracted = true;
859
- }
860
-
861
- for (const prop of callargexp.properties) {
862
- if (prop.key.type === 'Identifier' && prop.key.name === 'metadata') {
863
- result = augmentAgentMetadataNode(
864
- projectId,
865
- id,
866
- rel,
867
- version,
868
- ast,
869
- prop.value as ASTObjectExpression,
870
- filename,
871
- inputSchemaCode,
872
- outputSchemaCode
873
- );
874
- break;
875
- }
876
- }
877
- if (!result && name) {
878
- result = createAgentMetadataNode(
879
- id,
880
- name,
881
- rel,
882
- version,
883
- ast,
884
- callargexp,
885
- filename,
886
- projectId,
887
- inputSchemaCode,
888
- outputSchemaCode
889
- );
890
- }
891
- break;
892
- }
893
- }
894
- }
895
- }
896
- }
897
- }
898
- }
899
- }
900
- // If no createAgent found after checking all declarations, skip this file
901
- if (!result) {
902
- return undefined;
903
- }
904
-
905
- // Parse evals from eval.ts file in the same directory
906
- const logLevel = (process.env.AGENTUITY_LOG_LEVEL || 'info') as
907
- | 'trace'
908
- | 'debug'
909
- | 'info'
910
- | 'warn'
911
- | 'error';
912
- const logger = createLogger(logLevel);
913
- const agentDir = dirname(filename);
914
- const evalsPath = join(agentDir, 'eval.ts');
915
- logger.trace(`Checking for evals file at ${evalsPath}`);
916
- const evalsFile = Bun.file(evalsPath);
917
- if (await evalsFile.exists()) {
918
- logger.trace(`Found evals file at ${evalsPath}, parsing...`);
919
- const evalsSource = await evalsFile.text();
920
- const transpiler = new Bun.Transpiler({ loader: 'ts', target: 'bun' });
921
- const evalsContents = transpiler.transformSync(evalsSource);
922
- const agentId = result[1].get('agentId') || '';
923
- const [, evals] = await parseEvalMetadata(
924
- rootDir,
925
- evalsPath,
926
- evalsContents,
927
- projectId,
928
- deploymentId,
929
- agentId,
930
- new Map() // Empty map since we already have agentId
931
- );
932
- if (evals.length > 0) {
933
- logger.trace(`Adding ${evals.length} eval(s) to agent metadata for ${name}`);
934
- result[1].set('evals', JSON.stringify(evals));
935
- } else {
936
- logger.trace(`No evals found in ${evalsPath}`);
937
- }
938
- } else {
939
- logger.trace(`No evals file found at ${evalsPath}`);
940
- }
941
-
942
- return result;
943
- }
944
-
945
- type RouteDefinition = BuildMetadata['routes'];
946
-
947
- const InvalidCreateRouterError = StructuredError('InvalidCreateRouterError')<{
948
- filename: string;
949
- }>();
950
-
951
- const InvalidRouterConfigError = StructuredError('InvalidRouterConfigError')<{
952
- filename: string;
953
- line?: number;
954
- }>();
955
-
956
- const SchemaNotExportedError = StructuredError('SchemaNotExportedError')<{
957
- filename: string;
958
- schemaName: string;
959
- kind: 'input' | 'output';
960
- method?: string;
961
- path?: string;
962
- }>();
963
-
964
- /**
965
- * Build a set of exported identifiers from the top-level program.
966
- * Handles both `export const X = ...` and `export { X }` patterns.
967
- */
968
- function buildExportedIdentifierSet(program: ASTProgram): Set<string> {
969
- const exported = new Set<string>();
970
-
971
- for (const node of program.body) {
972
- if (node.type === 'ExportNamedDeclaration') {
973
- const exp = node as unknown as {
974
- declaration?: ASTNode;
975
- specifiers?: Array<{ local?: ASTNodeIdentifier; exported?: ASTNodeIdentifier }>;
976
- };
977
-
978
- // Handle `export const X = ...` or `export function X() { ... }` or `export class X { ... }`
979
- if (exp.declaration) {
980
- if (exp.declaration.type === 'VariableDeclaration') {
981
- const decl = exp.declaration as unknown as { declarations: ASTVariableDeclarator[] };
982
- for (const d of decl.declarations) {
983
- if (d.id.type === 'Identifier') {
984
- const id = d.id as ASTNodeIdentifier;
985
- exported.add(id.name);
986
- }
987
- }
988
- } else if (exp.declaration.type === 'FunctionDeclaration') {
989
- const funcDecl = exp.declaration as unknown as { id?: ASTNodeIdentifier };
990
- if (funcDecl.id?.name) {
991
- exported.add(funcDecl.id.name);
992
- }
993
- } else if (exp.declaration.type === 'ClassDeclaration') {
994
- const classDecl = exp.declaration as unknown as { id?: ASTNodeIdentifier };
995
- if (classDecl.id?.name) {
996
- exported.add(classDecl.id.name);
997
- }
998
- }
999
- }
1000
-
1001
- // Handle `export { X }` or `export { X as Y }`
1002
- if (exp.specifiers && Array.isArray(exp.specifiers)) {
1003
- for (const spec of exp.specifiers) {
1004
- // For `export { X }`, local.name is the variable name in this file
1005
- if (spec.local?.name) {
1006
- exported.add(spec.local.name);
1007
- }
1008
- }
1009
- }
1010
- }
1011
- }
1012
-
1013
- return exported;
1014
- }
1015
-
1016
- /**
1017
- * Validate that schema variables used in validators are either imported or exported.
1018
- * Throws SchemaNotExportedError if a locally-defined schema is not exported.
1019
- */
1020
- function validateSchemaExports(
1021
- schemaVariable: string | undefined,
1022
- kind: 'input' | 'output',
1023
- importedNames: Set<string>,
1024
- exportedNames: Set<string>,
1025
- filename: string,
1026
- method?: string,
1027
- path?: string
1028
- ): void {
1029
- if (!schemaVariable) return;
1030
-
1031
- // If the schema is imported from another file, it's already exported from its source
1032
- if (importedNames.has(schemaVariable)) return;
1033
-
1034
- // If the schema is defined locally, it must be exported
1035
- if (!exportedNames.has(schemaVariable)) {
1036
- const routeDesc = method && path ? ` for route "${method.toUpperCase()} ${path}"` : '';
1037
- throw new SchemaNotExportedError({
1038
- filename,
1039
- schemaName: schemaVariable,
1040
- kind,
1041
- method,
1042
- path,
1043
- message:
1044
- `Schema "${schemaVariable}" used as the ${kind} validator${routeDesc} in ${filename} is not exported.\n\n` +
1045
- `Agentuity generates a route registry that imports schema types by name, so the schema must be exported.\n\n` +
1046
- `To fix this, add "export" to the schema declaration:\n\n` +
1047
- ` export const ${schemaVariable} = s.object({ ... });\n`,
1048
- });
1049
- }
1050
- }
1051
-
1052
- /**
1053
- * Information extracted from validator middleware in route handler arguments.
1054
- */
1055
- interface ValidatorInfo {
1056
- hasValidator: boolean;
1057
- agentVariable?: string;
1058
- inputSchemaVariable?: string;
1059
- outputSchemaVariable?: string;
1060
- stream?: boolean;
1061
- }
1062
-
1063
- /**
1064
- * Scan route handler arguments for validator middleware and extract schema information.
1065
- *
1066
- * Accumulates schema info across ALL validator arguments in the route handler,
1067
- * supporting common patterns like combining param + JSON body validation:
1068
- *
1069
- * ```ts
1070
- * router.patch('/:id',
1071
- * zValidator('param', paramSchema), // detected, no schema extracted (non-json)
1072
- * zValidator('json', bodySchema), // detected, inputSchemaVariable = 'bodySchema'
1073
- * async (c) => { ... }
1074
- * );
1075
- * ```
1076
- *
1077
- * **Schema merge strategy — first match wins:**
1078
- * When multiple validators provide the same schema field (e.g., two `inputSchemaVariable`
1079
- * providers), the first one encountered is kept. This is intentional because:
1080
- *
1081
- * 1. Primary validators (e.g., `validator({ input, output })`, `agent.validator()`) are
1082
- * conventionally listed before supplementary validators (param, query, header, cookie).
1083
- * 2. For `zValidator`, only `'json'` targets extract schemas — other targets (param, query,
1084
- * header, cookie) return no schema variables, so ordering rarely matters in practice.
1085
- * 3. Duplicate json validators on the same route is uncommon; when it occurs, a warning
1086
- * is logged to help developers catch unintentional conflicts.
1087
- *
1088
- * Supported validator patterns:
1089
- * - `validator({ input, output, stream })` — Agentuity object-style
1090
- * - `validator('json', callback)` — Hono callback-style
1091
- * - `zValidator('json', schema)` — Zod validator (only 'json' target extracts schemas)
1092
- * - `agent.validator()` / `agent.validator({ input, output })` — Agent validators
1093
- *
1094
- * @param args - The arguments array from a route handler call expression (e.g., `router.post(path, ...args)`)
1095
- * @returns Accumulated validator info with merged schemas from all validators found
1096
- */
1097
- function hasValidatorCall(args: unknown[]): ValidatorInfo {
1098
- if (!args || args.length === 0) return { hasValidator: false };
1099
-
1100
- const result: ValidatorInfo = { hasValidator: false };
1101
-
1102
- // Helper: merge a schema field using first-match-wins strategy, warn on conflict.
1103
- // When a field is already set and a different value is encountered, the first value
1104
- // is kept and a warning is emitted to help developers catch unintentional duplicates.
1105
- const mergeField = <K extends 'inputSchemaVariable' | 'outputSchemaVariable'>(
1106
- field: K,
1107
- value: string | undefined
1108
- ) => {
1109
- if (!value) return;
1110
- if (result[field] && result[field] !== value) {
1111
- const label = field === 'inputSchemaVariable' ? 'inputSchema' : 'outputSchema';
1112
- logger.warn(
1113
- 'Multiple validators provide %s: using "%s", ignoring "%s"',
1114
- label,
1115
- result[field],
1116
- value
1117
- );
1118
- } else if (!result[field]) {
1119
- result[field] = value;
1120
- }
1121
- };
1122
-
1123
- for (const arg of args) {
1124
- if (!arg || typeof arg !== 'object') continue;
1125
- const node = arg as ASTNode;
1126
-
1127
- // Check if this is a CallExpression with callee named 'validator'
1128
- if (node.type === 'CallExpression') {
1129
- const callExpr = node as ASTCallExpression;
1130
-
1131
- // Check for standalone validator({ input, output }) or Hono validator('json', callback)
1132
- if (callExpr.callee.type === 'Identifier') {
1133
- const identifier = callExpr.callee as ASTNodeIdentifier;
1134
- if (identifier.name === 'validator') {
1135
- // Try to extract schema variables from validator({ input, output, stream })
1136
- const schemas = extractValidatorSchemas(callExpr);
1137
- // If we found schemas from object-style validator, merge them
1138
- if (
1139
- schemas.inputSchemaVariable ||
1140
- schemas.outputSchemaVariable ||
1141
- schemas.stream !== undefined
1142
- ) {
1143
- result.hasValidator = true;
1144
- mergeField('inputSchemaVariable', schemas.inputSchemaVariable);
1145
- mergeField('outputSchemaVariable', schemas.outputSchemaVariable);
1146
- if (schemas.stream !== undefined && result.stream === undefined) {
1147
- result.stream = schemas.stream;
1148
- }
1149
- continue;
1150
- }
1151
- // Try Hono validator('json', callback) pattern
1152
- const honoSchemas = extractHonoValidatorSchema(callExpr);
1153
- result.hasValidator = true;
1154
- mergeField('inputSchemaVariable', honoSchemas.inputSchemaVariable);
1155
- continue;
1156
- }
1157
- // Check for zValidator('json', schema)
1158
- if (identifier.name === 'zValidator') {
1159
- const schemas = extractZValidatorSchema(callExpr);
1160
- result.hasValidator = true;
1161
- mergeField('inputSchemaVariable', schemas.inputSchemaVariable);
1162
- continue;
1163
- }
1164
- }
1165
-
1166
- // Check for agent.validator()
1167
- if (callExpr.callee.type === 'MemberExpression') {
1168
- const member = callExpr.callee as ASTMemberExpression;
1169
- if (member.property && (member.property as ASTNodeIdentifier).name === 'validator') {
1170
- // Extract agent variable name (the object before .validator())
1171
- const agentVariable =
1172
- member.object.type === 'Identifier'
1173
- ? (member.object as ASTNodeIdentifier).name
1174
- : undefined;
1175
- // Also check for schema overrides: agent.validator({ input, output })
1176
- const schemas = extractValidatorSchemas(callExpr);
1177
- result.hasValidator = true;
1178
- if (agentVariable && !result.agentVariable) {
1179
- result.agentVariable = agentVariable;
1180
- }
1181
- mergeField('inputSchemaVariable', schemas.inputSchemaVariable);
1182
- mergeField('outputSchemaVariable', schemas.outputSchemaVariable);
1183
- if (schemas.stream !== undefined && result.stream === undefined) {
1184
- result.stream = schemas.stream;
1185
- }
1186
- continue;
1187
- }
1188
- }
1189
- }
1190
- }
1191
-
1192
- return result;
1193
- }
1194
-
1195
- /**
1196
- * Extract schema variable names and stream flag from validator() call arguments
1197
- * Example: validator({ input: myInputSchema, output: myOutputSchema, stream: true })
1198
- */
1199
- function extractValidatorSchemas(callExpr: ASTCallExpression): {
1200
- inputSchemaVariable?: string;
1201
- outputSchemaVariable?: string;
1202
- stream?: boolean;
1203
- } {
1204
- const result: { inputSchemaVariable?: string; outputSchemaVariable?: string; stream?: boolean } =
1205
- {};
1206
-
1207
- // Check if validator has arguments
1208
- if (!callExpr.arguments || callExpr.arguments.length === 0) {
1209
- return result;
1210
- }
1211
-
1212
- // First argument should be an object expression
1213
- const firstArg = callExpr.arguments[0] as ASTNode;
1214
- if (!firstArg || firstArg.type !== 'ObjectExpression') {
1215
- return result;
1216
- }
1217
-
1218
- const objExpr = firstArg as ASTObjectExpression;
1219
- for (const prop of objExpr.properties) {
1220
- // Extract key name defensively - could be Identifier or Literal
1221
- let keyName: string | undefined;
1222
- const propKey = prop.key as { type: string; name?: string; value?: unknown };
1223
- if (propKey.type === 'Identifier') {
1224
- keyName = propKey.name;
1225
- } else if (propKey.type === 'Literal') {
1226
- keyName = String(propKey.value);
1227
- }
1228
-
1229
- if (!keyName) continue;
1230
-
1231
- if ((keyName === 'input' || keyName === 'output') && prop.value.type === 'Identifier') {
1232
- const valueName = (prop.value as ASTNodeIdentifier).name;
1233
- if (keyName === 'input') {
1234
- result.inputSchemaVariable = valueName;
1235
- } else {
1236
- result.outputSchemaVariable = valueName;
1237
- }
1238
- }
1239
- // Extract stream flag - can be Literal, Identifier, or UnaryExpression (!0 or !1)
1240
- if (keyName === 'stream') {
1241
- if (prop.value.type === 'Literal') {
1242
- const literal = prop.value as ASTLiteral;
1243
- if (typeof literal.value === 'boolean') {
1244
- result.stream = literal.value;
1245
- }
1246
- } else if (prop.value.type === 'Identifier') {
1247
- const identifier = prop.value as ASTNodeIdentifier;
1248
- // Handle stream: true or stream: false as identifiers
1249
- if (identifier.name === 'true') {
1250
- result.stream = true;
1251
- } else if (identifier.name === 'false') {
1252
- result.stream = false;
1253
- }
1254
- } else if (prop.value.type === 'UnaryExpression') {
1255
- // Handle !0 (true) or !1 (false) - acorn-loose transpiles booleans this way
1256
- const unary = prop.value as { type: string; operator?: string; argument?: ASTNode };
1257
- if (unary.argument?.type === 'Literal') {
1258
- const literal = unary.argument as ASTLiteral;
1259
- // Numeric literal: !0 = true, !1 = false
1260
- if (typeof literal.value === 'number') {
1261
- if (unary.operator === '!') {
1262
- result.stream = literal.value === 0;
1263
- }
1264
- } else if (typeof literal.value === 'boolean') {
1265
- result.stream = unary.operator === '!' ? !literal.value : literal.value;
1266
- }
1267
- }
1268
- // Handle true/false as identifiers
1269
- if (unary.argument?.type === 'Identifier') {
1270
- const identifier = unary.argument as ASTNodeIdentifier;
1271
- if (identifier.name === 'true') {
1272
- result.stream = unary.operator !== '!';
1273
- } else if (identifier.name === 'false') {
1274
- result.stream = unary.operator === '!';
1275
- }
1276
- }
1277
- }
1278
- }
1279
- }
1280
-
1281
- return result;
1282
- }
1283
-
1284
- /**
1285
- * Extract schema from zValidator() call arguments
1286
- * Example: zValidator('json', mySchema) or zValidator('json', z.object({...}))
1287
- * Returns the schema as inputSchemaVariable since zValidator is for request body validation
1288
- * Only extracts schemas for 'json' target, not 'query', 'param', 'header', or 'cookie'
1289
- */
1290
- function extractZValidatorSchema(callExpr: ASTCallExpression): {
1291
- inputSchemaVariable?: string;
1292
- } {
1293
- const result: { inputSchemaVariable?: string } = {};
1294
-
1295
- // zValidator requires at least 2 arguments: zValidator(target, schema)
1296
- if (!callExpr.arguments || callExpr.arguments.length < 2) {
1297
- return result;
1298
- }
1299
-
1300
- // First argument should be 'json' literal
1301
- const targetArg = callExpr.arguments[0] as ASTNode;
1302
- if (targetArg.type === 'Literal') {
1303
- const targetValue = (targetArg as ASTLiteral).value;
1304
- // Only extract schemas for JSON body validation
1305
- if (typeof targetValue === 'string' && targetValue !== 'json') {
1306
- return result;
1307
- }
1308
- } else {
1309
- // If first arg is not a literal, we can't determine the target, skip
1310
- return result;
1311
- }
1312
-
1313
- // Second argument is the schema
1314
- const schemaArg = callExpr.arguments[1] as ASTNode;
1315
-
1316
- // If it's an identifier (variable reference), extract the name
1317
- if (schemaArg.type === 'Identifier') {
1318
- result.inputSchemaVariable = (schemaArg as ASTNodeIdentifier).name;
1319
- }
1320
- // If it's inline schema (CallExpression like z.object({...})), we detect but don't extract yet
1321
- // TODO: Extract inline schema code
1322
-
1323
- return result;
1324
- }
1325
-
1326
- /**
1327
- * Extract output schema from SSE options object.
1328
- * Example: sse({ output: MySchema }, handler)
1329
- *
1330
- * @param callExpr - The SSE CallExpression AST node
1331
- * @returns Object with outputSchemaVariable if found
1332
- */
1333
- function extractSSEOutputSchema(callExpr: ASTCallExpression): {
1334
- outputSchemaVariable?: string;
1335
- } {
1336
- const result: { outputSchemaVariable?: string } = {};
1337
-
1338
- // sse() can be called as:
1339
- // 1. sse(handler) - no schema
1340
- // 2. sse({ output: schema }, handler) - with schema
1341
- if (!callExpr.arguments || callExpr.arguments.length === 0) {
1342
- return result;
1343
- }
1344
-
1345
- // Check if first argument is an options object with 'output' property
1346
- const firstArg = callExpr.arguments[0] as ASTNode;
1347
- if (firstArg.type !== 'ObjectExpression') {
1348
- // First argument is handler function, no options
1349
- return result;
1350
- }
1351
-
1352
- const objExpr = firstArg as ASTObjectExpression;
1353
- for (const prop of objExpr.properties) {
1354
- // Skip SpreadElement entries (e.g., { ...obj }) which don't have key/value
1355
- if ((prop as ASTNode).type !== 'Property') {
1356
- continue;
1357
- }
1358
-
1359
- // Extract key name - could be Identifier or Literal
1360
- let keyName: string | undefined;
1361
- const propKey = prop.key as { type: string; name?: string; value?: unknown };
1362
- if (propKey.type === 'Identifier') {
1363
- keyName = propKey.name;
1364
- } else if (propKey.type === 'Literal') {
1365
- keyName = String(propKey.value);
1366
- }
1367
-
1368
- if (!keyName) continue;
1369
-
1370
- // Look for the 'output' property
1371
- if (keyName === 'output' && prop.value.type === 'Identifier') {
1372
- result.outputSchemaVariable = (prop.value as ASTNodeIdentifier).name;
1373
- break;
1374
- }
1375
- }
1376
-
1377
- return result;
1378
- }
1379
-
1380
- /**
1381
- * Extract schema from Hono validator('json', callback) pattern
1382
- * Example: validator('json', (value, c) => { const result = mySchema['~standard'].validate(value); ... })
1383
- * Searches the callback function body for schema.validate() or schema['~standard'].validate() calls
1384
- */
1385
- function extractHonoValidatorSchema(callExpr: ASTCallExpression): {
1386
- inputSchemaVariable?: string;
1387
- } {
1388
- const result: { inputSchemaVariable?: string } = {};
1389
-
1390
- // Hono validator requires at least 2 arguments: validator(target, callback)
1391
- if (!callExpr.arguments || callExpr.arguments.length < 2) {
1392
- return result;
1393
- }
1394
-
1395
- // First argument should be 'json' literal (only extract for JSON validation)
1396
- const targetArg = callExpr.arguments[0] as ASTNode;
1397
- if (targetArg.type === 'Literal') {
1398
- const targetValue = (targetArg as ASTLiteral).value;
1399
- if (typeof targetValue === 'string' && targetValue !== 'json') {
1400
- return result;
1401
- }
1402
- } else {
1403
- return result;
1404
- }
1405
-
1406
- // Second argument should be a function (arrow or regular)
1407
- const callbackArg = callExpr.arguments[1] as ASTNode;
1408
- if (
1409
- callbackArg.type !== 'ArrowFunctionExpression' &&
1410
- callbackArg.type !== 'FunctionExpression'
1411
- ) {
1412
- return result;
1413
- }
1414
-
1415
- // Get the function body
1416
- const funcExpr = callbackArg as {
1417
- body?: ASTNode;
1418
- };
1419
-
1420
- if (!funcExpr.body) {
1421
- return result;
1422
- }
1423
-
1424
- // Search the function body for schema.validate() or schema['~standard'].validate() calls
1425
- const schemaVar = findSchemaValidateCall(funcExpr.body);
1426
- if (schemaVar) {
1427
- result.inputSchemaVariable = schemaVar;
1428
- }
1429
-
1430
- return result;
1431
- }
1432
-
1433
- /**
1434
- * Recursively search AST for schema.validate() or schema['~standard'].validate() calls
1435
- * Returns the schema variable name if found
1436
- */
1437
- function findSchemaValidateCall(node: ASTNode): string | undefined {
1438
- if (!node || typeof node !== 'object') return undefined;
1439
-
1440
- // Check if this is a CallExpression with .validate()
1441
- if (node.type === 'CallExpression') {
1442
- const callExpr = node as ASTCallExpression;
1443
-
1444
- // Check for schema['~standard'].validate(value) pattern
1445
- // AST: CallExpression -> MemberExpression(validate) -> MemberExpression(['~standard']) -> Identifier(schema)
1446
- if (callExpr.callee.type === 'MemberExpression') {
1447
- const member = callExpr.callee as ASTMemberExpression;
1448
- const propName =
1449
- member.property.type === 'Identifier'
1450
- ? (member.property as ASTNodeIdentifier).name
1451
- : undefined;
1452
-
1453
- if (propName === 'validate') {
1454
- // Check if the object is schema['~standard'] or just schema
1455
- if (member.object.type === 'MemberExpression') {
1456
- // schema['~standard'].validate() pattern
1457
- const innerMember = member.object as ASTMemberExpression;
1458
- if (innerMember.object.type === 'Identifier') {
1459
- return (innerMember.object as ASTNodeIdentifier).name;
1460
- }
1461
- } else if (member.object.type === 'Identifier') {
1462
- // schema.validate() pattern
1463
- return (member.object as ASTNodeIdentifier).name;
1464
- }
1465
- }
1466
- }
1467
- }
1468
-
1469
- // Recursively search child nodes
1470
- for (const key of Object.keys(node)) {
1471
- const value = (node as unknown as Record<string, unknown>)[key];
1472
- if (Array.isArray(value)) {
1473
- for (const item of value) {
1474
- if (item && typeof item === 'object') {
1475
- const found = findSchemaValidateCall(item as ASTNode);
1476
- if (found) return found;
1477
- }
1478
- }
1479
- } else if (value && typeof value === 'object') {
1480
- const found = findSchemaValidateCall(value as ASTNode);
1481
- if (found) return found;
1482
- }
1483
- }
1484
-
1485
- return undefined;
1486
- }
1487
-
1488
- /**
1489
- * Resolve an import path to an actual file on disk.
1490
- * Tries the path as-is, then with common extensions.
1491
- * Returns null for non-relative (package) imports or if no file is found.
1492
- */
1493
- function resolveImportPath(fromDir: string, importPath: string): string | null {
1494
- // If it's a package import (not relative), skip
1495
- if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
1496
- return null;
1497
- }
1498
-
1499
- const basePath = resolve(fromDir, importPath);
1500
- const extensions = ['.ts', '.tsx', '/index.ts', '/index.tsx'];
1501
-
1502
- // Try exact path first (might already have extension)
1503
- if (existsSync(basePath)) {
1504
- try {
1505
- const stat = statSync(basePath);
1506
- if (stat.isFile()) return basePath;
1507
- } catch {
1508
- // ignore stat errors
1509
- }
1510
- }
1511
-
1512
- // Try with extensions
1513
- for (const ext of extensions) {
1514
- const candidate = basePath + ext;
1515
- if (existsSync(candidate)) {
1516
- return candidate;
1517
- }
1518
- }
1519
-
1520
- return null;
1521
- }
1522
-
1523
- /**
1524
- * Check if a CallExpression is a chained router initializer.
1525
- * Walks up the callee chain looking for createRouter() or new Hono() at the root.
1526
- *
1527
- * Example AST for `createRouter().get('/foo', handler).post('/bar', handler)`:
1528
- * ```
1529
- * CallExpression (.post)
1530
- * callee: MemberExpression
1531
- * object: CallExpression (.get)
1532
- * callee: MemberExpression
1533
- * object: CallExpression (createRouter)
1534
- * property: "post"
1535
- * ```
1536
- */
1537
- function isChainedRouterInit(node: ASTNode): boolean {
1538
- let current: ASTNode = node;
1539
-
1540
- // Walk down the chain: each link is CallExpression → MemberExpression → CallExpression
1541
- while (current.type === 'CallExpression') {
1542
- const callee = (current as ASTCallExpression).callee as ASTNode;
1543
- if (!callee) return false;
1544
-
1545
- // Direct createRouter()
1546
- if (callee.type === 'Identifier' && (callee as ASTNodeIdentifier).name === 'createRouter')
1547
- return true;
1548
-
1549
- // Chained: .method() → MemberExpression
1550
- if (callee.type === 'MemberExpression' && (callee as ASTMemberExpression).object) {
1551
- current = (callee as ASTMemberExpression).object as ASTNode;
1552
- continue;
1553
- }
1554
-
1555
- break;
1556
- }
1557
-
1558
- // Check if we landed on createRouter() or new Hono()
1559
- if (current.type === 'CallExpression') {
1560
- const callee = (current as ASTCallExpression).callee as ASTNode;
1561
- if (callee?.type === 'Identifier' && (callee as ASTNodeIdentifier).name === 'createRouter')
1562
- return true;
1563
- }
1564
- if (current.type === 'NewExpression') {
1565
- const callee = (current as ASTCallExpression).callee as ASTNode;
1566
- if (callee?.type === 'Identifier' && (callee as ASTNodeIdentifier).name === 'Hono')
1567
- return true;
1568
- }
1569
-
1570
- return false;
1571
- }
1572
-
1573
- /**
1574
- * Flatten a chained call expression into individual method calls.
1575
- *
1576
- * Given `createRouter().get('/a', h1).post('/b', h2).route('/c', sub)`, returns:
1577
- * ```
1578
- * [
1579
- * { method: 'get', arguments: ['/a', h1] },
1580
- * { method: 'post', arguments: ['/b', h2] },
1581
- * { method: 'route', arguments: ['/c', sub] },
1582
- * ]
1583
- * ```
1584
- */
1585
- function flattenChainedCalls(node: ASTNode): Array<{ method: string; arguments: unknown[] }> {
1586
- const calls: Array<{ method: string; arguments: unknown[] }> = [];
1587
-
1588
- let current: ASTNode = node;
1589
- while (current.type === 'CallExpression') {
1590
- const callee = (current as ASTCallExpression).callee as ASTNode;
1591
- if (!callee) break;
1592
-
1593
- if (
1594
- callee.type === 'MemberExpression' &&
1595
- ((callee as ASTMemberExpression).property as ASTNode)?.type === 'Identifier'
1596
- ) {
1597
- calls.unshift({
1598
- method: ((callee as ASTMemberExpression).property as ASTNodeIdentifier).name,
1599
- arguments: (current as ASTCallExpression).arguments || [],
1600
- });
1601
- current = (callee as ASTMemberExpression).object as ASTNode;
1602
- continue;
1603
- }
1604
-
1605
- break; // Reached the root (createRouter() / new Hono())
1606
- }
1607
-
1608
- return calls;
1609
- }
1610
-
1611
- export interface ParseRouteOptions {
1612
- visitedFiles?: Set<string>;
1613
- mountedSubrouters?: Set<string>;
1614
- /**
1615
- * Explicit mount prefix for this router file, derived from code-based routing
1616
- * (e.g., from `createApp({ router })` or `.route()` calls).
1617
- * When provided, overrides the filesystem-derived mount path.
1618
- */
1619
- mountPrefix?: string;
1620
- }
1621
-
1622
- export async function parseRoute(
1623
- rootDir: string,
1624
- filename: string,
1625
- projectId: string,
1626
- deploymentId: string,
1627
- visitedFilesOrOptions?: Set<string> | ParseRouteOptions,
1628
- mountedSubrouters?: Set<string>
1629
- ): Promise<BuildMetadata['routes']> {
1630
- // Support both old positional args and new options object
1631
- let options: ParseRouteOptions;
1632
- if (visitedFilesOrOptions instanceof Set) {
1633
- options = { visitedFiles: visitedFilesOrOptions, mountedSubrouters };
1634
- } else if (visitedFilesOrOptions && typeof visitedFilesOrOptions === 'object') {
1635
- options = visitedFilesOrOptions;
1636
- } else {
1637
- options = { mountedSubrouters };
1638
- }
1639
- // Track visited files to prevent infinite recursion
1640
- const visited = options.visitedFiles ?? new Set<string>();
1641
- const resolvedFilename = resolve(filename);
1642
- if (visited.has(resolvedFilename)) {
1643
- return []; // Already parsed this file, avoid infinite loop
1644
- }
1645
- visited.add(resolvedFilename);
1646
- const rawContents = await Bun.file(filename).text();
1647
- const version = hash(rawContents);
1648
- // Transpile TypeScript to JavaScript so acorn-loose can parse it properly
1649
- const transpiler = new Bun.Transpiler({ loader: 'ts', target: 'bun' });
1650
- const contents = transpiler.transformSync(rawContents);
1651
- const ast = acornLoose.parse(contents, {
1652
- locations: true,
1653
- ecmaVersion: 'latest',
1654
- sourceType: 'module',
1655
- });
1656
- let exportName: string | undefined;
1657
- let variableName: string | undefined;
1658
-
1659
- // Import info structure for tracking where identifiers come from
1660
- interface ImportInfo {
1661
- modulePath: string;
1662
- importedName: string; // The exported name from the source module
1663
- importKind: 'named' | 'default';
1664
- }
1665
-
1666
- // Extract import statements to map variable names to their import sources
1667
- const importMap = new Map<string, string>(); // Maps variable name to import path (for backwards compat)
1668
- const importInfoMap = new Map<string, ImportInfo>(); // Maps variable name to full import info
1669
- for (const body of ast.body) {
1670
- if (body.type === 'ImportDeclaration') {
1671
- const importDecl = body as {
1672
- source?: { value?: string };
1673
- specifiers?: Array<{
1674
- type: string;
1675
- local?: { name?: string };
1676
- imported?: { name?: string }; // For named imports: the exported name
1677
- }>;
1678
- };
1679
- const importPath = importDecl.source?.value;
1680
- if (importPath && importDecl.specifiers) {
1681
- for (const spec of importDecl.specifiers) {
1682
- if (spec.type === 'ImportDefaultSpecifier' && spec.local?.name) {
1683
- // import hello from '@agent/hello'
1684
- importMap.set(spec.local.name, importPath);
1685
- importInfoMap.set(spec.local.name, {
1686
- modulePath: importPath,
1687
- importedName: 'default',
1688
- importKind: 'default',
1689
- });
1690
- } else if (spec.type === 'ImportSpecifier' && spec.local?.name) {
1691
- // import { hello } from './shared' or import { hello as h } from './shared'
1692
- const importedName = spec.imported?.name ?? spec.local.name;
1693
- importMap.set(spec.local.name, importPath);
1694
- importInfoMap.set(spec.local.name, {
1695
- modulePath: importPath,
1696
- importedName,
1697
- importKind: 'named',
1698
- });
1699
- }
1700
- }
1701
- }
1702
- }
1703
- }
1704
-
1705
- // Build set of imported names for schema export validation
1706
- const importedNames = new Set(importMap.keys());
1707
-
1708
- // Build set of exported identifiers for schema export validation
1709
- const exportedNames = buildExportedIdentifierSet(ast as ASTProgram);
1710
-
1711
- // Scan for exported schemas (for WebSocket/SSE routes)
1712
- let exportedInputSchemaName: string | undefined;
1713
- let exportedOutputSchemaName: string | undefined;
1714
- for (const body of ast.body) {
1715
- if (body.type === 'ExportNamedDeclaration') {
1716
- const exportDecl = body as {
1717
- declaration?: {
1718
- type: string;
1719
- declarations?: Array<ASTVariableDeclarator>;
1720
- };
1721
- };
1722
-
1723
- if (exportDecl.declaration?.type === 'VariableDeclaration') {
1724
- const varDecl = exportDecl.declaration as {
1725
- declarations: Array<ASTVariableDeclarator>;
1726
- };
1727
- for (const d of varDecl.declarations) {
1728
- if (d.id.type === 'Identifier') {
1729
- const name = (d.id as ASTNodeIdentifier).name;
1730
- if (name === 'inputSchema') {
1731
- exportedInputSchemaName = name;
1732
- } else if (name === 'outputSchema') {
1733
- exportedOutputSchemaName = name;
1734
- }
1735
- }
1736
- }
1737
- }
1738
- }
1739
- }
1740
-
1741
- for (const body of ast.body) {
1742
- if (body.type === 'ExportDefaultDeclaration') {
1743
- const identifier = body.declaration as ASTNodeIdentifier;
1744
- exportName = identifier.name;
1745
- break;
1746
- }
1747
- }
1748
- if (!exportName) {
1749
- throw new InvalidExportError({
1750
- filename,
1751
- message: `could not find default export for ${filename} using ${rootDir}`,
1752
- });
1753
- }
1754
- // Track the chained init expression (e.g., createRouter().get(...).post(...))
1755
- // so we can extract routes from it later
1756
- let chainedInitExpr: ASTNode | undefined;
1757
-
1758
- for (const body of ast.body) {
1759
- if (body.type === 'VariableDeclaration') {
1760
- for (const vardecl of body.declarations) {
1761
- if (vardecl.type === 'VariableDeclarator' && vardecl.id.type === 'Identifier') {
1762
- const identifier = vardecl.id as ASTNodeIdentifier;
1763
- if (identifier.name === exportName) {
1764
- if (vardecl.init?.type === 'CallExpression') {
1765
- const call = vardecl.init as ASTCallExpression;
1766
- // Support both createRouter() and new Hono()
1767
- if (call.callee.name === 'createRouter') {
1768
- variableName = identifier.name;
1769
- break;
1770
- }
1771
- // Support chained calls: createRouter().get(...).post(...)
1772
- // The init is a CallExpression whose callee is a MemberExpression chain
1773
- // that eventually roots at createRouter() or new Hono()
1774
- if (isChainedRouterInit(call)) {
1775
- variableName = identifier.name;
1776
- chainedInitExpr = call;
1777
- break;
1778
- }
1779
- } else if (vardecl.init?.type === 'NewExpression') {
1780
- const newExpr = vardecl.init as ASTCallExpression;
1781
- // Support new Hono() pattern
1782
- if (newExpr.callee.name === 'Hono') {
1783
- variableName = identifier.name;
1784
- break;
1785
- }
1786
- }
1787
- }
1788
- }
1789
- }
1790
- }
1791
- }
1792
- if (!variableName) {
1793
- throw new InvalidCreateRouterError({
1794
- filename,
1795
- message: `error parsing: ${filename}. could not find an proper createRouter or new Hono() defined in this file`,
1796
- });
1797
- }
1798
-
1799
- const rel = toForwardSlash(relative(rootDir, filename));
1800
-
1801
- // Determine the base mount path for routes in this file.
1802
- // When mountPrefix is provided (explicit routing via createApp({ router })),
1803
- // use it directly — the mount path comes from the code, not the filesystem.
1804
- // Otherwise, derive it from the file's position in src/api/ (file-based routing).
1805
- let basePath: string;
1806
- if (options.mountPrefix !== undefined) {
1807
- basePath = options.mountPrefix;
1808
- } else {
1809
- const srcDir = join(rootDir, 'src');
1810
- const relativeApiPath = extractRelativeApiPath(filename, srcDir);
1811
- basePath = computeApiMountPath(relativeApiPath);
1812
- }
1813
-
1814
- const routes: RouteDefinition = [];
1815
-
1816
- try {
1817
- for (const body of ast.body) {
1818
- if (body.type === 'ExpressionStatement') {
1819
- const statement = body as ASTExpressionStatement;
1820
-
1821
- // Validate that the expression is a call expression (e.g. function call)
1822
- if (statement.expression.type !== 'CallExpression') {
1823
- continue;
1824
- }
1825
-
1826
- const callee = statement.expression.callee;
1827
-
1828
- // Validate that the callee is a member expression (e.g. object.method())
1829
- // This handles cases like 'console.log()' or 'router.get()'
1830
- // direct function calls like 'myFunc()' have type 'Identifier' and will be skipped
1831
- if (callee.type !== 'MemberExpression') {
1832
- continue;
1833
- }
1834
-
1835
- if (callee.object.type === 'Identifier' && statement.expression.arguments?.length > 0) {
1836
- const identifier = callee.object as ASTNodeIdentifier;
1837
- if (identifier.name === variableName) {
1838
- let method = (callee.property as ASTNodeIdentifier).name;
1839
- let type = 'api';
1840
- const action = statement.expression.arguments[0];
1841
- let suffix = '';
1842
- let config: Record<string, unknown> | undefined;
1843
- // Capture SSE call expression for output schema extraction
1844
- let sseCallExpr: ASTCallExpression | undefined;
1845
- // Supported HTTP methods that can be represented in BuildMetadata
1846
- const SUPPORTED_HTTP_METHODS = ['get', 'post', 'put', 'delete', 'patch'] as const;
1847
- type SupportedHttpMethod = (typeof SUPPORTED_HTTP_METHODS)[number];
1848
-
1849
- const isSupportedHttpMethod = (m: string): m is SupportedHttpMethod =>
1850
- SUPPORTED_HTTP_METHODS.includes(m.toLowerCase() as SupportedHttpMethod);
1851
-
1852
- switch (method) {
1853
- case 'use':
1854
- case 'onError':
1855
- case 'notFound':
1856
- case 'basePath':
1857
- case 'mount': {
1858
- // Skip Hono middleware, lifecycle handlers, and configuration methods - they don't represent API routes
1859
- continue;
1860
- }
1861
- case 'route': {
1862
- // router.route(mountPath, subRouterVariable)
1863
- // Follow the import to discover sub-router routes
1864
- const mountPathArg = statement.expression.arguments[0];
1865
- const subRouterArg = statement.expression.arguments[1];
1866
-
1867
- // First arg must be a string literal (the mount path)
1868
- if (!mountPathArg || (mountPathArg as ASTLiteral).type !== 'Literal') {
1869
- continue;
1870
- }
1871
- // Second arg must be an identifier (the sub-router variable)
1872
- if (
1873
- !subRouterArg ||
1874
- (subRouterArg as ASTNodeIdentifier).type !== 'Identifier'
1875
- ) {
1876
- continue;
1877
- }
1878
-
1879
- const mountPath = String((mountPathArg as ASTLiteral).value);
1880
- const subRouterName = (subRouterArg as ASTNodeIdentifier).name;
1881
-
1882
- // Look up import path
1883
- const subRouterImportPath = importMap.get(subRouterName);
1884
- if (!subRouterImportPath) {
1885
- continue; // Can't resolve, skip
1886
- }
1887
-
1888
- // Resolve to actual file path
1889
- const resolvedFile = resolveImportPath(
1890
- dirname(filename),
1891
- subRouterImportPath
1892
- );
1893
- if (!resolvedFile || visited.has(resolve(resolvedFile))) {
1894
- continue;
1895
- }
1896
-
1897
- try {
1898
- // The combined mount point for this sub-router
1899
- const combinedBase = joinMountAndRoute(basePath, mountPath);
1900
-
1901
- // Parse sub-router's routes with the code-derived mount prefix.
1902
- // This ensures the sub-router's routes are prefixed correctly
1903
- // regardless of where the file lives on disk.
1904
- const subRoutes = await parseRoute(
1905
- rootDir,
1906
- resolvedFile,
1907
- projectId,
1908
- deploymentId,
1909
- {
1910
- visitedFiles: visited,
1911
- mountedSubrouters: options.mountedSubrouters,
1912
- mountPrefix: combinedBase,
1913
- }
1914
- );
1915
-
1916
- // Track this file as a mounted sub-router
1917
- if (options.mountedSubrouters) {
1918
- options.mountedSubrouters.add(resolve(resolvedFile));
1919
- }
1920
-
1921
- for (const subRoute of subRoutes) {
1922
- // Sub-routes already have the correct full path
1923
- // (the recursive call used combinedBase as mountPrefix)
1924
- const fullPath = subRoute.path;
1925
- const id = generateRouteId(
1926
- projectId,
1927
- deploymentId,
1928
- subRoute.type,
1929
- subRoute.method,
1930
- rel,
1931
- fullPath,
1932
- subRoute.version
1933
- );
1934
-
1935
- // Preserve the sub-route's original filename for schema
1936
- // import resolution. The registry generator needs to know
1937
- // which file actually defines/exports the schema variable.
1938
- const config = { ...subRoute.config };
1939
- if (subRoute.filename && subRoute.filename !== rel) {
1940
- config.schemaSourceFile = subRoute.filename;
1941
- }
1942
-
1943
- routes.push({
1944
- ...subRoute,
1945
- id,
1946
- path: fullPath,
1947
- filename: rel,
1948
- config: Object.keys(config).length > 0 ? config : undefined,
1949
- });
1950
- }
1951
- } catch {
1952
- // Sub-router parse failure - skip silently (could be a non-route file)
1953
- }
1954
- continue;
1955
- }
1956
- case 'on': {
1957
- // router.on(method | method[], path, handler)
1958
- // First arg is method(s), second arg is path
1959
- const methodArg = statement.expression.arguments[0];
1960
- const pathArg = statement.expression.arguments[1];
1961
-
1962
- // Extract methods from first argument
1963
- const methods: SupportedHttpMethod[] = [];
1964
-
1965
- if (methodArg && (methodArg as ASTLiteral).type === 'Literal') {
1966
- // Single method: router.on('GET', '/path', handler)
1967
- const raw = String((methodArg as ASTLiteral).value || '').toLowerCase();
1968
- if (isSupportedHttpMethod(raw)) {
1969
- methods.push(raw as SupportedHttpMethod);
1970
- }
1971
- } else if (methodArg && (methodArg as ASTNode).type === 'ArrayExpression') {
1972
- // Array of methods: router.on(['GET', 'POST'], '/path', handler)
1973
- const arr = methodArg as { elements: ASTNode[] };
1974
- for (const el of arr.elements) {
1975
- if (!el || (el as ASTLiteral).type !== 'Literal') continue;
1976
- const raw = String((el as ASTLiteral).value || '').toLowerCase();
1977
- if (isSupportedHttpMethod(raw)) {
1978
- methods.push(raw as SupportedHttpMethod);
1979
- }
1980
- }
1981
- }
1982
-
1983
- // Skip if no supported methods or path is not a literal
1984
- if (
1985
- methods.length === 0 ||
1986
- !pathArg ||
1987
- (pathArg as ASTLiteral).type !== 'Literal'
1988
- ) {
1989
- continue;
1990
- }
1991
-
1992
- const pathSuffix = String((pathArg as ASTLiteral).value);
1993
-
1994
- // Create a route entry for each method
1995
- for (const httpMethod of methods) {
1996
- const thepath = joinMountAndRoute(basePath, pathSuffix);
1997
- const id = generateRouteId(
1998
- projectId,
1999
- deploymentId,
2000
- 'api',
2001
- httpMethod,
2002
- rel,
2003
- thepath,
2004
- version
2005
- );
2006
-
2007
- // Check if this route uses validator middleware
2008
- const validatorInfo = hasValidatorCall(statement.expression.arguments);
2009
- const routeConfig: Record<string, unknown> = {};
2010
- if (validatorInfo.hasValidator) {
2011
- routeConfig.hasValidator = true;
2012
- if (validatorInfo.agentVariable) {
2013
- routeConfig.agentVariable = validatorInfo.agentVariable;
2014
- const agentImportPath = importMap.get(validatorInfo.agentVariable);
2015
- if (agentImportPath) {
2016
- routeConfig.agentImportPath = agentImportPath;
2017
- }
2018
- }
2019
- // Validate that schema variables are exported (if defined locally)
2020
- validateSchemaExports(
2021
- validatorInfo.inputSchemaVariable,
2022
- 'input',
2023
- importedNames,
2024
- exportedNames,
2025
- rel,
2026
- httpMethod,
2027
- thepath
2028
- );
2029
- validateSchemaExports(
2030
- validatorInfo.outputSchemaVariable,
2031
- 'output',
2032
- importedNames,
2033
- exportedNames,
2034
- rel,
2035
- httpMethod,
2036
- thepath
2037
- );
2038
- if (validatorInfo.inputSchemaVariable) {
2039
- routeConfig.inputSchemaVariable =
2040
- validatorInfo.inputSchemaVariable;
2041
- // Track where the schema is imported from (if imported)
2042
- const inputImportInfo = importInfoMap.get(
2043
- validatorInfo.inputSchemaVariable
2044
- );
2045
- if (inputImportInfo) {
2046
- routeConfig.inputSchemaImportPath = inputImportInfo.modulePath;
2047
- routeConfig.inputSchemaImportedName =
2048
- inputImportInfo.importedName;
2049
- }
2050
- }
2051
- if (validatorInfo.outputSchemaVariable) {
2052
- routeConfig.outputSchemaVariable =
2053
- validatorInfo.outputSchemaVariable;
2054
- // Track where the schema is imported from (if imported)
2055
- const outputImportInfo = importInfoMap.get(
2056
- validatorInfo.outputSchemaVariable
2057
- );
2058
- if (outputImportInfo) {
2059
- routeConfig.outputSchemaImportPath =
2060
- outputImportInfo.modulePath;
2061
- routeConfig.outputSchemaImportedName =
2062
- outputImportInfo.importedName;
2063
- }
2064
- }
2065
- if (validatorInfo.stream !== undefined) {
2066
- routeConfig.stream = validatorInfo.stream;
2067
- }
2068
- }
2069
-
2070
- routes.push({
2071
- id,
2072
- method: httpMethod,
2073
- type: 'api',
2074
- filename: rel,
2075
- path: thepath,
2076
- version,
2077
- config: Object.keys(routeConfig).length > 0 ? routeConfig : undefined,
2078
- });
2079
- }
2080
- continue;
2081
- }
2082
- case 'all': {
2083
- // router.all(path, handler) - matches all HTTP methods
2084
- // First arg is path (same as get/post/etc.)
2085
- if (!action || (action as ASTLiteral).type !== 'Literal') {
2086
- continue;
2087
- }
2088
-
2089
- const pathSuffix = String((action as ASTLiteral).value);
2090
-
2091
- // Create a route entry for each supported method
2092
- for (const httpMethod of SUPPORTED_HTTP_METHODS) {
2093
- const thepath = joinMountAndRoute(basePath, pathSuffix);
2094
- const id = generateRouteId(
2095
- projectId,
2096
- deploymentId,
2097
- 'api',
2098
- httpMethod,
2099
- rel,
2100
- thepath,
2101
- version
2102
- );
2103
-
2104
- // Check if this route uses validator middleware
2105
- const validatorInfo = hasValidatorCall(statement.expression.arguments);
2106
- const routeConfig: Record<string, unknown> = {};
2107
- if (validatorInfo.hasValidator) {
2108
- routeConfig.hasValidator = true;
2109
- if (validatorInfo.agentVariable) {
2110
- routeConfig.agentVariable = validatorInfo.agentVariable;
2111
- const agentImportPath = importMap.get(validatorInfo.agentVariable);
2112
- if (agentImportPath) {
2113
- routeConfig.agentImportPath = agentImportPath;
2114
- }
2115
- }
2116
- // Validate that schema variables are exported (if defined locally)
2117
- validateSchemaExports(
2118
- validatorInfo.inputSchemaVariable,
2119
- 'input',
2120
- importedNames,
2121
- exportedNames,
2122
- rel,
2123
- httpMethod,
2124
- thepath
2125
- );
2126
- validateSchemaExports(
2127
- validatorInfo.outputSchemaVariable,
2128
- 'output',
2129
- importedNames,
2130
- exportedNames,
2131
- rel,
2132
- httpMethod,
2133
- thepath
2134
- );
2135
- if (validatorInfo.inputSchemaVariable) {
2136
- routeConfig.inputSchemaVariable =
2137
- validatorInfo.inputSchemaVariable;
2138
- // Track where the schema is imported from (if imported)
2139
- const inputImportInfo = importInfoMap.get(
2140
- validatorInfo.inputSchemaVariable
2141
- );
2142
- if (inputImportInfo) {
2143
- routeConfig.inputSchemaImportPath = inputImportInfo.modulePath;
2144
- routeConfig.inputSchemaImportedName =
2145
- inputImportInfo.importedName;
2146
- }
2147
- }
2148
- if (validatorInfo.outputSchemaVariable) {
2149
- routeConfig.outputSchemaVariable =
2150
- validatorInfo.outputSchemaVariable;
2151
- // Track where the schema is imported from (if imported)
2152
- const outputImportInfo = importInfoMap.get(
2153
- validatorInfo.outputSchemaVariable
2154
- );
2155
- if (outputImportInfo) {
2156
- routeConfig.outputSchemaImportPath =
2157
- outputImportInfo.modulePath;
2158
- routeConfig.outputSchemaImportedName =
2159
- outputImportInfo.importedName;
2160
- }
2161
- }
2162
- if (validatorInfo.stream !== undefined) {
2163
- routeConfig.stream = validatorInfo.stream;
2164
- }
2165
- }
2166
-
2167
- routes.push({
2168
- id,
2169
- method: httpMethod,
2170
- type: 'api',
2171
- filename: rel,
2172
- path: thepath,
2173
- version,
2174
- config: Object.keys(routeConfig).length > 0 ? routeConfig : undefined,
2175
- });
2176
- }
2177
- continue;
2178
- }
2179
- case 'get':
2180
- case 'put':
2181
- case 'post':
2182
- case 'patch':
2183
- case 'delete': {
2184
- if (action && (action as ASTLiteral).type === 'Literal') {
2185
- suffix = String((action as ASTLiteral).value);
2186
-
2187
- // Check if any argument is a middleware function call (websocket, sse, stream, cron)
2188
- // New pattern: router.get('/ws', websocket((c, ws) => { ... }))
2189
- for (const arg of statement.expression.arguments) {
2190
- if ((arg as ASTCallExpression).type === 'CallExpression') {
2191
- const callExpr = arg as ASTCallExpression;
2192
- // Only handle simple Identifier callees (e.g., websocket(), sse())
2193
- // Skip MemberExpression callees (e.g., obj.method())
2194
- if (callExpr.callee.type !== 'Identifier') {
2195
- continue;
2196
- }
2197
- const calleeName = (callExpr.callee as ASTNodeIdentifier).name;
2198
- if (
2199
- calleeName === 'websocket' ||
2200
- calleeName === 'sse' ||
2201
- calleeName === 'stream'
2202
- ) {
2203
- type = calleeName;
2204
- // Capture SSE call expression for output schema extraction
2205
- if (calleeName === 'sse') {
2206
- sseCallExpr = callExpr;
2207
- }
2208
- break;
2209
- }
2210
- if (calleeName === 'cron') {
2211
- type = 'cron';
2212
- // First argument to cron() is the schedule expression
2213
- if (callExpr.arguments && callExpr.arguments.length > 0) {
2214
- const cronArg = callExpr.arguments[0] as ASTLiteral;
2215
- if (cronArg.type === 'Literal') {
2216
- const expression = String(cronArg.value);
2217
- try {
2218
- parseCronExpression(expression, {
2219
- hasSeconds: false,
2220
- });
2221
- } catch (ex) {
2222
- throw new InvalidRouterConfigError({
2223
- filename,
2224
- cause: ex,
2225
- line: body.loc?.start?.line,
2226
- message: `invalid cron expression "${expression}" in ${filename} at line ${body.loc?.start?.line}`,
2227
- });
2228
- }
2229
- config = { expression };
2230
- }
2231
- }
2232
- break;
2233
- }
2234
- }
2235
- }
2236
- } else {
2237
- throw new InvalidRouterConfigError({
2238
- filename,
2239
- line: body.loc?.start?.line,
2240
- message: `unsupported HTTP method ${method} in ${filename} at line ${body.loc?.start?.line}`,
2241
- });
2242
- }
2243
- break;
2244
- }
2245
- case 'stream':
2246
- case 'sse':
2247
- case 'websocket': {
2248
- // DEPRECATED: router.stream(), router.sse(), router.websocket()
2249
- // These methods now throw errors at runtime
2250
- type = method;
2251
- method = 'post';
2252
- const theaction = action as ASTLiteral;
2253
- if (theaction.type === 'Literal') {
2254
- suffix = String(theaction.value);
2255
- break;
2256
- }
2257
- break;
2258
- }
2259
- case 'cron': {
2260
- // DEPRECATED: router.cron()
2261
- // This method now throws errors at runtime
2262
- type = method;
2263
- method = 'post';
2264
- const theaction = action as ASTLiteral;
2265
- if (theaction.type === 'Literal') {
2266
- const expression = String(theaction.value);
2267
- try {
2268
- parseCronExpression(expression, { hasSeconds: false });
2269
- } catch (ex) {
2270
- throw new InvalidRouterConfigError({
2271
- filename,
2272
- cause: ex,
2273
- line: body.loc?.start?.line,
2274
- message: `invalid cron expression "${expression}" in ${filename} at line ${body.loc?.start?.line}`,
2275
- });
2276
- }
2277
- suffix = hash(expression);
2278
- config = { expression };
2279
- break;
2280
- }
2281
- break;
2282
- }
2283
- default: {
2284
- throw new InvalidRouterConfigError({
2285
- filename,
2286
- line: body.loc?.start?.line,
2287
- message: `unsupported router method ${method} in ${filename} at line ${body.loc?.start?.line}`,
2288
- });
2289
- }
2290
- }
2291
- const thepath = joinMountAndRoute(basePath, suffix);
2292
- const id = generateRouteId(
2293
- projectId,
2294
- deploymentId,
2295
- type,
2296
- method,
2297
- rel,
2298
- thepath,
2299
- version
2300
- );
2301
-
2302
- // Check if this route uses validator middleware
2303
- const validatorInfo = hasValidatorCall(statement.expression.arguments);
2304
-
2305
- // Store validator info in config if present
2306
- const routeConfig = config ? { ...config } : {};
2307
- if (validatorInfo.hasValidator) {
2308
- routeConfig.hasValidator = true;
2309
- if (validatorInfo.agentVariable) {
2310
- routeConfig.agentVariable = validatorInfo.agentVariable;
2311
- // Look up where this agent variable is imported from
2312
- const agentImportPath = importMap.get(validatorInfo.agentVariable);
2313
- if (agentImportPath) {
2314
- routeConfig.agentImportPath = agentImportPath;
2315
- }
2316
- }
2317
- // Validate that schema variables are exported (if defined locally)
2318
- validateSchemaExports(
2319
- validatorInfo.inputSchemaVariable,
2320
- 'input',
2321
- importedNames,
2322
- exportedNames,
2323
- rel,
2324
- method,
2325
- thepath
2326
- );
2327
- validateSchemaExports(
2328
- validatorInfo.outputSchemaVariable,
2329
- 'output',
2330
- importedNames,
2331
- exportedNames,
2332
- rel,
2333
- method,
2334
- thepath
2335
- );
2336
- if (validatorInfo.inputSchemaVariable) {
2337
- routeConfig.inputSchemaVariable = validatorInfo.inputSchemaVariable;
2338
- // Track where the schema is imported from (if imported)
2339
- const inputImportInfo = importInfoMap.get(
2340
- validatorInfo.inputSchemaVariable
2341
- );
2342
- if (inputImportInfo) {
2343
- routeConfig.inputSchemaImportPath = inputImportInfo.modulePath;
2344
- routeConfig.inputSchemaImportedName = inputImportInfo.importedName;
2345
- }
2346
- }
2347
- if (validatorInfo.outputSchemaVariable) {
2348
- routeConfig.outputSchemaVariable = validatorInfo.outputSchemaVariable;
2349
- // Track where the schema is imported from (if imported)
2350
- const outputImportInfo = importInfoMap.get(
2351
- validatorInfo.outputSchemaVariable
2352
- );
2353
- if (outputImportInfo) {
2354
- routeConfig.outputSchemaImportPath = outputImportInfo.modulePath;
2355
- routeConfig.outputSchemaImportedName = outputImportInfo.importedName;
2356
- }
2357
- }
2358
- if (validatorInfo.stream !== undefined) {
2359
- routeConfig.stream = validatorInfo.stream;
2360
- }
2361
- }
2362
-
2363
- // Extract output schema from SSE options: sse({ output: schema }, handler)
2364
- // For SSE routes, the sse({ output }) pattern takes precedence over any
2365
- // validator-provided schema. Imported schemas need not be exported, but
2366
- // locally-defined schemas must be exported and are validated below.
2367
- if (sseCallExpr) {
2368
- const sseSchemaInfo = extractSSEOutputSchema(sseCallExpr);
2369
- if (sseSchemaInfo.outputSchemaVariable) {
2370
- // Track where the schema is imported from (if imported)
2371
- const outputImportInfo = importInfoMap.get(
2372
- sseSchemaInfo.outputSchemaVariable
2373
- );
2374
- // Validate that locally-defined schemas are exported
2375
- // (skip validation if schema is imported from another module)
2376
- if (!outputImportInfo) {
2377
- validateSchemaExports(
2378
- sseSchemaInfo.outputSchemaVariable,
2379
- 'output',
2380
- importedNames,
2381
- exportedNames,
2382
- rel,
2383
- method,
2384
- thepath
2385
- );
2386
- }
2387
- // Override any validator-provided schema with SSE-specific schema
2388
- routeConfig.outputSchemaVariable = sseSchemaInfo.outputSchemaVariable;
2389
- if (outputImportInfo) {
2390
- routeConfig.outputSchemaImportPath = outputImportInfo.modulePath;
2391
- routeConfig.outputSchemaImportedName = outputImportInfo.importedName;
2392
- } else {
2393
- // Clear any validator-provided import info since we're using local schema
2394
- delete routeConfig.outputSchemaImportPath;
2395
- delete routeConfig.outputSchemaImportedName;
2396
- }
2397
- }
2398
- }
2399
-
2400
- // Fall back to exported schemas when validator doesn't provide them
2401
- // This works for all route types (API, WebSocket, SSE, stream)
2402
- // For API routes, this enables `export const outputSchema` pattern
2403
- // which is useful when using zValidator (input-only) but needing typed outputs
2404
- if (!routeConfig.inputSchemaVariable && exportedInputSchemaName) {
2405
- routeConfig.inputSchemaVariable = exportedInputSchemaName;
2406
- // Check if exported schema name is also imported
2407
- const inputImportInfo = importInfoMap.get(exportedInputSchemaName);
2408
- if (inputImportInfo) {
2409
- routeConfig.inputSchemaImportPath = inputImportInfo.modulePath;
2410
- routeConfig.inputSchemaImportedName = inputImportInfo.importedName;
2411
- }
2412
- }
2413
- if (!routeConfig.outputSchemaVariable && exportedOutputSchemaName) {
2414
- routeConfig.outputSchemaVariable = exportedOutputSchemaName;
2415
- // Check if exported schema name is also imported
2416
- const outputImportInfo = importInfoMap.get(exportedOutputSchemaName);
2417
- if (outputImportInfo) {
2418
- routeConfig.outputSchemaImportPath = outputImportInfo.modulePath;
2419
- routeConfig.outputSchemaImportedName = outputImportInfo.importedName;
2420
- }
2421
- }
2422
-
2423
- routes.push({
2424
- id,
2425
- method: method as 'get' | 'post' | 'put' | 'delete' | 'patch',
2426
- type: type as 'api' | 'sms' | 'email' | 'cron',
2427
- filename: rel,
2428
- path: thepath,
2429
- version,
2430
- config: Object.keys(routeConfig).length > 0 ? routeConfig : undefined,
2431
- });
2432
- }
2433
- }
2434
- }
2435
- }
2436
- // Process routes from chained initialization expressions
2437
- // e.g., const router = createRouter().get('/foo', handler).post('/bar', handler)
2438
- if (chainedInitExpr) {
2439
- const chainedCalls = flattenChainedCalls(chainedInitExpr);
2440
- for (const chainedCall of chainedCalls) {
2441
- const { method: chainMethod, arguments: chainArgs } = chainedCall;
2442
-
2443
- // Skip non-route methods
2444
- if (
2445
- chainMethod === 'use' ||
2446
- chainMethod === 'onError' ||
2447
- chainMethod === 'notFound' ||
2448
- chainMethod === 'basePath' ||
2449
- chainMethod === 'mount'
2450
- ) {
2451
- continue;
2452
- }
2453
-
2454
- // Handle .route() for sub-router mounting (same as the ExpressionStatement case)
2455
- if (chainMethod === 'route') {
2456
- const mountPathArg = chainArgs[0] as ASTNode | undefined;
2457
- const subRouterArg = chainArgs[1] as ASTNode | undefined;
2458
-
2459
- if (
2460
- mountPathArg &&
2461
- (mountPathArg as ASTLiteral).type === 'Literal' &&
2462
- subRouterArg &&
2463
- (subRouterArg as ASTNodeIdentifier).type === 'Identifier'
2464
- ) {
2465
- const mountPath = String((mountPathArg as ASTLiteral).value);
2466
- const subRouterName = (subRouterArg as ASTNodeIdentifier).name;
2467
- const subRouterImportPath = importMap.get(subRouterName);
2468
-
2469
- if (subRouterImportPath) {
2470
- const resolvedFile = resolveImportPath(dirname(filename), subRouterImportPath);
2471
- if (resolvedFile && !visited.has(resolve(resolvedFile))) {
2472
- try {
2473
- const combinedBase = joinMountAndRoute(basePath, mountPath);
2474
- const subRoutes = await parseRoute(
2475
- rootDir,
2476
- resolvedFile,
2477
- projectId,
2478
- deploymentId,
2479
- {
2480
- visitedFiles: visited,
2481
- mountedSubrouters: options.mountedSubrouters,
2482
- mountPrefix: combinedBase,
2483
- }
2484
- );
2485
-
2486
- if (options.mountedSubrouters) {
2487
- options.mountedSubrouters.add(resolve(resolvedFile));
2488
- }
2489
-
2490
- for (const subRoute of subRoutes) {
2491
- const fullPath = subRoute.path;
2492
- const id = generateRouteId(
2493
- projectId,
2494
- deploymentId,
2495
- subRoute.type,
2496
- subRoute.method,
2497
- rel,
2498
- fullPath,
2499
- subRoute.version
2500
- );
2501
-
2502
- const config = { ...subRoute.config };
2503
- if (subRoute.filename && subRoute.filename !== rel) {
2504
- config.schemaSourceFile = subRoute.filename;
2505
- }
2506
-
2507
- routes.push({
2508
- ...subRoute,
2509
- id,
2510
- path: fullPath,
2511
- filename: rel,
2512
- config: Object.keys(config).length > 0 ? config : undefined,
2513
- });
2514
- }
2515
- } catch {
2516
- // Sub-router parse failure — skip
2517
- }
2518
- }
2519
- }
2520
- }
2521
- continue;
2522
- }
2523
-
2524
- // Handle HTTP methods: get, post, put, patch, delete
2525
- const CHAINED_HTTP_METHODS = ['get', 'post', 'put', 'delete', 'patch'] as const;
2526
- if (
2527
- CHAINED_HTTP_METHODS.includes(
2528
- chainMethod.toLowerCase() as (typeof CHAINED_HTTP_METHODS)[number]
2529
- )
2530
- ) {
2531
- const pathArg = chainArgs[0] as ASTNode | undefined;
2532
- if (!pathArg || (pathArg as ASTLiteral).type !== 'Literal') continue;
2533
-
2534
- const suffix = String((pathArg as ASTLiteral).value);
2535
- let type = 'api';
2536
-
2537
- // Check for websocket/sse/stream wrappers in handler args
2538
- for (const arg of chainArgs.slice(1)) {
2539
- if ((arg as ASTCallExpression)?.type === 'CallExpression') {
2540
- const callExpr = arg as ASTCallExpression;
2541
- if (callExpr.callee?.type === 'Identifier') {
2542
- const calleeName = (callExpr.callee as ASTNodeIdentifier).name;
2543
- if (
2544
- calleeName === 'websocket' ||
2545
- calleeName === 'sse' ||
2546
- calleeName === 'stream'
2547
- ) {
2548
- type = calleeName;
2549
- break;
2550
- }
2551
- }
2552
- }
2553
- }
2554
-
2555
- const thepath = joinMountAndRoute(basePath, suffix);
2556
- const id = generateRouteId(
2557
- projectId,
2558
- deploymentId,
2559
- type,
2560
- chainMethod,
2561
- rel,
2562
- thepath,
2563
- version
2564
- );
2565
-
2566
- // Check for validators in chained args
2567
- const validatorInfo = hasValidatorCall(chainArgs as unknown[]);
2568
- const routeConfig: Record<string, unknown> = {};
2569
- if (validatorInfo.hasValidator) {
2570
- routeConfig.hasValidator = true;
2571
- if (validatorInfo.agentVariable) {
2572
- routeConfig.agentVariable = validatorInfo.agentVariable;
2573
- const agentImportPath = importMap.get(validatorInfo.agentVariable);
2574
- if (agentImportPath) routeConfig.agentImportPath = agentImportPath;
2575
- }
2576
- if (validatorInfo.inputSchemaVariable) {
2577
- routeConfig.inputSchemaVariable = validatorInfo.inputSchemaVariable;
2578
- const inputImportInfo = importInfoMap.get(validatorInfo.inputSchemaVariable);
2579
- if (inputImportInfo) {
2580
- routeConfig.inputSchemaImportPath = inputImportInfo.modulePath;
2581
- routeConfig.inputSchemaImportedName = inputImportInfo.importedName;
2582
- }
2583
- }
2584
- if (validatorInfo.outputSchemaVariable) {
2585
- routeConfig.outputSchemaVariable = validatorInfo.outputSchemaVariable;
2586
- const outputImportInfo = importInfoMap.get(validatorInfo.outputSchemaVariable);
2587
- if (outputImportInfo) {
2588
- routeConfig.outputSchemaImportPath = outputImportInfo.modulePath;
2589
- routeConfig.outputSchemaImportedName = outputImportInfo.importedName;
2590
- }
2591
- }
2592
- if (validatorInfo.stream !== undefined) routeConfig.stream = validatorInfo.stream;
2593
- }
2594
-
2595
- // Fall back to exported schemas
2596
- if (!routeConfig.inputSchemaVariable && exportedInputSchemaName) {
2597
- routeConfig.inputSchemaVariable = exportedInputSchemaName;
2598
- }
2599
- if (!routeConfig.outputSchemaVariable && exportedOutputSchemaName) {
2600
- routeConfig.outputSchemaVariable = exportedOutputSchemaName;
2601
- }
2602
-
2603
- routes.push({
2604
- id,
2605
- method: chainMethod as 'get' | 'post' | 'put' | 'delete' | 'patch',
2606
- type: type as 'api' | 'sms' | 'email' | 'cron',
2607
- filename: rel,
2608
- path: thepath,
2609
- version,
2610
- config: Object.keys(routeConfig).length > 0 ? routeConfig : undefined,
2611
- });
2612
- }
2613
- }
2614
- }
2615
- } catch (error) {
2616
- if (error instanceof InvalidRouterConfigError || error instanceof SchemaNotExportedError) {
2617
- throw error;
2618
- }
2619
- throw new InvalidRouterConfigError({
2620
- filename,
2621
- cause: error,
2622
- });
2623
- }
2624
- return routes;
2625
- }
2626
-
2627
- /**
2628
- * Result of workbench analysis
2629
- */
2630
- export interface WorkbenchAnalysis {
2631
- hasWorkbench: boolean;
2632
- config: WorkbenchConfig | null;
2633
- }
2634
-
2635
- /**
2636
- * Check if a TypeScript file actively uses a specific function
2637
- * (ignores comments and unused imports)
2638
- *
2639
- * @param content - The TypeScript source code
2640
- * @param functionName - The function name to check for (e.g., 'createWorkbench')
2641
- * @returns true if the function is both imported and called
2642
- */
2643
- export function checkFunctionUsage(content: string, functionName: string): boolean {
2644
- try {
2645
- const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
2646
-
2647
- let hasImport = false;
2648
- let hasUsage = false;
2649
-
2650
- function visitNode(node: ts.Node): void {
2651
- // Check for import declarations with the function
2652
- if (ts.isImportDeclaration(node) && node.importClause?.namedBindings) {
2653
- if (ts.isNamedImports(node.importClause.namedBindings)) {
2654
- for (const element of node.importClause.namedBindings.elements) {
2655
- if (element.name.text === functionName) {
2656
- hasImport = true;
2657
- }
2658
- }
2659
- }
2660
- }
2661
- // Check for function calls
2662
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
2663
- if (node.expression.text === functionName) {
2664
- hasUsage = true;
2665
- }
2666
- }
2667
- // Recursively visit child nodes
2668
- ts.forEachChild(node, visitNode);
2669
- }
2670
-
2671
- visitNode(sourceFile);
2672
- // Only return true if both import and usage are present
2673
- return hasImport && hasUsage;
2674
- } catch (error) {
2675
- // Fallback to string check if AST parsing fails
2676
- logger.warn(`AST parsing failed for ${functionName}, falling back to string check:`, error);
2677
- return content.includes(functionName);
2678
- }
2679
- }
2680
-
2681
- /**
2682
- * Check if app.ts contains conflicting routes for a given endpoint
2683
- */
2684
- export function checkRouteConflicts(content: string, workbenchEndpoint: string): boolean {
2685
- try {
2686
- const sourceFile = ts.createSourceFile('app.ts', content, ts.ScriptTarget.Latest, true);
2687
-
2688
- let hasConflict = false;
2689
-
2690
- function visitNode(node: ts.Node): void {
2691
- // Check for router.get calls
2692
- if (
2693
- ts.isCallExpression(node) &&
2694
- ts.isPropertyAccessExpression(node.expression) &&
2695
- ts.isIdentifier(node.expression.name) &&
2696
- node.expression.name.text === 'get'
2697
- ) {
2698
- // Check if first argument is the workbench endpoint
2699
- const firstArg = node.arguments[0];
2700
- if (node.arguments.length > 0 && firstArg && ts.isStringLiteral(firstArg)) {
2701
- if (firstArg.text === workbenchEndpoint) {
2702
- hasConflict = true;
2703
- }
2704
- }
2705
- }
2706
-
2707
- ts.forEachChild(node, visitNode);
2708
- }
2709
-
2710
- visitNode(sourceFile);
2711
- return hasConflict;
2712
- } catch (_error) {
2713
- return false;
2714
- }
2715
- }
2716
-
2717
- /**
2718
- * Extract AppState type from setup() return value in createApp call
2719
- *
2720
- * @param content - The TypeScript source code from app.ts
2721
- * @returns Type definition string or null if no setup found
2722
- */
2723
- export function extractAppStateType(content: string): string | null {
2724
- try {
2725
- const sourceFile = ts.createSourceFile(
2726
- 'app.ts',
2727
- content,
2728
- ts.ScriptTarget.Latest,
2729
- true,
2730
- ts.ScriptKind.TS
2731
- );
2732
- let appStateType: string | null = null;
2733
- let foundCreateApp = false;
2734
- let foundSetup = false;
2735
- let exportedSetupFunc: ts.FunctionDeclaration | undefined;
2736
-
2737
- function visitNode(node: ts.Node): void {
2738
- // Look for createApp call expression (can be on await expression)
2739
- let callExpr: ts.CallExpression | undefined;
2740
-
2741
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
2742
- if (node.expression.text === 'createApp') {
2743
- foundCreateApp = true;
2744
- callExpr = node;
2745
- }
2746
- } else if (ts.isAwaitExpression(node) && ts.isCallExpression(node.expression)) {
2747
- const call = node.expression;
2748
- if (ts.isIdentifier(call.expression) && call.expression.text === 'createApp') {
2749
- foundCreateApp = true;
2750
- callExpr = call;
2751
- }
2752
- }
2753
-
2754
- if (callExpr) {
2755
- // Check if it has a config object argument
2756
- const configArg = callExpr.arguments[0];
2757
- if (callExpr.arguments.length > 0 && configArg) {
2758
- if (ts.isObjectLiteralExpression(configArg)) {
2759
- // Find setup property
2760
- for (const prop of configArg.properties) {
2761
- if (
2762
- ts.isPropertyAssignment(prop) &&
2763
- ts.isIdentifier(prop.name) &&
2764
- prop.name.text === 'setup'
2765
- ) {
2766
- foundSetup = true;
2767
- // Found setup function - extract return type
2768
- const setupFunc = prop.initializer;
2769
- if (ts.isFunctionExpression(setupFunc) || ts.isArrowFunction(setupFunc)) {
2770
- // Find return statement
2771
- const returnObj = findReturnObject(setupFunc);
2772
- if (returnObj) {
2773
- appStateType = objectLiteralToTypeDefinition(returnObj, sourceFile);
2774
- } else {
2775
- logger.debug('No return object found in setup function');
2776
- }
2777
- } else {
2778
- logger.debug(
2779
- `Setup is not a function expression or arrow function, it's: ${ts.SyntaxKind[setupFunc.kind]}`
2780
- );
2781
- }
2782
- }
2783
- }
2784
- }
2785
- }
2786
- }
2787
-
2788
- // Also record exported setup function
2789
- if (
2790
- ts.isFunctionDeclaration(node) &&
2791
- node.name &&
2792
- node.name.text === 'setup' &&
2793
- node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
2794
- ) {
2795
- exportedSetupFunc = node;
2796
- }
2797
-
2798
- ts.forEachChild(node, visitNode);
2799
- }
2800
-
2801
- function findReturnObject(
2802
- func: ts.FunctionExpression | ts.ArrowFunction | ts.FunctionDeclaration
2803
- ): ts.ObjectLiteralExpression | null {
2804
- let returnObject: ts.ObjectLiteralExpression | null = null;
2805
-
2806
- // Handle arrow function with expression body: () => ({ ... })
2807
- if (ts.isArrowFunction(func) && !ts.isBlock(func.body)) {
2808
- const bodyExpr = func.body;
2809
- // Handle parenthesized expression: () => ({ ... })
2810
- const expr = ts.isParenthesizedExpression(bodyExpr) ? bodyExpr.expression : bodyExpr;
2811
-
2812
- if (ts.isObjectLiteralExpression(expr)) {
2813
- return expr;
2814
- }
2815
- if (ts.isIdentifier(expr)) {
2816
- // Support: const state = {...}; const setup = () => state;
2817
- // Walk up to find the enclosing source file or statement list
2818
- let scope: ts.Node = func;
2819
- while (scope && !ts.isSourceFile(scope) && !ts.isBlock(scope)) {
2820
- scope = scope.parent;
2821
- }
2822
- if (scope) {
2823
- findVariableDeclaration(scope, expr.text);
2824
- }
2825
- return returnObject;
2826
- }
2827
- // For other expressions, can't extract type
2828
- return null;
2829
- }
2830
-
2831
- function visitFuncNode(node: ts.Node): void {
2832
- if (ts.isReturnStatement(node) && node.expression) {
2833
- // Handle direct object literal
2834
- if (ts.isObjectLiteralExpression(node.expression)) {
2835
- returnObject = node.expression;
2836
- }
2837
- // Handle variable reference (const state = {...}; return state;)
2838
- else if (ts.isIdentifier(node.expression)) {
2839
- // Try to find the variable declaration
2840
- const varName = node.expression.text;
2841
- // Walk back through the function to find the declaration
2842
- if (func.body && ts.isBlock(func.body)) {
2843
- findVariableDeclaration(func.body, varName);
2844
- }
2845
- }
2846
- }
2847
- ts.forEachChild(node, visitFuncNode);
2848
- }
2849
-
2850
- function findVariableDeclaration(body: ts.Node, varName: string): void {
2851
- function visitForVar(node: ts.Node): void {
2852
- if (ts.isVariableStatement(node)) {
2853
- for (const decl of node.declarationList.declarations) {
2854
- if (ts.isIdentifier(decl.name) && decl.name.text === varName) {
2855
- if (decl.initializer && ts.isObjectLiteralExpression(decl.initializer)) {
2856
- returnObject = decl.initializer;
2857
- }
2858
- }
2859
- }
2860
- }
2861
- ts.forEachChild(node, visitForVar);
2862
- }
2863
- visitForVar(body);
2864
- }
2865
-
2866
- if (func.body && ts.isBlock(func.body)) {
2867
- visitFuncNode(func.body);
2868
- }
2869
-
2870
- return returnObject;
2871
- }
2872
-
2873
- function objectLiteralToTypeDefinition(
2874
- obj: ts.ObjectLiteralExpression,
2875
- sourceFile: ts.SourceFile
2876
- ): string {
2877
- const properties: string[] = [];
2878
-
2879
- for (const prop of obj.properties) {
2880
- if (ts.isPropertyAssignment(prop)) {
2881
- const name = prop.name.getText(sourceFile);
2882
- const value = prop.initializer;
2883
- const typeStr = inferTypeFromValue(value, sourceFile);
2884
- properties.push(`\t${name}: ${typeStr};`);
2885
- } else if (ts.isShorthandPropertyAssignment(prop)) {
2886
- const name = prop.name.getText(sourceFile);
2887
- properties.push(`\t${name}: unknown;`);
2888
- }
2889
- }
2890
-
2891
- return `{\n${properties.join('\n')}\n}`;
2892
- }
2893
-
2894
- function inferTypeFromValue(value: ts.Expression, sourceFile: ts.SourceFile): string {
2895
- if (ts.isStringLiteral(value)) {
2896
- return 'string';
2897
- }
2898
- if (ts.isNumericLiteral(value)) {
2899
- return 'number';
2900
- }
2901
- if (
2902
- value.kind === ts.SyntaxKind.TrueKeyword ||
2903
- value.kind === ts.SyntaxKind.FalseKeyword
2904
- ) {
2905
- return 'boolean';
2906
- }
2907
- if (ts.isNewExpression(value) && ts.isIdentifier(value.expression)) {
2908
- if (value.expression.text === 'Date') {
2909
- return 'Date';
2910
- }
2911
- }
2912
- if (ts.isObjectLiteralExpression(value)) {
2913
- return objectLiteralToTypeDefinition(value, sourceFile);
2914
- }
2915
- if (ts.isArrayLiteralExpression(value)) {
2916
- return 'unknown[]';
2917
- }
2918
- return 'unknown';
2919
- }
2920
-
2921
- visitNode(sourceFile);
2922
-
2923
- // If no inline setup found but we have an exported setup function, use that
2924
- if (foundCreateApp && !foundSetup && exportedSetupFunc) {
2925
- foundSetup = true;
2926
- const returnObj = findReturnObject(exportedSetupFunc);
2927
- if (returnObj) {
2928
- appStateType = objectLiteralToTypeDefinition(returnObj, sourceFile);
2929
- } else {
2930
- logger.debug('Exported setup function found but no return object');
2931
- }
2932
- }
2933
-
2934
- if (!foundCreateApp) {
2935
- logger.debug('Did not find createApp call in app.ts');
2936
- } else if (!foundSetup) {
2937
- logger.debug('Found createApp but no setup property');
2938
- } else if (!appStateType) {
2939
- logger.debug('Found createApp and setup but could not extract type');
2940
- }
2941
-
2942
- return appStateType;
2943
- } catch (error) {
2944
- logger.warn('AppState type extraction failed:', error);
2945
- return null;
2946
- }
2947
- }
2948
-
2949
- /**
2950
- * Update tsconfig.json to add path mapping for @agentuity/runtime
2951
- *
2952
- * @param rootDir - Root directory of the project
2953
- * @param shouldAdd - If true, add the mapping; if false, remove it
2954
- */
2955
- async function updateTsconfigPathMapping(rootDir: string, shouldAdd: boolean): Promise<void> {
2956
- const tsconfigPath = join(rootDir, 'tsconfig.json');
2957
-
2958
- if (!(await Bun.file(tsconfigPath).exists())) {
2959
- logger.debug('No tsconfig.json found, skipping path mapping update');
2960
- return;
2961
- }
2962
-
2963
- try {
2964
- const tsconfigContent = await Bun.file(tsconfigPath).text();
2965
-
2966
- // Use JSONC parser to handle comments in tsconfig.json
2967
- const tsconfig = parseJSONC(tsconfigContent) as {
2968
- compilerOptions?: { paths?: Record<string, string[]> };
2969
- [key: string]: unknown;
2970
- };
2971
- const _before = JSON.stringify(tsconfig);
2972
-
2973
- // Initialize compilerOptions and paths if they don't exist
2974
- if (!tsconfig.compilerOptions) {
2975
- tsconfig.compilerOptions = {};
2976
- }
2977
- if (!tsconfig.compilerOptions.paths) {
2978
- tsconfig.compilerOptions.paths = {};
2979
- }
2980
-
2981
- if (shouldAdd) {
2982
- // Add or update the path mapping
2983
- tsconfig.compilerOptions.paths['@agentuity/runtime'] = ['./src/generated/router.ts'];
2984
-
2985
- logger.debug('Added @agentuity/runtime path mapping to tsconfig.json');
2986
- } else {
2987
- // Remove the path mapping if it exists
2988
- if (tsconfig.compilerOptions.paths['@agentuity/runtime']) {
2989
- delete tsconfig.compilerOptions.paths['@agentuity/runtime'];
2990
- logger.debug('Removed @agentuity/runtime path mapping from tsconfig.json');
2991
- }
2992
-
2993
- // Clean up empty paths object
2994
- if (Object.keys(tsconfig.compilerOptions.paths).length === 0) {
2995
- delete tsconfig.compilerOptions.paths;
2996
- }
2997
- }
2998
-
2999
- const _after = JSON.stringify(tsconfig);
3000
- if (_before === _after) {
3001
- return;
3002
- }
3003
-
3004
- // Write back using standard JSON (TypeScript requires strict JSON format)
3005
- await Bun.write(tsconfigPath, JSON.stringify(tsconfig, null, '\t') + '\n');
3006
- } catch (error) {
3007
- logger.warn('Failed to update tsconfig.json:', error);
3008
- }
3009
- }
3010
-
3011
- const RuntimePackageNotFound = StructuredError('RuntimePackageNotFound');
3012
-
3013
- /**
3014
- * Generate lifecycle type files (src/generated/state.ts and src/generated/router.ts)
3015
- *
3016
- * @param rootDir - Root directory of the project
3017
- * @param outDir - Output directory (typically src/generated/)
3018
- * @param appFilePath - Path to app.ts file
3019
- * @returns true if files were generated, false if no setup found
3020
- */
3021
- export async function generateLifecycleTypes(
3022
- rootDir: string,
3023
- outDir: string,
3024
- appFilePath: string
3025
- ): Promise<boolean> {
3026
- const appContent = await Bun.file(appFilePath).text();
3027
- if (typeof appContent !== 'string') {
3028
- return false;
3029
- }
3030
-
3031
- const appStateType = extractAppStateType(appContent as string);
3032
-
3033
- if (!appStateType) {
3034
- logger.debug('No setup() function found in app.ts, skipping lifecycle type generation');
3035
- // Remove path mapping if no setup found
3036
- await updateTsconfigPathMapping(rootDir, false);
3037
- return false;
3038
- }
3039
-
3040
- // Ensure output directory exists (now src/generated instead of .agentuity)
3041
- if (!existsSync(outDir)) {
3042
- mkdirSync(outDir, { recursive: true });
3043
- }
3044
-
3045
- // Find @agentuity/runtime by walking up directory tree
3046
- // This works in any project structure - monorepos, nested projects, etc.
3047
- let runtimePkgPath: string | null = null;
3048
- let currentDir = rootDir;
3049
- const searchedPaths: string[] = [];
3050
-
3051
- while (currentDir && currentDir !== '/' && currentDir !== '.') {
3052
- const candidatePath = join(currentDir, 'node_modules', '@agentuity', 'runtime');
3053
- searchedPaths.push(candidatePath);
3054
-
3055
- if (existsSync(candidatePath)) {
3056
- runtimePkgPath = candidatePath;
3057
- logger.debug(`Found runtime package at: ${candidatePath}`);
3058
- break;
3059
- }
3060
-
3061
- // Try packages/ for monorepo source layout
3062
- const packagesPath = join(currentDir, 'packages', 'runtime');
3063
- searchedPaths.push(packagesPath);
3064
-
3065
- if (existsSync(packagesPath)) {
3066
- runtimePkgPath = packagesPath;
3067
- logger.debug(`Found runtime package (source) at: ${packagesPath}`);
3068
- break;
3069
- }
3070
-
3071
- // Move up one directory
3072
- const parent = dirname(currentDir);
3073
- if (parent === currentDir) break; // Reached root
3074
- currentDir = parent;
3075
- }
3076
-
3077
- if (!runtimePkgPath) {
3078
- throw new RuntimePackageNotFound({
3079
- message:
3080
- `@agentuity/runtime package not found.\n` +
3081
- `Searched paths:\n${searchedPaths.map((p) => ` - ${p}`).join('\n')}\n` +
3082
- `Make sure dependencies are installed by running 'bun install' or 'npm install'`,
3083
- });
3084
- }
3085
-
3086
- let runtimeImportPath: string | null = null;
3087
-
3088
- // Calculate relative path from src/generated/ to the package location
3089
- // Don't resolve symlinks - we want to use the symlink path so it works in both
3090
- // local dev (symlinked to packages/) and CI (actual node_modules)
3091
- if (existsSync(runtimePkgPath)) {
3092
- // Calculate relative path from src/generated/ to node_modules package
3093
- const relPath = toForwardSlash(relative(outDir, runtimePkgPath));
3094
- runtimeImportPath = relPath;
3095
- logger.debug(`Using relative path to runtime package: ${relPath}`);
3096
- } else {
3097
- throw new RuntimePackageNotFound({
3098
- message:
3099
- `Failed to access @agentuity/runtime package at ${runtimePkgPath}\n` +
3100
- `Make sure dependencies are installed`,
3101
- });
3102
- }
3103
-
3104
- if (!runtimeImportPath) {
3105
- throw new RuntimePackageNotFound({
3106
- message: `Failed to determine import path for @agentuity/runtime`,
3107
- });
3108
- }
3109
-
3110
- // Now generate state.ts with AppState type
3111
- // NOTE: We can ONLY augment the package name, not relative paths
3112
- // TypeScript resolves @agentuity/runtime through path mapping -> wrapper -> actual package
3113
- const typesContent = `// @generated
3114
- // AUTO-GENERATED from app.ts setup() return type
3115
- // This file is auto-generated by the build tool - do not edit manually
3116
-
3117
- /**
3118
- * Application state type inferred from your createApp setup function.
3119
- * This type is automatically generated and available throughout your app via ctx.app.
3120
- *
3121
- * @example
3122
- * \`\`\`typescript
3123
- * // In your agents:
3124
- * const agent = createAgent({
3125
- * handler: async (ctx, input) => {
3126
- * // ctx.app is strongly typed as GeneratedAppState
3127
- * const value = ctx.app; // All properties from your setup return value
3128
- * return 'result';
3129
- * }
3130
- * });
3131
- * \`\`\`
3132
- */
3133
- export type GeneratedAppState = ${appStateType};
3134
-
3135
- // Augment the @agentuity/runtime module with AppState
3136
- // This will be picked up when imported through the wrapper
3137
- declare module '@agentuity/runtime' {
3138
- interface AppState extends GeneratedAppState {}
3139
- }
3140
-
3141
- // FOUND AN ERROR IN THIS FILE?
3142
- // Please file an issue at https://github.com/agentuity/sdk/issues
3143
- // or if you know the fix please submit a PR!
3144
- `;
3145
- const typesPath = join(outDir, 'state.ts');
3146
- await Bun.write(typesPath, typesContent);
3147
- logger.debug(`Generated lifecycle types: ${typesPath}`);
3148
-
3149
- const wrapperContent = `// @generated
3150
- // AUTO-GENERATED runtime wrapper
3151
- // This file is auto-generated by the build tool - do not edit manually
3152
-
3153
- // Import augmentations file (NOT type-only) to trigger module augmentation
3154
- import type { GeneratedAppState } from './state';
3155
- import './state';
3156
-
3157
- // Import from actual package location
3158
- import { createRouter as baseCreateRouter, type Env } from '${runtimeImportPath}/src/index';
3159
- import type { Hono } from 'hono';
3160
-
3161
- // Type aliases to avoid repeating the generic parameter
3162
- type AppEnv = Env<GeneratedAppState>;
3163
- type AppRouter = Hono<AppEnv>;
3164
-
3165
- /**
3166
- * Creates a Hono router with extended methods for Agentuity-specific routing patterns.
3167
- *
3168
- * In addition to standard HTTP methods (get, post, put, delete, patch), the router includes:
3169
- * - **stream()** - Stream responses with ReadableStream
3170
- * - **websocket()** - WebSocket connections
3171
- * - **sse()** - Server-Sent Events
3172
- * - **email()** - Email handler routing
3173
- * - **sms()** - SMS handler routing
3174
- * - **cron()** - Scheduled task routing
3175
- *
3176
- * @returns Extended Hono router with custom methods and app state typing
3177
- *
3178
- * @example
3179
- * \`\`\`typescript
3180
- * const router = createRouter();
3181
- *
3182
- * // Standard HTTP routes
3183
- * router.get('/hello', (c) => c.text('Hello!'));
3184
- * router.post('/data', async (c) => {
3185
- * const body = await c.req.json();
3186
- * return c.json({ received: body });
3187
- * });
3188
- *
3189
- * // Access app state (strongly typed!)
3190
- * router.get('/db', (c) => {
3191
- * const db = c.var.app; // Your app state from createApp setup
3192
- * return c.json({ connected: true });
3193
- * });
3194
- * \`\`\`
3195
- */
3196
- export function createRouter(): AppRouter {
3197
- return baseCreateRouter() as unknown as AppRouter;
3198
- }
3199
-
3200
- // Re-export everything else
3201
- export * from '${runtimeImportPath}/src/index';
3202
-
3203
- // FOUND AN ERROR IN THIS FILE?
3204
- // Please file an issue at https://github.com/agentuity/sdk/issues
3205
- // or if you know the fix please submit a PR!
3206
- `;
3207
- const wrapperPath = join(outDir, 'router.ts');
3208
- await Bun.write(wrapperPath, wrapperContent);
3209
- logger.debug(`Generated lifecycle wrapper: ${wrapperPath}`);
3210
-
3211
- // Update tsconfig.json with path mapping
3212
- await updateTsconfigPathMapping(rootDir, true);
3213
-
3214
- return true;
3215
- }
3216
-
3217
- /**
3218
- * Analyze workbench usage and extract configuration
3219
- *
3220
- * @param content - The TypeScript source code
3221
- * @returns workbench analysis including usage and config
3222
- */
3223
- export function analyzeWorkbench(content: string): WorkbenchAnalysis {
3224
- try {
3225
- if (!content.includes('@agentuity/workbench')) {
3226
- return {
3227
- hasWorkbench: false,
3228
- config: null,
3229
- };
3230
- }
3231
- const sourceFile = ts.createSourceFile('app.ts', content, ts.ScriptTarget.Latest, true);
3232
-
3233
- let hasImport = false;
3234
- const workbenchVariables = new Map<string, WorkbenchConfig>();
3235
- let usedInServices = false;
3236
-
3237
- function visitNode(node: ts.Node): void {
3238
- // Check for import declarations with createWorkbench
3239
- if (ts.isImportDeclaration(node) && node.importClause?.namedBindings) {
3240
- if (ts.isNamedImports(node.importClause.namedBindings)) {
3241
- for (const element of node.importClause.namedBindings.elements) {
3242
- if (element.name.text === 'createWorkbench') {
3243
- hasImport = true;
3244
- }
3245
- }
3246
- }
3247
- }
3248
-
3249
- // Track variables assigned from createWorkbench() calls
3250
- if (
3251
- ts.isVariableDeclaration(node) &&
3252
- node.initializer &&
3253
- ts.isCallExpression(node.initializer) &&
3254
- ts.isIdentifier(node.initializer.expression) &&
3255
- node.initializer.expression.text === 'createWorkbench'
3256
- ) {
3257
- // Extract variable name
3258
- if (ts.isIdentifier(node.name)) {
3259
- // Extract configuration from the first argument (if any)
3260
- let varConfig: WorkbenchConfig;
3261
- const firstInitArg = node.initializer.arguments[0];
3262
- if (node.initializer.arguments.length > 0 && firstInitArg) {
3263
- varConfig = parseConfigObject(firstInitArg) || { route: '/workbench' };
3264
- } else {
3265
- // Default config if no arguments provided
3266
- varConfig = { route: '/workbench' };
3267
- }
3268
- workbenchVariables.set(node.name.text, varConfig);
3269
- }
3270
- }
3271
-
3272
- // Check if workbench variable is used in createApp's services config
3273
- if (
3274
- ts.isCallExpression(node) &&
3275
- ts.isIdentifier(node.expression) &&
3276
- node.expression.text === 'createApp' &&
3277
- node.arguments.length > 0
3278
- ) {
3279
- const createAppConfigArg = node.arguments[0];
3280
- if (createAppConfigArg && ts.isObjectLiteralExpression(createAppConfigArg)) {
3281
- // Find the services property
3282
- for (const prop of createAppConfigArg.properties) {
3283
- if (
3284
- ts.isPropertyAssignment(prop) &&
3285
- ts.isIdentifier(prop.name) &&
3286
- prop.name.text === 'services'
3287
- ) {
3288
- // Check if any workbench variable is referenced in services
3289
- const foundVariableName = checkForWorkbenchUsage(
3290
- prop.initializer,
3291
- workbenchVariables
3292
- );
3293
- if (foundVariableName) {
3294
- usedInServices = true;
3295
- }
3296
- break;
3297
- }
3298
- }
3299
- }
3300
- }
3301
-
3302
- // Recursively visit child nodes
3303
- ts.forEachChild(node, visitNode);
3304
- }
3305
-
3306
- // Helper function to check if workbench variable is used in services config
3307
- // Returns the variable name if found, otherwise null
3308
- function checkForWorkbenchUsage(
3309
- node: ts.Node,
3310
- variables: Map<string, WorkbenchConfig>
3311
- ): string | null {
3312
- let foundVar: string | null = null;
3313
-
3314
- function visit(n: ts.Node): void {
3315
- if (foundVar) return; // Already found
3316
-
3317
- // Check for identifier references
3318
- if (ts.isIdentifier(n) && variables.has(n.text)) {
3319
- foundVar = n.text;
3320
- return;
3321
- }
3322
-
3323
- // Check for property shorthand: { workbench } in services
3324
- if (ts.isShorthandPropertyAssignment(n) && variables.has(n.name.text)) {
3325
- foundVar = n.name.text;
3326
- return;
3327
- }
3328
-
3329
- // Check for property value: { workbench: workbench } in services
3330
- if (
3331
- ts.isPropertyAssignment(n) &&
3332
- n.initializer &&
3333
- ts.isIdentifier(n.initializer) &&
3334
- variables.has(n.initializer.text)
3335
- ) {
3336
- foundVar = n.initializer.text;
3337
- return;
3338
- }
3339
-
3340
- ts.forEachChild(n, visit);
3341
- }
3342
-
3343
- visit(node);
3344
- return foundVar;
3345
- }
3346
-
3347
- visitNode(sourceFile);
3348
-
3349
- // Get the config from the first used workbench variable
3350
- let config: WorkbenchConfig | null = null;
3351
- if (hasImport && usedInServices) {
3352
- // Re-check which variable was used
3353
- const ast = sourceFile;
3354
- for (const [varName, varConfig] of workbenchVariables.entries()) {
3355
- // Simple check: walk through and find if this variable is used in services
3356
- let used = false;
3357
- function checkUsage(node: ts.Node): void {
3358
- if (
3359
- ts.isCallExpression(node) &&
3360
- ts.isIdentifier(node.expression) &&
3361
- node.expression.text === 'createApp' &&
3362
- node.arguments.length > 0
3363
- ) {
3364
- const checkConfigArg = node.arguments[0];
3365
- if (checkConfigArg && ts.isObjectLiteralExpression(checkConfigArg)) {
3366
- for (const prop of checkConfigArg.properties) {
3367
- if (
3368
- ts.isPropertyAssignment(prop) &&
3369
- ts.isIdentifier(prop.name) &&
3370
- prop.name.text === 'services'
3371
- ) {
3372
- const foundVar = checkForWorkbenchUsage(
3373
- prop.initializer,
3374
- workbenchVariables
3375
- );
3376
- if (foundVar === varName) {
3377
- used = true;
3378
- config = varConfig;
3379
- }
3380
- break;
3381
- }
3382
- }
3383
- }
3384
- }
3385
- ts.forEachChild(node, checkUsage);
3386
- }
3387
- checkUsage(ast);
3388
- if (used) break;
3389
- }
3390
- }
3391
-
3392
- return {
3393
- hasWorkbench: hasImport && usedInServices,
3394
- config: config,
3395
- };
3396
- } catch (error) {
3397
- // Fallback to simple check if AST parsing fails
3398
- logger.warn('Workbench AST parsing failed, falling back to string check:', error);
3399
- const hasWorkbench = content.includes('createWorkbench');
3400
- return {
3401
- hasWorkbench,
3402
- config: hasWorkbench ? { route: '/workbench' } : null,
3403
- };
3404
- }
3405
- }
3406
-
3407
- /**
3408
- * Parse a TypeScript object literal to extract configuration
3409
- */
3410
- function parseConfigObject(node: ts.Node): WorkbenchConfig | null {
3411
- if (!ts.isObjectLiteralExpression(node)) {
3412
- return { route: '/workbench' }; // Default config
3413
- }
3414
-
3415
- const config: WorkbenchConfig = { route: '/workbench' };
3416
-
3417
- for (const property of node.properties) {
3418
- if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.name)) {
3419
- const propertyName = property.name.text;
3420
-
3421
- if (propertyName === 'route' && ts.isStringLiteral(property.initializer)) {
3422
- config.route = property.initializer.text;
3423
- } else if (
3424
- propertyName === 'headers' &&
3425
- ts.isObjectLiteralExpression(property.initializer)
3426
- ) {
3427
- // Parse headers object if needed (not implemented for now)
3428
- config.headers = {};
3429
- }
3430
- }
3431
- }
3432
-
3433
- return config;
3434
- }
3435
-
3436
- /**
3437
- * Find the end position of createApp call statement in the source code
3438
- * Uses AST parsing to reliably find the complete statement including await/const assignment
3439
- *
3440
- * @param content - The source code content
3441
- * @returns The character position after the createApp statement, or -1 if not found
3442
- */
3443
- export function findCreateAppEndPosition(content: string): number {
3444
- try {
3445
- const ast = acornLoose.parse(content, {
3446
- ecmaVersion: 'latest',
3447
- sourceType: 'module',
3448
- }) as ASTProgram;
3449
-
3450
- // Walk through all top-level statements
3451
- for (const node of ast.body) {
3452
- let targetNode: ASTNode | undefined;
3453
-
3454
- // Check for: const app = await createApp(...)
3455
- if (node.type === 'VariableDeclaration') {
3456
- const varDecl = node as unknown as { declarations: ASTVariableDeclarator[] };
3457
- for (const declarator of varDecl.declarations) {
3458
- if (declarator.init) {
3459
- // Handle await createApp(...)
3460
- if (declarator.init.type === 'AwaitExpression') {
3461
- const awaitExpr = declarator.init as unknown as {
3462
- argument: ASTCallExpression;
3463
- };
3464
- if (
3465
- awaitExpr.argument?.type === 'CallExpression' &&
3466
- isCreateAppCall(awaitExpr.argument)
3467
- ) {
3468
- targetNode = node;
3469
- break;
3470
- }
3471
- }
3472
- // Handle createApp(...) without await
3473
- else if (declarator.init.type === 'CallExpression') {
3474
- if (isCreateAppCall(declarator.init as ASTCallExpression)) {
3475
- targetNode = node;
3476
- break;
3477
- }
3478
- }
3479
- }
3480
- }
3481
- }
3482
- // Check for: await createApp(...)
3483
- else if (node.type === 'ExpressionStatement') {
3484
- const exprStmt = node as ASTExpressionStatement;
3485
- if (exprStmt.expression.type === 'AwaitExpression') {
3486
- const awaitExpr = exprStmt.expression as unknown as { argument: ASTCallExpression };
3487
- if (
3488
- awaitExpr.argument?.type === 'CallExpression' &&
3489
- isCreateAppCall(awaitExpr.argument)
3490
- ) {
3491
- targetNode = node;
3492
- }
3493
- } else if (exprStmt.expression.type === 'CallExpression') {
3494
- if (isCreateAppCall(exprStmt.expression as ASTCallExpression)) {
3495
- targetNode = node;
3496
- }
3497
- }
3498
- }
3499
-
3500
- if (targetNode && targetNode.end !== undefined) {
3501
- // Find the semicolon after the statement (if it exists)
3502
- const afterStmt = content.slice(targetNode.end);
3503
- const semiMatch = afterStmt.match(/^\s*;/);
3504
- if (semiMatch) {
3505
- return targetNode.end + semiMatch[0].length;
3506
- }
3507
- // No semicolon, return end of statement
3508
- return targetNode.end;
3509
- }
3510
- }
3511
-
3512
- return -1;
3513
- } catch (error) {
3514
- logger.warn('Failed to parse AST for createApp detection:', error);
3515
- return -1;
3516
- }
3517
- }
3518
-
3519
- /**
3520
- * Check if a CallExpression is a call to createApp
3521
- */
3522
- function isCreateAppCall(node: ASTCallExpression): boolean {
3523
- const callee = node.callee;
3524
- if (callee.type === 'Identifier') {
3525
- const id = callee as ASTNodeIdentifier;
3526
- return id.name === 'createApp';
3527
- }
3528
- return false;
3529
- }