@agentuity/cli 1.0.48 → 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 (124) 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 -21
  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 -36
  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/cloud/deploy.d.ts.map +1 -1
  55. package/dist/cmd/cloud/deploy.js +0 -1
  56. package/dist/cmd/cloud/deploy.js.map +1 -1
  57. package/dist/cmd/dev/file-watcher.d.ts.map +1 -1
  58. package/dist/cmd/dev/file-watcher.js +2 -8
  59. package/dist/cmd/dev/file-watcher.js.map +1 -1
  60. package/dist/cmd/dev/index.d.ts.map +1 -1
  61. package/dist/cmd/dev/index.js +369 -720
  62. package/dist/cmd/dev/index.js.map +1 -1
  63. package/package.json +6 -8
  64. package/src/cmd/ai/prompt/agent.md +0 -1
  65. package/src/cmd/ai/prompt/api.md +0 -7
  66. package/src/cmd/ai/prompt/web.md +51 -213
  67. package/src/cmd/build/app-router-detector.ts +152 -182
  68. package/src/cmd/build/ids.ts +19 -0
  69. package/src/cmd/build/vite/agent-discovery.ts +208 -679
  70. package/src/cmd/build/vite/bun-dev-server.ts +78 -154
  71. package/src/cmd/build/vite/docs-generator.ts +0 -2
  72. package/src/cmd/build/vite/index.ts +1 -42
  73. package/src/cmd/build/vite/lifecycle-generator.ts +345 -21
  74. package/src/cmd/build/vite/route-discovery.ts +116 -274
  75. package/src/cmd/build/vite/server-bundler.ts +1 -1
  76. package/src/cmd/build/vite/static-renderer.ts +1 -11
  77. package/src/cmd/build/vite/vite-asset-server-config.ts +196 -23
  78. package/src/cmd/build/vite/vite-asset-server.ts +25 -15
  79. package/src/cmd/build/vite/vite-builder.ts +6 -53
  80. package/src/cmd/build/vite/ws-proxy.ts +126 -0
  81. package/src/cmd/build/vite-bundler.ts +0 -4
  82. package/src/cmd/cloud/deploy.ts +0 -1
  83. package/src/cmd/dev/file-watcher.ts +2 -9
  84. package/src/cmd/dev/index.ts +409 -832
  85. package/dist/cmd/build/ast.d.ts +0 -78
  86. package/dist/cmd/build/ast.d.ts.map +0 -1
  87. package/dist/cmd/build/ast.js +0 -2703
  88. package/dist/cmd/build/ast.js.map +0 -1
  89. package/dist/cmd/build/entry-generator.d.ts +0 -25
  90. package/dist/cmd/build/entry-generator.d.ts.map +0 -1
  91. package/dist/cmd/build/entry-generator.js +0 -695
  92. package/dist/cmd/build/entry-generator.js.map +0 -1
  93. package/dist/cmd/build/vite/api-mount-path.d.ts +0 -61
  94. package/dist/cmd/build/vite/api-mount-path.d.ts.map +0 -1
  95. package/dist/cmd/build/vite/api-mount-path.js +0 -83
  96. package/dist/cmd/build/vite/api-mount-path.js.map +0 -1
  97. package/dist/cmd/build/vite/registry-generator.d.ts +0 -19
  98. package/dist/cmd/build/vite/registry-generator.d.ts.map +0 -1
  99. package/dist/cmd/build/vite/registry-generator.js +0 -1108
  100. package/dist/cmd/build/vite/registry-generator.js.map +0 -1
  101. package/dist/cmd/build/vite/tailwind-source-plugin.d.ts +0 -13
  102. package/dist/cmd/build/vite/tailwind-source-plugin.d.ts.map +0 -1
  103. package/dist/cmd/build/vite/tailwind-source-plugin.js +0 -44
  104. package/dist/cmd/build/vite/tailwind-source-plugin.js.map +0 -1
  105. package/dist/cmd/build/webanalytics-generator.d.ts +0 -16
  106. package/dist/cmd/build/webanalytics-generator.d.ts.map +0 -1
  107. package/dist/cmd/build/webanalytics-generator.js +0 -178
  108. package/dist/cmd/build/webanalytics-generator.js.map +0 -1
  109. package/dist/cmd/build/workbench.d.ts +0 -7
  110. package/dist/cmd/build/workbench.d.ts.map +0 -1
  111. package/dist/cmd/build/workbench.js +0 -55
  112. package/dist/cmd/build/workbench.js.map +0 -1
  113. package/dist/utils/route-migration.d.ts +0 -62
  114. package/dist/utils/route-migration.d.ts.map +0 -1
  115. package/dist/utils/route-migration.js +0 -630
  116. package/dist/utils/route-migration.js.map +0 -1
  117. package/src/cmd/build/ast.ts +0 -3529
  118. package/src/cmd/build/entry-generator.ts +0 -760
  119. package/src/cmd/build/vite/api-mount-path.ts +0 -87
  120. package/src/cmd/build/vite/registry-generator.ts +0 -1267
  121. package/src/cmd/build/vite/tailwind-source-plugin.ts +0 -54
  122. package/src/cmd/build/webanalytics-generator.ts +0 -197
  123. package/src/cmd/build/workbench.ts +0 -58
  124. package/src/utils/route-migration.ts +0 -757
