@agentuity/cli 1.0.47 → 2.0.0-beta.0

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