@@ -1,1108 +0,0 @@
1
- /**
2
- * Registry Generator
3
- *
4
- * Generates src/generated/registry.ts from discovered agents
5
- */
6
- import { join, dirname, relative, resolve } from 'node:path';
7
- import { writeFileSync, mkdirSync, existsSync, unlinkSync, readFileSync } from 'node:fs';
8
- import { stat } from 'node:fs/promises';
9
- import { StructuredError } from '@agentuity/core';
10
- import { toCamelCase, toPascalCase } from '../../../utils/string';
11
- import { toForwardSlash } from '../../../utils/normalize-path';
12
- /**
13
- * Rebase a relative import path from the route file's location to the generated file's location.
14
- * @param routeFilename - The route file path (e.g., 'api/example/route.ts' or './api/example/route.ts')
15
- * @param schemaImportPath - The import path as written in the route file (e.g., '../../utils/schemas')
16
- * @param srcDir - The src directory path
17
- * @returns The rebased import path relative to src/generated/
18
- */
19
- function rebaseImportPath(routeFilename, schemaImportPath, srcDir) {
20
- // Non-relative imports (bare modules like '@company/schemas') should be used as-is
21
- if (!schemaImportPath.startsWith('.') && !schemaImportPath.startsWith('/')) {
22
- return schemaImportPath;
23
- }
24
- // Normalize route filename to get its directory relative to srcDir
25
- let routeDir;
26
- const cleanFilename = toForwardSlash(routeFilename);
27
- if (cleanFilename.startsWith('./')) {
28
- routeDir = dirname(join(srcDir, cleanFilename.substring(2)));
29
- }
30
- else if (cleanFilename.startsWith('src/')) {
31
- routeDir = dirname(join(srcDir, '..', cleanFilename));
32
- }
33
- else {
34
- routeDir = dirname(join(srcDir, cleanFilename));
35
- }
36
- // Resolve the schema module path from the route file's directory
37
- const resolvedSchemaPath = resolve(routeDir, schemaImportPath);
38
- // Calculate the relative path from src/generated/ to the resolved schema path
39
- const generatedDir = join(srcDir, 'generated');
40
- let rebasedPath = toForwardSlash(relative(generatedDir, resolvedSchemaPath));
41
- // Ensure it starts with './' or '../'
42
- if (!rebasedPath.startsWith('.') && !rebasedPath.startsWith('/')) {
43
- rebasedPath = './' + rebasedPath;
44
- }
45
- return rebasedPath;
46
- }
47
- const AgentIdentifierCollisionError = StructuredError('AgentIdentifierCollisionError');
48
- /**
49
- * Regex to strip route parameter characters that produce invalid TypeScript property names:
50
- * - Leading : (path parameters, e.g., :id)
51
- * - Leading * (wildcard routes, e.g., *path)
52
- * - Trailing ?, +, * (optional/one-or-more/wildcard modifiers, e.g., :userId?)
53
- */
54
- const ROUTE_PARAM_CHARS = /^[:*]|[?+*]$/g;
55
- /**
56
- * Sanitize a route path segment for use as a TypeScript property name.
57
- * Strips route parameter characters and converts to camelCase.
58
- */
59
- function sanitizePathSegment(segment) {
60
- return toCamelCase(segment.replace(ROUTE_PARAM_CHARS, ''));
61
- }
62
- /**
63
- * Valid unquoted TypeScript/JavaScript property name pattern.
64
- * A property name can be unquoted if it starts with a letter, underscore, or dollar sign,
65
- * and contains only letters, digits, underscores, or dollar signs.
66
- */
67
- const VALID_UNQUOTED_PROPERTY = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
68
- /**
69
- * Quote a property name for TypeScript object type definitions if it contains
70
- * characters that require quoting (e.g., dots, hyphens, spaces).
71
- */
72
- function quotePropertyName(name) {
73
- return VALID_UNQUOTED_PROPERTY.test(name) ? name : JSON.stringify(name);
74
- }
75
- /**
76
- * Generate TypeScript type for path parameters.
77
- * Returns 'never' if no path params, or '{ param1: string; param2: string }' format.
78
- */
79
- function generatePathParamsType(pathParams) {
80
- if (!pathParams || pathParams.length === 0) {
81
- return 'never';
82
- }
83
- return `{ ${pathParams.map((p) => `${p}: string`).join('; ')} }`;
84
- }
85
- /**
86
- * Generate TypeScript tuple type for path parameters (for positional args).
87
- * Returns '[]' if no path params, or '[string, string]' format.
88
- */
89
- function generatePathParamsTupleType(pathParams) {
90
- if (!pathParams || pathParams.length === 0) {
91
- return '[]';
92
- }
93
- return `[${pathParams.map(() => 'string').join(', ')}]`;
94
- }
95
- /**
96
- * Generate src/generated/registry.ts with agent registry and types
97
- */
98
- export function generateAgentRegistry(srcDir, agents) {
99
- const generatedDir = join(srcDir, 'generated');
100
- const registryPath = join(generatedDir, 'registry.ts');
101
- // Sort agents by name for deterministic output
102
- const sortedAgents = [...agents].sort((a, b) => a.name.localeCompare(b.name));
103
- // Detect naming collisions in generated identifiers
104
- const generatedNames = new Set();
105
- const collisions = [];
106
- for (const agent of sortedAgents) {
107
- const camelName = toCamelCase(agent.name);
108
- if (generatedNames.has(camelName)) {
109
- collisions.push(`Identifier collision detected: "${camelName}" (from "${agent.name}")`);
110
- }
111
- generatedNames.add(camelName);
112
- }
113
- if (collisions.length > 0) {
114
- throw new AgentIdentifierCollisionError({
115
- message: `Agent identifier naming collisions detected:\n${collisions.join('\n')}\n\n` +
116
- `This occurs when different agent names produce the same camelCase identifier.\n` +
117
- `Please rename your agents to avoid this collision.`,
118
- });
119
- }
120
- // Collect eval files that need to be imported for createEval calls to run
121
- // These are eval.ts files in the same directory as agents that have evals
122
- const evalImports = [];
123
- const seenEvalPaths = new Set();
124
- for (const agent of sortedAgents) {
125
- if (agent.evals && agent.evals.length > 0) {
126
- // Check if any eval comes from a separate eval.ts file (not the agent file itself)
127
- for (const evalMeta of agent.evals) {
128
- // Skip if eval is defined in the agent file itself
129
- if (evalMeta.filename === agent.filename)
130
- continue;
131
- // Build the relative path for the eval file
132
- let evalRelativePath = toForwardSlash(evalMeta.filename);
133
- if (evalRelativePath.startsWith('./agent/')) {
134
- evalRelativePath = evalRelativePath
135
- .replace(/^\.\/agent\//, '../agent/')
136
- .replace(/\.tsx?$/, '.js');
137
- }
138
- else if (evalRelativePath.startsWith('src/agent/')) {
139
- evalRelativePath = evalRelativePath
140
- .replace(/^src\/agent\//, '../agent/')
141
- .replace(/\.tsx?$/, '.js');
142
- }
143
- else if (evalRelativePath.includes('/src/agent/')) {
144
- // Handle absolute paths by extracting the relative part
145
- evalRelativePath = evalRelativePath
146
- .replace(/^.*\/src\/agent\//, '../agent/')
147
- .replace(/\.tsx?$/, '.js');
148
- }
149
- // Avoid duplicate imports
150
- if (!seenEvalPaths.has(evalRelativePath)) {
151
- seenEvalPaths.add(evalRelativePath);
152
- evalImports.push(`import '${evalRelativePath}';`);
153
- }
154
- }
155
- }
156
- }
157
- // Generate imports for all agents
158
- const imports = sortedAgents
159
- .map(({ name, filename }) => {
160
- const camelName = toCamelCase(name);
161
- // Handle both './agent/...' and 'src/agent/...' formats
162
- let relativePath = toForwardSlash(filename);
163
- if (relativePath.startsWith('./agent/')) {
164
- // ./agent/foo.ts -> ../agent/foo.js (use .js extension for TypeScript)
165
- relativePath = relativePath
166
- .replace(/^\.\/agent\//, '../agent/')
167
- .replace(/\.tsx?$/, '.js');
168
- }
169
- else if (relativePath.startsWith('src/agent/')) {
170
- // src/agent/foo.ts -> ../agent/foo.js (use .js extension for TypeScript)
171
- relativePath = relativePath
172
- .replace(/^src\/agent\//, '../agent/')
173
- .replace(/\.tsx?$/, '.js');
174
- }
175
- return `import ${camelName} from '${relativePath}';`;
176
- })
177
- .join('\n');
178
- // Generate schema type exports for all agents
179
- const schemaTypeExports = sortedAgents
180
- .map(({ name, description }) => {
181
- const camelName = toCamelCase(name);
182
- const pascalName = toPascalCase(name);
183
- const descComment = description ? `\n * ${description}` : '';
184
- const parts = [
185
- '',
186
- `/**`,
187
- ` * Input type for ${name} agent${descComment}`,
188
- ` */`,
189
- `export type ${pascalName}Input = InferInput<typeof ${camelName}['inputSchema']>;`,
190
- '',
191
- `/**`,
192
- ` * Output type for ${name} agent${descComment}`,
193
- ` */`,
194
- `export type ${pascalName}Output = InferOutput<typeof ${camelName}['outputSchema']>;`,
195
- '',
196
- `/**`,
197
- ` * Input schema type for ${name} agent${descComment}`,
198
- ` */`,
199
- `export type ${pascalName}InputSchema = typeof ${camelName}['inputSchema'];`,
200
- '',
201
- `/**`,
202
- ` * Output schema type for ${name} agent${descComment}`,
203
- ` */`,
204
- `export type ${pascalName}OutputSchema = typeof ${camelName}['outputSchema'];`,
205
- '',
206
- `/**`,
207
- ` * Agent type for ${name}${descComment}`,
208
- ` */`,
209
- `export type ${pascalName}Agent = AgentRunner<`,
210
- `\t${pascalName}InputSchema,`,
211
- `\t${pascalName}OutputSchema,`,
212
- `\ttypeof ${camelName}['stream'] extends true ? true : false`,
213
- `>;`,
214
- ];
215
- return parts.join('\n');
216
- })
217
- .join('\n');
218
- // Generate flat registry structure with JSDoc
219
- const registry = sortedAgents
220
- .map(({ name, description }) => {
221
- const camelName = toCamelCase(name);
222
- const pascalName = toPascalCase(name);
223
- const descComment = description ? `\n\t * ${description}` : '';
224
- return `\t/**
225
- \t * ${name}${descComment}
226
- \t * @type {${pascalName}Agent}
227
- \t */
228
- \t${camelName},`;
229
- })
230
- .join('\n');
231
- // Generate flat agent type definitions for AgentRegistry interface augmentation
232
- // Uses the exported Agent types defined above
233
- const runtimeAgentTypes = sortedAgents
234
- .map(({ name }) => {
235
- const camelName = toCamelCase(name);
236
- const pascalName = toPascalCase(name);
237
- return ` ${camelName}: ${pascalName}Agent;`;
238
- })
239
- .join('\n');
240
- // Build eval imports section (side-effect imports for createEval registration)
241
- const evalImportsSection = evalImports.length > 0
242
- ? `
243
- // Eval file imports (side-effect imports to register evals via createEval)
244
- ${evalImports.join('\n')}
245
- `
246
- : '';
247
- const generatedContent = `// @generated
248
- // Auto-generated by Agentuity - DO NOT EDIT
249
- ${imports}
250
- import type { AgentRunner } from '@agentuity/runtime';
251
- import type { InferInput, InferOutput } from '@agentuity/core';
252
- ${evalImportsSection}
253
-
254
- // ============================================================================
255
- // Schema Type Exports
256
- // ============================================================================
257
- ${schemaTypeExports}
258
-
259
- // ============================================================================
260
- // Agent Definitions
261
- // ============================================================================
262
-
263
- /**
264
- * Agent Definitions
265
- *
266
- * Registry of all agents in this application.
267
- * Provides strongly-typed access to agent metadata and runner functions.
268
- *
269
- * @remarks
270
- * This object is auto-generated from your agent files during build.
271
- * Each agent has corresponding Input, Output, and Runner types exported above.
272
- *
273
- * @example
274
- * \`\`\`typescript
275
- * import { AgentDefinitions, SessionBasicInput } from './generated/registry';
276
- *
277
- * // Access agent definition
278
- * const agent = AgentDefinitions.sessionBasic;
279
- *
280
- * // Use typed schema types
281
- * const input: SessionBasicInput = { ... };
282
- * const result = await agent.run(input);
283
- * \`\`\`
284
- */
285
- export const AgentDefinitions = {
286
- ${registry}
287
- } as const;
288
-
289
- // ============================================================================
290
- // Module Augmentation
291
- // ============================================================================
292
-
293
- // Augment @agentuity/runtime types with strongly-typed agents from this project
294
- declare module "@agentuity/runtime" {
295
- // Augment the AgentRegistry interface with project-specific strongly-typed agents
296
- export interface AgentRegistry {
297
- ${runtimeAgentTypes}
298
- }
299
- }
300
-
301
- // FOUND AN ERROR IN THIS FILE?
302
- // Please file an issue at https://github.com/agentuity/sdk/issues
303
- // or if you know the fix please submit a PR!
304
- `;
305
- const agentsDir = join(srcDir, 'agent');
306
- const legacyTypesPath = join(agentsDir, 'types.generated.d.ts');
307
- // Ensure src/generated directory exists
308
- if (!existsSync(generatedDir)) {
309
- mkdirSync(generatedDir, { recursive: true });
310
- }
311
- // Collapse 2+ consecutive empty lines into 1 empty line (3+ \n becomes 2 \n)
312
- const cleanedContent = generatedContent.replace(/\n{3,}/g, '\n\n');
313
- writeFileSync(registryPath, cleanedContent, 'utf-8');
314
- // Remove legacy types.generated.d.ts if it exists (legacy cleanup)
315
- if (existsSync(legacyTypesPath)) {
316
- unlinkSync(legacyTypesPath);
317
- }
318
- }
319
- /**
320
- * Helper function to generate RPC-style nested registry type.
321
- * Converts routes like "POST /api/hello" to nested structure: post.api.hello
322
- */
323
- function generateRPCRegistryType(apiRoutes, websocketRoutes, sseRoutes, agentImports, _schemaImportAliases, agentMetadataMap) {
324
- const tree = {};
325
- // Helper to add route to tree
326
- const addRoute = (route, routeType) => {
327
- const method = route.method.toLowerCase();
328
- // Strip /api prefix from path
329
- let cleanPath = route.path;
330
- if (cleanPath.startsWith('/api/')) {
331
- cleanPath = cleanPath.substring(4); // Remove '/api'
332
- }
333
- else if (cleanPath === '/api') {
334
- cleanPath = '/';
335
- }
336
- const pathParts = cleanPath.split('/').filter(Boolean);
337
- // Navigate/create tree structure: path segments first, then method
338
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
339
- let current = tree;
340
- // Add path segments - sanitize for valid TypeScript property names
341
- for (let i = 0; i < pathParts.length; i++) {
342
- const rawPart = pathParts[i];
343
- if (!rawPart) {
344
- continue;
345
- }
346
- const part = sanitizePathSegment(rawPart);
347
- // Skip empty segments (e.g., wildcards like '*' that sanitize to '')
348
- if (!part) {
349
- continue;
350
- }
351
- if (!current[part]) {
352
- current[part] = {};
353
- }
354
- current = current[part];
355
- }
356
- // Determine terminal method name based on route type
357
- // For stream types (websocket, sse, stream), use the type name as the method
358
- // For regular API routes, use the HTTP method
359
- const terminalMethod = routeType === 'websocket'
360
- ? 'websocket'
361
- : routeType === 'sse'
362
- ? 'eventstream'
363
- : routeType === 'stream'
364
- ? 'stream'
365
- : method;
366
- // Add method as final level with schema types
367
- const routeKey = `${route.method.toUpperCase()} ${route.path}`;
368
- const safeName = routeKey
369
- .replace(/[^a-zA-Z0-9]/g, '_')
370
- .replace(/^_+|_+$/g, '')
371
- .replace(/_+/g, '_');
372
- const pascalName = toPascalCase(safeName);
373
- // Only reference type names if route has actual schemas extracted, otherwise use 'never'
374
- // Note: hasValidator may be true (e.g., zValidator('query', ...)) but no schemas extracted
375
- // because only 'json' validators extract input schemas
376
- // Also check if agentVariable exists but import wasn't added (missing agentImportPath)
377
- const hasValidAgentImport = route.agentVariable
378
- ? !!agentImports.get(route.agentVariable)
379
- : false;
380
- const hasSchemas = route.inputSchemaVariable || route.outputSchemaVariable || hasValidAgentImport;
381
- current[terminalMethod] = {
382
- input: hasSchemas ? `${pascalName}Input` : 'never',
383
- output: hasSchemas ? `${pascalName}Output` : 'never',
384
- type: `'${routeType}'`,
385
- route,
386
- };
387
- };
388
- // Add all routes with their types
389
- apiRoutes.forEach((route) => {
390
- const routeType = route.routeType === 'stream' ? 'stream' : 'api';
391
- addRoute(route, routeType);
392
- });
393
- websocketRoutes.forEach((route) => addRoute(route, 'websocket'));
394
- sseRoutes.forEach((route) => addRoute(route, 'sse'));
395
- // Convert tree to TypeScript type string
396
- function treeToTypeString(node, indent = '\t\t') {
397
- const lines = [];
398
- // Sort entries alphabetically for deterministic output
399
- const sortedEntries = Object.entries(node).sort(([a], [b]) => a.localeCompare(b));
400
- for (const [key, value] of sortedEntries) {
401
- if (value &&
402
- typeof value === 'object' &&
403
- 'input' in value &&
404
- 'output' in value &&
405
- 'type' in value &&
406
- 'route' in value) {
407
- // Leaf node with schema and type - add JSDoc
408
- const route = value.route;
409
- const jsdoc = [];
410
- // Access route info from value
411
- const routeInfo = route;
412
- // Look up agent metadata
413
- let agentMeta;
414
- if (routeInfo.agentVariable) {
415
- agentMeta = agentMetadataMap.get(routeInfo.agentVariable);
416
- }
417
- // Build JSDoc comment
418
- jsdoc.push(`${indent}/**`);
419
- jsdoc.push(`${indent} * Route: ${routeInfo.method.toUpperCase()} ${routeInfo.path}`);
420
- if (agentMeta?.name) {
421
- jsdoc.push(`${indent} * @agent ${agentMeta.name}`);
422
- }
423
- if (agentMeta?.description) {
424
- jsdoc.push(`${indent} * @description ${agentMeta.description}`);
425
- }
426
- jsdoc.push(`${indent} */`);
427
- lines.push(...jsdoc);
428
- const pathParamsType = generatePathParamsType(routeInfo.pathParams);
429
- const pathParamsTupleType = generatePathParamsTupleType(routeInfo.pathParams);
430
- lines.push(`${indent}${quotePropertyName(key)}: { input: ${value.input}; output: ${value.output}; type: ${value.type}; params: ${pathParamsType}; paramsTuple: ${pathParamsTupleType} };`);
431
- }
432
- else {
433
- // Nested node
434
- lines.push(`${indent}${quotePropertyName(key)}: {`);
435
- lines.push(treeToTypeString(value, indent + '\t'));
436
- lines.push(`${indent}};`);
437
- }
438
- }
439
- return lines.join('\n');
440
- }
441
- if (Object.keys(tree).length === 0) {
442
- return '\t\t// No routes discovered';
443
- }
444
- return treeToTypeString(tree);
445
- }
446
- /**
447
- * Generate runtime metadata object for RPC routes.
448
- * This allows the client to know route types at runtime.
449
- */
450
- function generateRPCRuntimeMetadata(apiRoutes, websocketRoutes, sseRoutes) {
451
- const tree = {};
452
- const addRoute = (route, routeType) => {
453
- let cleanPath = route.path;
454
- if (cleanPath.startsWith('/api/')) {
455
- cleanPath = cleanPath.substring(4);
456
- }
457
- else if (cleanPath === '/api') {
458
- cleanPath = '/';
459
- }
460
- const pathParts = cleanPath.split('/').filter(Boolean);
461
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
462
- let current = tree;
463
- // Sanitize path segments for valid property names (must match type generation)
464
- for (const part of pathParts) {
465
- const sanitized = sanitizePathSegment(part);
466
- // Skip empty segments (e.g., wildcards like '*' that sanitize to '')
467
- if (!sanitized) {
468
- continue;
469
- }
470
- if (!current[sanitized])
471
- current[sanitized] = {};
472
- current = current[sanitized];
473
- }
474
- // Use terminal method name based on route type
475
- const terminalMethod = routeType === 'websocket'
476
- ? 'websocket'
477
- : routeType === 'sse'
478
- ? 'eventstream'
479
- : routeType === 'stream'
480
- ? 'stream'
481
- : route.method.toLowerCase();
482
- const metadata = {
483
- type: routeType,
484
- path: route.path,
485
- };
486
- if (route.pathParams && route.pathParams.length > 0) {
487
- metadata.pathParams = route.pathParams;
488
- }
489
- current[terminalMethod] = metadata;
490
- };
491
- apiRoutes.forEach((r) => addRoute(r, r.routeType === 'stream' ? 'stream' : 'api'));
492
- websocketRoutes.forEach((r) => addRoute(r, 'websocket'));
493
- sseRoutes.forEach((r) => addRoute(r, 'sse'));
494
- // Sort object keys recursively for deterministic output
495
- const sortObject = (obj) => {
496
- const sorted = {};
497
- for (const key of Object.keys(obj).sort()) {
498
- const value = obj[key];
499
- if (value === undefined) {
500
- continue;
501
- }
502
- if (typeof value === 'object' && !('type' in value)) {
503
- sorted[key] = sortObject(value);
504
- }
505
- else {
506
- sorted[key] = value;
507
- }
508
- }
509
- return sorted;
510
- };
511
- return JSON.stringify(sortObject(tree), null, '\t\t');
512
- }
513
- /**
514
- * Generate RouteRegistry type definitions from discovered routes.
515
- *
516
- * Creates a module augmentation for @agentuity/react that provides
517
- * strongly-typed route keys with input/output schema information.
518
- */
519
- export async function generateRouteRegistry(srcDir, routes, agents = []) {
520
- const projectRoot = join(srcDir, '..');
521
- const packageJsonPath = join(projectRoot, 'package.json');
522
- let hasReactDependency = false;
523
- let hasFrontendDependency = false;
524
- try {
525
- const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
526
- hasReactDependency = !!(packageJson.dependencies?.['@agentuity/react'] ||
527
- packageJson.devDependencies?.['@agentuity/react']);
528
- hasFrontendDependency = !!(packageJson.dependencies?.['@agentuity/frontend'] ||
529
- packageJson.devDependencies?.['@agentuity/frontend']);
530
- }
531
- catch {
532
- // If we can't read package.json, assume no frontend dependencies
533
- }
534
- const webDir = join(srcDir, 'web');
535
- let hasWebDirectory = false;
536
- try {
537
- const webDirStat = await stat(webDir);
538
- hasWebDirectory = webDirStat.isDirectory();
539
- }
540
- catch {
541
- // Directory doesn't exist
542
- }
543
- const shouldEmitFrontendClient = hasFrontendDependency && !hasReactDependency && hasWebDirectory;
544
- // Filter routes by type and sort by path for deterministic output
545
- const sortByPath = (a, b) => a.path.localeCompare(b.path);
546
- const apiRoutes = routes
547
- .filter((r) => r.routeType === 'api' || r.routeType === 'stream')
548
- .sort(sortByPath);
549
- const websocketRoutes = routes.filter((r) => r.routeType === 'websocket').sort(sortByPath);
550
- const sseRoutes = routes.filter((r) => r.routeType === 'sse').sort(sortByPath);
551
- const allRoutes = [...apiRoutes, ...websocketRoutes, ...sseRoutes];
552
- // Create maps for agent metadata lookup
553
- const agentMetadataMap = new Map();
554
- const agentNameMap = new Map();
555
- // Map by agent name for easy lookup
556
- agents.forEach((agent) => {
557
- agentNameMap.set(agent.name, agent);
558
- });
559
- // Map agent import variables to metadata by extracting agent name from import path
560
- allRoutes.forEach((route) => {
561
- if (route.agentVariable && route.agentImportPath) {
562
- // Extract agent name from import path (e.g., "@agent/hello" -> "hello")
563
- const match = route.agentImportPath.match(/@agent[s]?\/([^/]+)/);
564
- if (match) {
565
- const agentName = match[1];
566
- if (agentName) {
567
- const metadata = agentNameMap.get(agentName);
568
- if (metadata) {
569
- agentMetadataMap.set(route.agentVariable, metadata);
570
- }
571
- }
572
- }
573
- }
574
- });
575
- if (apiRoutes.length === 0 && websocketRoutes.length === 0 && sseRoutes.length === 0) {
576
- // Clean up stale routes.ts from previous builds (issue #924)
577
- // When all API routes are removed, the old file would reference deleted modules
578
- const generatedDir = join(srcDir, 'generated');
579
- const registryPath = join(generatedDir, 'routes.ts');
580
- if (existsSync(registryPath)) {
581
- unlinkSync(registryPath);
582
- }
583
- return;
584
- }
585
- // Generate imports for agents and schemas
586
- const imports = [];
587
- const agentImports = new Map();
588
- const routeFileImports = new Map();
589
- // Track per-route which import path and schema name to use for alias lookup
590
- const routeSchemaImportInfo = new Map();
591
- // Collect agent and schema imports from routes with validators or exported schemas
592
- allRoutes.forEach((route) => {
593
- const hasSchemaVars = !!route.inputSchemaVariable || !!route.outputSchemaVariable;
594
- if (!route.hasValidator && !hasSchemaVars && !route.agentVariable)
595
- return;
596
- // Collect agent imports (when using agent.validator())
597
- if (route.hasValidator &&
598
- route.agentVariable &&
599
- route.agentImportPath &&
600
- !agentImports.has(route.agentVariable)) {
601
- let resolvedPath = route.agentImportPath;
602
- if (resolvedPath.startsWith('@agents/') || resolvedPath.startsWith('@agent/')) {
603
- // Handle both @agents/ and @agent/ aliases -> ../agent/
604
- const suffix = resolvedPath.startsWith('@agents/')
605
- ? resolvedPath.substring('@agents/'.length)
606
- : resolvedPath.substring('@agent/'.length);
607
- // Convert @agent/hello -> ../agent/hello/index.js
608
- // Convert @agent/hello/agent -> ../agent/hello/agent.js
609
- if (!suffix.includes('/')) {
610
- // Bare module (e.g., @agent/hello) - add /index.js
611
- resolvedPath = `../agent/${suffix}/index.js`;
612
- }
613
- else {
614
- // File path (e.g., @agent/hello/agent) - add .js
615
- const finalPath = suffix.endsWith('.js')
616
- ? suffix
617
- : suffix.replace(/\.tsx?$/, '') + '.js';
618
- resolvedPath = `../agent/${finalPath}`;
619
- }
620
- }
621
- else if (resolvedPath.startsWith('@api/')) {
622
- // src/generated/ -> src/api/ is ../api/
623
- const suffix = resolvedPath.substring('@api/'.length);
624
- const finalPath = suffix.endsWith('.js')
625
- ? suffix
626
- : suffix.replace(/\.tsx?$/, '') + '.js';
627
- resolvedPath = `../api/${finalPath}`;
628
- }
629
- else if (resolvedPath.startsWith('./') || resolvedPath.startsWith('../')) {
630
- // Resolve relative import from route file's directory
631
- // Use schemaSourceFile when available (sub-router routes)
632
- const normalizedFilename = toForwardSlash(route.schemaSourceFile ?? route.filename);
633
- const routeDir = normalizedFilename.substring(0, normalizedFilename.lastIndexOf('/'));
634
- // Join and normalize the path
635
- const joined = `${routeDir}/${resolvedPath}`;
636
- // Normalize by resolving .. and . segments
637
- const normalized = joined
638
- .split('/')
639
- .reduce((acc, segment) => {
640
- if (segment === '..') {
641
- acc.pop();
642
- }
643
- else if (segment !== '.' && segment !== '') {
644
- acc.push(segment);
645
- }
646
- return acc;
647
- }, [])
648
- .join('/');
649
- // Remove 'src/' prefix if present (routes are in src/, generated is in src/generated/)
650
- const withoutSrc = normalized.startsWith('src/') ? normalized.substring(4) : normalized;
651
- // Make it relative from src/generated/
652
- resolvedPath = `../${withoutSrc}`;
653
- // Check if this is a directory import (no file extension) vs a file import
654
- // Directory imports like '../agent/translate' should resolve to '../agent/translate/index.js'
655
- // File imports like '../agent/translate/agent' should resolve to '../agent/translate/agent.js'
656
- const hasExtension = /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(resolvedPath);
657
- if (!hasExtension) {
658
- // No extension - check if it's a directory or a file
659
- // Try to resolve the actual path on disk to determine
660
- const absolutePath = join(srcDir, withoutSrc);
661
- const isDirectory = existsSync(absolutePath) ||
662
- existsSync(join(absolutePath, 'index.ts')) ||
663
- existsSync(join(absolutePath, 'index.tsx'));
664
- const isFile = existsSync(`${absolutePath}.ts`) || existsSync(`${absolutePath}.tsx`);
665
- if (isDirectory && !isFile) {
666
- // It's a directory import, add /index.js
667
- resolvedPath = `${resolvedPath}/index.js`;
668
- }
669
- else {
670
- // It's a file import (or we can't determine), add .js
671
- resolvedPath = `${resolvedPath}.js`;
672
- }
673
- }
674
- else {
675
- // Has extension - replace with .js
676
- resolvedPath = resolvedPath.replace(/\.tsx?$/, '.js');
677
- }
678
- }
679
- const uniqueImportName = route.agentVariable;
680
- imports.push(`import type ${uniqueImportName} from '${resolvedPath}';`);
681
- agentImports.set(route.agentVariable, uniqueImportName);
682
- }
683
- // Collect schema variable imports
684
- // If the schema is imported from another file, use that file's path (rebased)
685
- // Otherwise fall back to the route file path (for locally defined schemas)
686
- if (route.inputSchemaVariable) {
687
- let importPath;
688
- let schemaNameToImport;
689
- if (route.inputSchemaImportPath) {
690
- // Schema is imported - rebase the import path from route file to generated file
691
- // Use schemaSourceFile if available (sub-router routes carry the original file)
692
- const sourceFile = route.schemaSourceFile ?? route.filename;
693
- importPath = rebaseImportPath(sourceFile, route.inputSchemaImportPath, srcDir);
694
- // Use the actual exported name (handles aliased imports like `import { A as B }`)
695
- schemaNameToImport =
696
- route.inputSchemaImportedName === 'default'
697
- ? route.inputSchemaVariable
698
- : (route.inputSchemaImportedName ?? route.inputSchemaVariable);
699
- }
700
- else {
701
- // Schema is locally defined - import from the file that defines/exports it
702
- // When route is mounted via .route(), schemaSourceFile points to the sub-router file
703
- const sourceFile = route.schemaSourceFile ?? route.filename;
704
- const filename = toForwardSlash(sourceFile);
705
- const withoutSrc = filename.startsWith('src/') ? filename.substring(4) : filename;
706
- const withoutLeadingDot = withoutSrc.startsWith('./')
707
- ? withoutSrc.substring(2)
708
- : withoutSrc;
709
- importPath = `../${withoutLeadingDot.replace(/\.ts$/, '')}`;
710
- schemaNameToImport = route.inputSchemaVariable;
711
- }
712
- if (!routeFileImports.has(importPath)) {
713
- routeFileImports.set(importPath, new Set());
714
- }
715
- routeFileImports.get(importPath).add(schemaNameToImport);
716
- // Store the resolved import info for later alias lookup
717
- routeSchemaImportInfo.set(`input:${route.path}:${route.method}`, {
718
- importPath,
719
- schemaName: schemaNameToImport,
720
- });
721
- }
722
- if (route.outputSchemaVariable) {
723
- let importPath;
724
- let schemaNameToImport;
725
- if (route.outputSchemaImportPath) {
726
- // Schema is imported - rebase the import path from route file to generated file
727
- const sourceFile = route.schemaSourceFile ?? route.filename;
728
- importPath = rebaseImportPath(sourceFile, route.outputSchemaImportPath, srcDir);
729
- // Use the actual exported name (handles aliased imports like `import { A as B }`)
730
- schemaNameToImport =
731
- route.outputSchemaImportedName === 'default'
732
- ? route.outputSchemaVariable
733
- : (route.outputSchemaImportedName ?? route.outputSchemaVariable);
734
- }
735
- else {
736
- // Schema is locally defined - import from the file that defines/exports it
737
- const sourceFile = route.schemaSourceFile ?? route.filename;
738
- const filename = toForwardSlash(sourceFile);
739
- const withoutSrc = filename.startsWith('src/') ? filename.substring(4) : filename;
740
- const withoutLeadingDot = withoutSrc.startsWith('./')
741
- ? withoutSrc.substring(2)
742
- : withoutSrc;
743
- importPath = `../${withoutLeadingDot.replace(/\.ts$/, '')}`;
744
- schemaNameToImport = route.outputSchemaVariable;
745
- }
746
- if (!routeFileImports.has(importPath)) {
747
- routeFileImports.set(importPath, new Set());
748
- }
749
- routeFileImports.get(importPath).add(schemaNameToImport);
750
- // Store the resolved import info for later alias lookup
751
- routeSchemaImportInfo.set(`output:${route.path}:${route.method}`, {
752
- importPath,
753
- schemaName: schemaNameToImport,
754
- });
755
- }
756
- });
757
- // Generate schema imports, only aliasing when names collide across files
758
- const schemaImportAliases = new Map(); // importPath -> (schemaName -> alias)
759
- // First pass: count how many times each schema name appears across all import paths
760
- const globalNameCount = new Map();
761
- routeFileImports.forEach((schemas) => {
762
- for (const schemaName of schemas) {
763
- globalNameCount.set(schemaName, (globalNameCount.get(schemaName) ?? 0) + 1);
764
- }
765
- });
766
- // Track aliases assigned to duplicated names for uniqueness
767
- const duplicateCounters = new Map();
768
- routeFileImports.forEach((schemas, importPath) => {
769
- const aliases = new Map();
770
- const importParts = [];
771
- for (const schemaName of Array.from(schemas)) {
772
- if ((globalNameCount.get(schemaName) ?? 0) > 1) {
773
- // Name appears in multiple import paths — alias to avoid collision
774
- const counter = duplicateCounters.get(schemaName) ?? 0;
775
- duplicateCounters.set(schemaName, counter + 1);
776
- const alias = `${schemaName}_${counter}`;
777
- aliases.set(schemaName, alias);
778
- importParts.push(`${schemaName} as ${alias}`);
779
- }
780
- else {
781
- // Unique name — import directly, no alias needed
782
- aliases.set(schemaName, schemaName);
783
- importParts.push(schemaName);
784
- }
785
- }
786
- schemaImportAliases.set(importPath, aliases);
787
- imports.push(`import type { ${importParts.join(', ')} } from '${importPath}';`);
788
- });
789
- const importsStr = imports.length > 0 ? imports.join('\n') + '\n' : '';
790
- // Add InferInput/InferOutput imports if we have any routes with schemas
791
- const hasSchemas = allRoutes.some((r) => r.hasValidator || r.inputSchemaVariable || r.outputSchemaVariable || r.agentVariable);
792
- const typeImports = hasSchemas
793
- ? `import type { InferInput, InferOutput } from '@agentuity/core';\n`
794
- : '';
795
- // Generate individual route schema types
796
- const routeSchemaTypes = allRoutes
797
- .filter((r) => r.hasValidator || r.inputSchemaVariable || r.outputSchemaVariable || r.agentVariable)
798
- .map((route) => {
799
- const routeKey = route.method ? `${route.method.toUpperCase()} ${route.path}` : route.path;
800
- const safeName = routeKey
801
- .replace(/[^a-zA-Z0-9]/g, '_')
802
- .replace(/^_+|_+$/g, '')
803
- .replace(/_+/g, '_');
804
- const pascalName = toPascalCase(safeName);
805
- let inputType = 'never';
806
- let outputType = 'never';
807
- let inputSchemaType = 'never';
808
- let outputSchemaType = 'never';
809
- let agentMeta;
810
- // Look up agent metadata if available
811
- if (route.agentVariable) {
812
- agentMeta = agentMetadataMap.get(route.agentVariable);
813
- }
814
- // Only generate agent-based types if the import was successfully added
815
- // (import is only added when hasValidator && agentVariable && agentImportPath are all present)
816
- const importName = route.agentVariable ? agentImports.get(route.agentVariable) : undefined;
817
- if (importName) {
818
- inputType = `InferInput<typeof ${importName}['inputSchema']>`;
819
- outputType = `InferOutput<typeof ${importName}['outputSchema']>`;
820
- inputSchemaType = `typeof ${importName} extends { inputSchema?: infer I } ? I : never`;
821
- outputSchemaType = `typeof ${importName} extends { outputSchema?: infer O } ? O : never`;
822
- }
823
- else if (route.inputSchemaVariable || route.outputSchemaVariable) {
824
- // Get the aliased schema names using the stored import info
825
- // (which correctly handles schemas imported from shared files)
826
- const inputInfo = routeSchemaImportInfo.get(`input:${route.path}:${route.method}`);
827
- const outputInfo = routeSchemaImportInfo.get(`output:${route.path}:${route.method}`);
828
- let inputAlias;
829
- let outputAlias;
830
- if (inputInfo) {
831
- const aliases = schemaImportAliases.get(inputInfo.importPath);
832
- inputAlias = aliases?.get(inputInfo.schemaName);
833
- }
834
- if (outputInfo) {
835
- const aliases = schemaImportAliases.get(outputInfo.importPath);
836
- outputAlias = aliases?.get(outputInfo.schemaName);
837
- }
838
- inputType = inputAlias ? `InferInput<typeof ${inputAlias}>` : 'never';
839
- outputType = outputAlias ? `InferOutput<typeof ${outputAlias}>` : 'never';
840
- inputSchemaType = inputAlias ? `typeof ${inputAlias}` : 'never';
841
- outputSchemaType = outputAlias ? `typeof ${outputAlias}` : 'never';
842
- }
843
- if (inputType === 'never' && outputType === 'never') {
844
- return ''; // Skip routes without schemas
845
- }
846
- // Build JSDoc with agent description and schema details
847
- const inputJSDoc = ['/**', ` * Input type for route: ${routeKey}`];
848
- if (agentMeta?.description) {
849
- inputJSDoc.push(` * @description ${agentMeta.description}`);
850
- }
851
- if (agentMeta?.inputSchemaCode) {
852
- inputJSDoc.push(` * @schema ${agentMeta.inputSchemaCode}`);
853
- }
854
- inputJSDoc.push(' */');
855
- const outputJSDoc = ['/**', ` * Output type for route: ${routeKey}`];
856
- if (agentMeta?.description) {
857
- outputJSDoc.push(` * @description ${agentMeta.description}`);
858
- }
859
- if (agentMeta?.outputSchemaCode) {
860
- outputJSDoc.push(` * @schema ${agentMeta.outputSchemaCode}`);
861
- }
862
- outputJSDoc.push(' */');
863
- const parts = [
864
- '',
865
- ...inputJSDoc,
866
- `export type ${pascalName}Input = ${inputType};`,
867
- '',
868
- ...outputJSDoc,
869
- `export type ${pascalName}Output = ${outputType};`,
870
- '',
871
- `/**`,
872
- ` * Input schema type for route: ${routeKey}`,
873
- ` */`,
874
- `export type ${pascalName}InputSchema = ${inputSchemaType};`,
875
- '',
876
- `/**`,
877
- ` * Output schema type for route: ${routeKey}`,
878
- ` */`,
879
- `export type ${pascalName}OutputSchema = ${outputSchemaType};`,
880
- ];
881
- return parts.join('\n');
882
- })
883
- .filter(Boolean)
884
- .join('\n');
885
- // Helper to generate route entry - uses exported schema types
886
- const generateRouteEntry = (route, pathIncludesMethod = false) => {
887
- const routeKey = route.path;
888
- // For WebSocket/SSE routes, we need to include the method in the type name
889
- // to match the generated types (which use "POST /api/websocket/echo" as the routeKey)
890
- // For API routes, the method is already in the path from the caller
891
- const typeRouteKey = pathIncludesMethod
892
- ? route.path
893
- : `${route.method?.toUpperCase()} ${route.path}`;
894
- const safeName = typeRouteKey
895
- .replace(/[^a-zA-Z0-9]/g, '_')
896
- .replace(/^_+|_+$/g, '')
897
- .replace(/_+/g, '_');
898
- const pascalName = toPascalCase(safeName);
899
- // Use the exported schema types we generated above
900
- // Note: agentImports.get() may return undefined if import wasn't added
901
- const importName = route.agentVariable ? agentImports.get(route.agentVariable) : null;
902
- // Use 'never' types if no schemas were actually extracted
903
- // Note: hasValidator may be true (e.g., zValidator('query', ...)) but no schemas extracted
904
- // because only 'json' validators extract input schemas
905
- // Also check if agentVariable exists but import wasn't added (missing agentImportPath)
906
- const hasValidAgentImport = route.agentVariable ? !!importName : false;
907
- // Generate pathParams type
908
- const pathParamsType = generatePathParamsType(route.pathParams);
909
- if (!route.inputSchemaVariable && !route.outputSchemaVariable && !hasValidAgentImport) {
910
- const streamValue = route.stream === true ? 'true' : 'false';
911
- return `\t'${routeKey}': {
912
- \t\tinputSchema: never;
913
- \t\toutputSchema: never;
914
- \t\tstream: ${streamValue};
915
- \t\tparams: ${pathParamsType};
916
- \t};`;
917
- }
918
- const streamValue = importName
919
- ? `typeof ${importName} extends { stream?: infer S } ? S : false`
920
- : route.stream === true
921
- ? 'true'
922
- : 'false';
923
- return `\t'${routeKey}': {
924
- \t\tinputSchema: ${pascalName}InputSchema;
925
- \t\toutputSchema: ${pascalName}OutputSchema;
926
- \t\tstream: ${streamValue};
927
- \t\tparams: ${pathParamsType};
928
- \t};`;
929
- };
930
- // Generate route entries with METHOD prefix for API routes
931
- const apiRouteEntries = apiRoutes
932
- .map((route) => {
933
- const routeKey = `${route.method.toUpperCase()} ${route.path}`;
934
- return generateRouteEntry({ ...route, path: routeKey }, true);
935
- })
936
- .join('\n');
937
- const websocketRouteEntries = websocketRoutes
938
- .map((r) => generateRouteEntry(r, false))
939
- .join('\n');
940
- const sseRouteEntries = sseRoutes.map((r) => generateRouteEntry(r, false)).join('\n');
941
- // Generate RPC-style nested registry type
942
- const rpcRegistryType = generateRPCRegistryType(apiRoutes, websocketRoutes, sseRoutes, agentImports, schemaImportAliases, agentMetadataMap);
943
- const rpcRuntimeMetadata = generateRPCRuntimeMetadata(apiRoutes, websocketRoutes, sseRoutes);
944
- const generatedContent = `// @generated
945
- // Auto-generated by Agentuity - DO NOT EDIT
946
- ${importsStr}${typeImports}${shouldEmitFrontendClient
947
- ? `
948
- import { createClient } from '@agentuity/frontend';`
949
- : ''}
950
- // ============================================================================
951
- // Route Schema Type Exports
952
- // ============================================================================
953
- ${routeSchemaTypes}
954
-
955
- // ============================================================================
956
- // Route Definitions
957
- // ============================================================================
958
-
959
- /**
960
- * Route Definitions
961
- *
962
- * Type-safe route registry for all API routes, WebSocket connections, and SSE endpoints.
963
- * Used by @agentuity/react and @agentuity/frontend for client-side type-safe routing.
964
- *
965
- * @remarks
966
- * This module augmentation is auto-generated from your route files during build.
967
- * Individual route Input/Output types are exported above for direct usage.
968
- *
969
- * The augmentation targets @agentuity/frontend (the canonical source of registry types).
970
- * Since @agentuity/react re-exports these types, the augmentation is visible when
971
- * importing from either package.
972
- */
973
- ${shouldEmitFrontendClient
974
- ? `
975
- /**
976
- * RPC Route Registry
977
- *
978
- * Nested structure for RPC-style client access (e.g., client.hello.post())
979
- * Used by createClient() from @agentuity/frontend for type-safe RPC calls.
980
- */
981
- export interface RPCRouteRegistry {
982
- ${rpcRegistryType}
983
- }
984
- `
985
- : ''}
986
- declare module '@agentuity/frontend' {
987
- \t/**
988
- \t * API Route Registry
989
- \t *
990
- \t * Maps route keys (METHOD /path) to their input/output schemas
991
- \t */
992
- \texport interface RouteRegistry {
993
- ${apiRouteEntries}
994
- \t}
995
- \t
996
- \t/**
997
- \t * WebSocket Route Registry
998
- \t *
999
- \t * Maps WebSocket route paths to their schemas
1000
- \t */
1001
- \texport interface WebSocketRouteRegistry {
1002
- ${websocketRouteEntries}
1003
- \t}
1004
- \t
1005
- \t/**
1006
- \t * Server-Sent Events Route Registry
1007
- \t *
1008
- \t * Maps SSE route paths to their schemas
1009
- \t */
1010
- \texport interface SSERouteRegistry {
1011
- ${sseRouteEntries}
1012
- \t}
1013
-
1014
- \t/**
1015
- \t * RPC Route Registry
1016
- \t *
1017
- \t * Nested structure for RPC-style client access (e.g., client.hello.post())
1018
- \t * Used by createClient() from @agentuity/frontend for type-safe RPC calls.
1019
- \t */
1020
- \texport interface RPCRouteRegistry {
1021
- ${rpcRegistryType}
1022
- \t}
1023
- }
1024
- ${hasReactDependency
1025
- ? `
1026
- // Backward compatibility: also augment @agentuity/react for older versions
1027
- // that define RouteRegistry locally instead of re-exporting from @agentuity/frontend
1028
- declare module '@agentuity/react' {
1029
- \texport interface RouteRegistry {
1030
- ${apiRouteEntries}
1031
- \t}
1032
- \texport interface WebSocketRouteRegistry {
1033
- ${websocketRouteEntries}
1034
- \t}
1035
- \texport interface SSERouteRegistry {
1036
- ${sseRouteEntries}
1037
- \t}
1038
- \texport interface RPCRouteRegistry {
1039
- ${rpcRegistryType}
1040
- \t}
1041
- }
1042
- `
1043
- : ''}
1044
- /**
1045
- * Runtime metadata for RPC routes.
1046
- * Contains route type information for client routing decisions.
1047
- * @internal
1048
- */
1049
- const _rpcRouteMetadata = ${rpcRuntimeMetadata} as const;
1050
-
1051
- // Store metadata globally for createAPIClient() to access
1052
- if (typeof globalThis !== 'undefined') {
1053
- (globalThis as Record<string, unknown>).__rpcRouteMetadata = _rpcRouteMetadata;
1054
- }
1055
- ${shouldEmitFrontendClient
1056
- ? `
1057
- /**
1058
- * Create a type-safe API client with optional configuration.
1059
- *
1060
- * This function is only generated when @agentuity/frontend is installed
1061
- * but @agentuity/react is not. For React apps, import createAPIClient
1062
- * from '@agentuity/react' instead.
1063
- *
1064
- * @example
1065
- * \`\`\`typescript
1066
- * import { createAPIClient } from './generated/routes';
1067
- *
1068
- * // Basic usage
1069
- * const api = createAPIClient();
1070
- * const result = await api.hello.post({ name: 'World' });
1071
- *
1072
- * // With custom headers
1073
- * const api = createAPIClient({ headers: { 'X-Custom-Header': 'value' } });
1074
- * await api.hello.post({ name: 'World' });
1075
- * \`\`\`
1076
- */
1077
- export function createAPIClient(options?: Parameters<typeof createClient>[0]): import('@agentuity/frontend').Client<RPCRouteRegistry> {
1078
- return createClient(options || {}, _rpcRouteMetadata) as import('@agentuity/frontend').Client<RPCRouteRegistry>;
1079
- }
1080
- `
1081
- : hasReactDependency
1082
- ? `
1083
- /**
1084
- * Type-safe API client is available from @agentuity/react
1085
- *
1086
- * @example
1087
- * \`\`\`typescript
1088
- * import { createAPIClient } from '@agentuity/react';
1089
- *
1090
- * const api = createAPIClient();
1091
- * const result = await api.hello.post({ name: 'World' });
1092
- * \`\`\`
1093
- */
1094
- `
1095
- : ''}
1096
-
1097
- // FOUND AN ERROR IN THIS FILE?
1098
- // Please file an issue at https://github.com/agentuity/sdk/issues
1099
- // or if you know the fix please submit a PR!
1100
- `;
1101
- const generatedDir = join(srcDir, 'generated');
1102
- const registryPath = join(generatedDir, 'routes.ts');
1103
- mkdirSync(generatedDir, { recursive: true });
1104
- // Collapse 2+ consecutive empty lines into 1 empty line (3+ \n becomes 2 \n)
1105
- const cleanedContent = generatedContent.replace(/\n{3,}/g, '\n\n');
1106
- writeFileSync(registryPath, cleanedContent, 'utf-8');
1107
- }
1108
- //# sourceMappingURL=registry-generator.js.map