@agentuity/cli 0.0.72 → 0.0.74

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 (126) hide show
  1. package/bin/cli.ts +19 -5
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +13 -9
  4. package/dist/auth.js.map +1 -1
  5. package/dist/banner.js +1 -1
  6. package/dist/banner.js.map +1 -1
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +79 -21
  9. package/dist/cli.js.map +1 -1
  10. package/dist/cmd/ai/prompt/api.d.ts.map +1 -1
  11. package/dist/cmd/ai/prompt/api.js +5 -4
  12. package/dist/cmd/ai/prompt/api.js.map +1 -1
  13. package/dist/cmd/auth/api.d.ts +2 -2
  14. package/dist/cmd/auth/api.d.ts.map +1 -1
  15. package/dist/cmd/auth/api.js +15 -14
  16. package/dist/cmd/auth/api.js.map +1 -1
  17. package/dist/cmd/auth/login.d.ts.map +1 -1
  18. package/dist/cmd/auth/login.js +37 -16
  19. package/dist/cmd/auth/login.js.map +1 -1
  20. package/dist/cmd/auth/ssh/api.d.ts.map +1 -1
  21. package/dist/cmd/auth/ssh/api.js +3 -2
  22. package/dist/cmd/auth/ssh/api.js.map +1 -1
  23. package/dist/cmd/build/ast.d.ts.map +1 -1
  24. package/dist/cmd/build/ast.js +76 -14
  25. package/dist/cmd/build/ast.js.map +1 -1
  26. package/dist/cmd/build/bundler.d.ts +3 -1
  27. package/dist/cmd/build/bundler.d.ts.map +1 -1
  28. package/dist/cmd/build/bundler.js +21 -9
  29. package/dist/cmd/build/bundler.js.map +1 -1
  30. package/dist/cmd/build/format-schema.d.ts +6 -0
  31. package/dist/cmd/build/format-schema.d.ts.map +1 -0
  32. package/dist/cmd/build/format-schema.js +60 -0
  33. package/dist/cmd/build/format-schema.js.map +1 -0
  34. package/dist/cmd/build/index.d.ts.map +1 -1
  35. package/dist/cmd/build/index.js +13 -0
  36. package/dist/cmd/build/index.js.map +1 -1
  37. package/dist/cmd/build/plugin.d.ts.map +1 -1
  38. package/dist/cmd/build/plugin.js +123 -32
  39. package/dist/cmd/build/plugin.js.map +1 -1
  40. package/dist/cmd/build/route-discovery.d.ts +50 -0
  41. package/dist/cmd/build/route-discovery.d.ts.map +1 -0
  42. package/dist/cmd/build/route-discovery.js +143 -0
  43. package/dist/cmd/build/route-discovery.js.map +1 -0
  44. package/dist/cmd/build/route-registry.d.ts.map +1 -1
  45. package/dist/cmd/build/route-registry.js +25 -10
  46. package/dist/cmd/build/route-registry.js.map +1 -1
  47. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  48. package/dist/cmd/cloud/deploy.js +8 -6
  49. package/dist/cmd/cloud/deploy.js.map +1 -1
  50. package/dist/cmd/cloud/deployment/show.d.ts.map +1 -1
  51. package/dist/cmd/cloud/deployment/show.js +34 -10
  52. package/dist/cmd/cloud/deployment/show.js.map +1 -1
  53. package/dist/cmd/dev/agents.d.ts.map +1 -1
  54. package/dist/cmd/dev/agents.js +2 -2
  55. package/dist/cmd/dev/agents.js.map +1 -1
  56. package/dist/cmd/dev/index.d.ts.map +1 -1
  57. package/dist/cmd/dev/index.js +21 -0
  58. package/dist/cmd/dev/index.js.map +1 -1
  59. package/dist/cmd/dev/sync.d.ts.map +1 -1
  60. package/dist/cmd/dev/sync.js +2 -2
  61. package/dist/cmd/dev/sync.js.map +1 -1
  62. package/dist/cmd/project/download.d.ts.map +1 -1
  63. package/dist/cmd/project/download.js +16 -2
  64. package/dist/cmd/project/download.js.map +1 -1
  65. package/dist/cmd/project/list.d.ts.map +1 -1
  66. package/dist/cmd/project/list.js +2 -10
  67. package/dist/cmd/project/list.js.map +1 -1
  68. package/dist/cmd/project/show.d.ts.map +1 -1
  69. package/dist/cmd/project/show.js +8 -7
  70. package/dist/cmd/project/show.js.map +1 -1
  71. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  72. package/dist/cmd/project/template-flow.js +14 -2
  73. package/dist/cmd/project/template-flow.js.map +1 -1
  74. package/dist/config.d.ts.map +1 -1
  75. package/dist/config.js +9 -0
  76. package/dist/config.js.map +1 -1
  77. package/dist/index.d.ts +2 -2
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +1 -1
  80. package/dist/index.js.map +1 -1
  81. package/dist/steps.d.ts +20 -30
  82. package/dist/steps.d.ts.map +1 -1
  83. package/dist/steps.js +339 -184
  84. package/dist/steps.js.map +1 -1
  85. package/dist/tui/box.d.ts.map +1 -1
  86. package/dist/tui/box.js +8 -4
  87. package/dist/tui/box.js.map +1 -1
  88. package/dist/tui/prompt.d.ts.map +1 -1
  89. package/dist/tui/prompt.js +7 -2
  90. package/dist/tui/prompt.js.map +1 -1
  91. package/dist/tui.d.ts +20 -1
  92. package/dist/tui.d.ts.map +1 -1
  93. package/dist/tui.js +90 -18
  94. package/dist/tui.js.map +1 -1
  95. package/dist/types.d.ts +10 -0
  96. package/dist/types.d.ts.map +1 -1
  97. package/package.json +3 -3
  98. package/src/auth.ts +13 -10
  99. package/src/banner.ts +1 -1
  100. package/src/cli.ts +89 -27
  101. package/src/cmd/ai/prompt/api.ts +5 -4
  102. package/src/cmd/auth/api.ts +20 -22
  103. package/src/cmd/auth/login.ts +36 -17
  104. package/src/cmd/auth/ssh/api.ts +5 -9
  105. package/src/cmd/build/ast.ts +88 -14
  106. package/src/cmd/build/bundler.ts +32 -11
  107. package/src/cmd/build/format-schema.ts +66 -0
  108. package/src/cmd/build/index.ts +14 -0
  109. package/src/cmd/build/plugin.ts +146 -36
  110. package/src/cmd/build/route-discovery.ts +197 -0
  111. package/src/cmd/build/route-registry.ts +26 -10
  112. package/src/cmd/cloud/deploy.ts +19 -6
  113. package/src/cmd/cloud/deployment/show.ts +42 -10
  114. package/src/cmd/dev/agents.ts +2 -10
  115. package/src/cmd/dev/index.ts +25 -0
  116. package/src/cmd/dev/sync.ts +2 -12
  117. package/src/cmd/project/download.ts +16 -2
  118. package/src/cmd/project/list.ts +2 -9
  119. package/src/cmd/project/show.ts +8 -6
  120. package/src/cmd/project/template-flow.ts +21 -2
  121. package/src/config.ts +10 -0
  122. package/src/index.ts +2 -2
  123. package/src/steps.ts +397 -229
  124. package/src/tui/box.ts +8 -4
  125. package/src/tui/prompt.ts +7 -4
  126. package/src/tui.ts +125 -20
@@ -16,6 +16,7 @@ import { createLogger } from '@agentuity/server';
16
16
  import type { LogLevel } from '../../types';
17
17
  import { toCamelCase, toPascalCase } from '../../utils/string';
18
18
  import { generateRouteRegistry, type RouteInfo } from './route-registry';
19
+ import { discoverRouteFiles } from './route-discovery';
19
20
 
20
21
  /**
21
22
  * Setup lifecycle types by analyzing app.ts for setup() function
@@ -371,10 +372,30 @@ import { readFileSync, existsSync } from 'node:fs';
371
372
  }
372
373
  }
373
374
  const webstatic = serveStatic({ root: import.meta.dir + '/web' });
375
+ // In dev mode, serve from source; in prod, serve from build output
376
+ const publicRoot = ${isDevMode} ? ${JSON.stringify(join(srcDir, 'web', 'public'))} : import.meta.dir + '/web/public';
377
+ const publicstatic = serveStatic({ root: publicRoot, rewriteRequestPath: (path) => path });
374
378
  router.get('/', (c) => c.html(index));
375
379
  router.get('/web/chunk/*', webstatic);
376
380
  router.get('/web/asset/*', webstatic);
377
- router.get('/public/*', webstatic);
381
+ // Serve public assets at root (e.g., /favicon.ico) - must be last
382
+ router.get('/*', async (c, next) => {
383
+ const path = c.req.path;
384
+ // Prevent directory traversal attacks
385
+ if (path.includes('..') || path.includes('%2e%2e')) {
386
+ return c.notFound();
387
+ }
388
+ // Only serve from public folder at root (skip /web/* routes and /)
389
+ if (path !== '/' && !path.startsWith('/web/')) {
390
+ try {
391
+ // serveStatic calls next() internally if file not found
392
+ return await publicstatic(c, next);
393
+ } catch (err) {
394
+ return next();
395
+ }
396
+ }
397
+ return next();
398
+ });
378
399
  })();`);
379
400
  }
380
401
 
@@ -415,7 +436,11 @@ import { readFileSync, existsSync } from 'node:fs';
415
436
 
416
437
  for (const subdir of subdirs) {
417
438
  const fullPath = join(agentBaseDir, subdir);
418
- if (!agentDirs.has(fullPath)) {
439
+ // Check if this directory or any subdirectory contains agents
440
+ const hasAgentInTree = Array.from(agentDirs).some((agentDir) =>
441
+ agentDir.startsWith(fullPath)
442
+ );
443
+ if (!hasAgentInTree) {
419
444
  throw new Error(
420
445
  `Directory ${subdir} in src/agent must contain at least one agent (a file with a createAgent export)`
421
446
  );
@@ -437,46 +462,116 @@ import { readFileSync, existsSync } from 'node:fs';
437
462
  const apiRoutesMetadata: BuildMetadata['routes'] = [];
438
463
  const routeInfoList: RouteInfo[] = [];
439
464
  const apiDir = join(srcDir, 'api');
465
+ // Track subdirectory routes for auto-mounting
466
+ const subRouteInserts: string[] = [];
440
467
  if (existsSync(apiDir)) {
441
- const { readdirSync, statSync } = await import('node:fs');
442
- const apiFiles = readdirSync(apiDir)
443
- .filter((name) => name.endsWith('.ts') && !name.endsWith('.generated.ts'))
444
- .map((name) => join(apiDir, name));
468
+ // Use recursive route discovery to find all route files in subdirectories
469
+ const discoveredRoutes = discoverRouteFiles(apiDir);
470
+
471
+ // Collect all API files: index.ts and discovered subdirectory routes
472
+ const apiFiles: string[] = [];
473
+
474
+ // Check for root index.ts
475
+ const indexFile = join(apiDir, 'index.ts');
476
+ if (existsSync(indexFile)) {
477
+ apiFiles.push(indexFile);
478
+ }
479
+
480
+ // Add all discovered route files
481
+ for (const route of discoveredRoutes) {
482
+ apiFiles.push(route.filepath);
483
+
484
+ // Generate auto-mount code for this subdirectory route
485
+ subRouteInserts.push(`await (async() => {
486
+ const { getRouter } = await import('@agentuity/runtime');
487
+ const router = getRouter()!;
488
+ const ${route.variableName} = (await import('${route.importPath}')).default;
489
+ router.route('${route.mountPath}', ${route.variableName});
490
+ })();`);
491
+ }
445
492
 
446
493
  for (const apiFile of apiFiles) {
447
- if (statSync(apiFile).isFile()) {
448
- try {
449
- const routes = await parseRoute(rootDir, apiFile, projectId, deploymentId);
450
- apiRoutesMetadata.push(...routes);
451
-
452
- // Collect route info for RouteRegistry generation
453
- for (const route of routes) {
454
- routeInfoList.push({
455
- method: route.method.toUpperCase(),
456
- path: route.path,
457
- filename: route.filename,
458
- hasValidator: route.config?.hasValidator === true,
459
- routeType: route.type || 'api',
460
- agentVariable: route.config?.agentVariable as string | undefined,
461
- agentImportPath: route.config?.agentImportPath as string | undefined,
462
- inputSchemaVariable: route.config?.inputSchemaVariable as
463
- | string
464
- | undefined,
465
- outputSchemaVariable: route.config?.outputSchemaVariable as
466
- | string
467
- | undefined,
468
- });
469
- }
470
- } catch (error) {
471
- // Skip files that don't have createRouter (they might be utilities)
494
+ try {
495
+ const routes = await parseRoute(rootDir, apiFile, projectId, deploymentId);
496
+
497
+ // Extract schemas from agents for routes that use validators
498
+ for (const route of routes) {
499
+ // Check if route has custom schema overrides from validator({ input, output })
500
+ const hasCustomInput = route.config?.inputSchemaVariable;
501
+ const hasCustomOutput = route.config?.outputSchemaVariable;
502
+
503
+ // If route uses agent.validator(), get schemas from the agent (unless overridden)
472
504
  if (
473
- error instanceof Error &&
474
- error.message.includes('could not find an proper createRouter')
505
+ route.config?.agentImportPath &&
506
+ (!hasCustomInput || !hasCustomOutput)
475
507
  ) {
476
- logger.trace(`Skipping ${apiFile}: no createRouter found`);
477
- } else {
478
- throw error;
508
+ const agentImportPath = route.config.agentImportPath as string;
509
+ // Match by import path: @agent/zod-test -> src/agent/zod-test/agent.ts
510
+ // Normalize import path by removing leading '@' -> agent/zod-test
511
+ const importPattern = agentImportPath.replace(/^@/, '');
512
+ // Escape regex special characters for safe pattern matching
513
+ const escapedPattern = importPattern.replace(
514
+ /[.*+?^${}()|[\]\\]/g,
515
+ '\\$&'
516
+ );
517
+ // Match as complete path segment to avoid false positives (e.g., "agent/hello" matching "agent/hello-world")
518
+ const segmentPattern = new RegExp(`(^|/)${escapedPattern}(/|$)`);
519
+
520
+ for (const [, agentMd] of agentMetadata) {
521
+ const agentFilename = agentMd.get('filename');
522
+ if (agentFilename && segmentPattern.test(agentFilename)) {
523
+ // Use agent schemas unless overridden
524
+ const inputSchemaCode = hasCustomInput
525
+ ? undefined
526
+ : agentMd.get('inputSchemaCode');
527
+ const outputSchemaCode = hasCustomOutput
528
+ ? undefined
529
+ : agentMd.get('outputSchemaCode');
530
+
531
+ if (inputSchemaCode || outputSchemaCode) {
532
+ route.schema = {
533
+ input: inputSchemaCode,
534
+ output: outputSchemaCode,
535
+ };
536
+ }
537
+ break;
538
+ }
539
+ }
479
540
  }
541
+
542
+ // TODO: Extract inline schema code from custom validator({ input: z.string(), output: ... })
543
+ // For now, custom schema overrides with inline code are not extracted (would require parsing the validator call's object expression)
544
+ }
545
+
546
+ apiRoutesMetadata.push(...routes);
547
+
548
+ // Collect route info for RouteRegistry generation
549
+ for (const route of routes) {
550
+ routeInfoList.push({
551
+ method: route.method.toUpperCase(),
552
+ path: route.path,
553
+ filename: route.filename,
554
+ hasValidator: route.config?.hasValidator === true,
555
+ routeType: route.type || 'api',
556
+ agentVariable: route.config?.agentVariable as string | undefined,
557
+ agentImportPath: route.config?.agentImportPath as string | undefined,
558
+ inputSchemaVariable: route.config?.inputSchemaVariable as
559
+ | string
560
+ | undefined,
561
+ outputSchemaVariable: route.config?.outputSchemaVariable as
562
+ | string
563
+ | undefined,
564
+ });
565
+ }
566
+ } catch (error) {
567
+ // Skip files that don't have createRouter (they might be utilities)
568
+ if (
569
+ error instanceof Error &&
570
+ error.message.includes('could not find an proper createRouter')
571
+ ) {
572
+ logger.trace(`Skipping ${apiFile}: no createRouter found`);
573
+ } else {
574
+ throw error;
480
575
  }
481
576
  }
482
577
  }
@@ -499,6 +594,11 @@ import { readFileSync, existsSync } from 'node:fs';
499
594
  })();`);
500
595
  }
501
596
 
597
+ // Auto-mount subdirectory routes (src/api/foo/route.ts -> /api/foo)
598
+ for (const subRouteInsert of subRouteInserts) {
599
+ inserts.push(subRouteInsert);
600
+ }
601
+
502
602
  // Only create the workbench routes if workbench is actually configured
503
603
  if (workbenchConfig) {
504
604
  inserts.push(`await (async() => {
@@ -594,6 +694,16 @@ await (async() => {
594
694
  projectId,
595
695
  };
596
696
 
697
+ // Extract schema codes if available
698
+ const inputSchemaCode = v.get('inputSchemaCode');
699
+ const outputSchemaCode = v.get('outputSchemaCode');
700
+ if (inputSchemaCode || outputSchemaCode) {
701
+ agentData.schema = {
702
+ input: inputSchemaCode,
703
+ output: outputSchemaCode,
704
+ };
705
+ }
706
+
597
707
  const evalsStr = v.get('evals');
598
708
  if (evalsStr) {
599
709
  logger.trace(
@@ -0,0 +1,197 @@
1
+ import { existsSync, readdirSync, lstatSync } from 'node:fs';
2
+ import { join, relative } from 'node:path';
3
+ import { toCamelCase } from '../../utils/string';
4
+
5
+ /**
6
+ * Information about a discovered route file
7
+ */
8
+ export interface DiscoveredRouteFile {
9
+ /** Full path to the route file */
10
+ filepath: string;
11
+ /** Relative path from apiDir (e.g., 'auth/route.ts', 'v1/users/route.ts') */
12
+ relativePath: string;
13
+ /** Mount path for the route (e.g., '/api/auth', '/api/v1/users') */
14
+ mountPath: string;
15
+ /** Safe variable name for importing (e.g., 'authRoute', 'v1UsersRoute') */
16
+ variableName: string;
17
+ /** Import path relative to build output (e.g., './src/api/auth/route') */
18
+ importPath: string;
19
+ }
20
+
21
+ /**
22
+ * Recursively discover all TypeScript route files in an API directory
23
+ * Supports nested structures like:
24
+ * - src/api/index.ts (root API router)
25
+ * - src/api/auth/route.ts -> mounted at /api/auth
26
+ * - src/api/v1/users/route.ts -> mounted at /api/v1/users
27
+ * - src/api/admin/users/login.ts -> mounted at /api/admin/users (any .ts file works)
28
+ *
29
+ * @param apiDir - Absolute path to the src/api directory
30
+ * @param currentDir - Current directory being scanned (used for recursion)
31
+ * @param results - Accumulated results (used for recursion)
32
+ * @returns Array of discovered route files with mount information
33
+ */
34
+ export function discoverRouteFiles(
35
+ apiDir: string,
36
+ currentDir: string = apiDir,
37
+ results: DiscoveredRouteFile[] = []
38
+ ): DiscoveredRouteFile[] {
39
+ if (!existsSync(currentDir)) {
40
+ return results;
41
+ }
42
+
43
+ const entries = readdirSync(currentDir);
44
+
45
+ for (const entry of entries) {
46
+ const entryPath = join(currentDir, entry);
47
+ const stat = lstatSync(entryPath);
48
+
49
+ // Skip symlinks to prevent infinite recursion
50
+ if (stat.isSymbolicLink()) {
51
+ continue;
52
+ }
53
+
54
+ if (stat.isFile() && entry.endsWith('.ts') && !entry.endsWith('.generated.ts')) {
55
+ // Found a TypeScript file
56
+ const relativePath = relative(apiDir, entryPath);
57
+ const isRootIndex = relativePath === 'index.ts';
58
+
59
+ // Skip root index.ts - it's handled separately
60
+ if (isRootIndex) {
61
+ continue;
62
+ }
63
+
64
+ // For subdirectory files, determine mount path
65
+ // src/api/auth/route.ts -> /api/auth
66
+ // src/api/v1/users/route.ts -> /api/v1/users
67
+ // src/api/admin/login.ts -> /api/admin
68
+ const pathParts = relativePath.split('/');
69
+ pathParts.pop(); // Remove filename
70
+
71
+ // Skip files directly in src/api/ to avoid mount path conflicts with root index.ts
72
+ if (pathParts.length === 0) {
73
+ continue;
74
+ }
75
+
76
+ const mountPath = `/api/${pathParts.join('/')}`;
77
+
78
+ // Generate safe variable name
79
+ // auth/route.ts -> authRoute
80
+ // v1/users/route.ts -> v1UsersRoute
81
+ // admin/login.ts -> adminLoginRoute
82
+ const variableParts = pathParts.map((p, idx) => {
83
+ const camel = toCamelCase(p);
84
+ // Capitalize first letter of all parts except the first
85
+ return idx === 0 ? camel : camel.charAt(0).toUpperCase() + camel.slice(1);
86
+ });
87
+ const baseName = entry.replace('.ts', '');
88
+ if (baseName !== 'route' && baseName !== 'index') {
89
+ const camelBase = toCamelCase(baseName);
90
+ // Always capitalize the base name since it's not the first part
91
+ variableParts.push(camelBase.charAt(0).toUpperCase() + camelBase.slice(1));
92
+ }
93
+ const variableName = variableParts.join('') + 'Route';
94
+
95
+ // Generate import path relative to build output
96
+ // src/api/auth/route.ts -> ./src/api/auth/route
97
+ const importPath = './src/api/' + relativePath.replace('.ts', '');
98
+
99
+ results.push({
100
+ filepath: entryPath,
101
+ relativePath,
102
+ mountPath,
103
+ variableName,
104
+ importPath,
105
+ });
106
+ } else if (stat.isDirectory()) {
107
+ // Recursively scan subdirectories
108
+ discoverRouteFiles(apiDir, entryPath, results);
109
+ }
110
+ }
111
+
112
+ return results;
113
+ }
114
+
115
+ /**
116
+ * Detect potential route path conflicts
117
+ *
118
+ * @param routes - Array of route metadata with method and path
119
+ * @returns Array of conflict descriptions
120
+ */
121
+ export interface RouteConflict {
122
+ type: 'duplicate' | 'ambiguous-param' | 'wildcard-overlap';
123
+ routes: Array<{ method: string; path: string; filename: string }>;
124
+ message: string;
125
+ }
126
+
127
+ export function detectRouteConflicts(
128
+ routes: Array<{ method: string; path: string; filename: string }>
129
+ ): RouteConflict[] {
130
+ const conflicts: RouteConflict[] = [];
131
+
132
+ // Group routes by method+path
133
+ const methodPathMap = new Map<string, Array<{ path: string; filename: string }>>();
134
+
135
+ for (const route of routes) {
136
+ const key = `${route.method.toUpperCase()} ${route.path}`;
137
+ if (!methodPathMap.has(key)) {
138
+ methodPathMap.set(key, []);
139
+ }
140
+ methodPathMap.get(key)!.push({ path: route.path, filename: route.filename });
141
+ }
142
+
143
+ // Check for exact duplicates
144
+ for (const [methodPath, routeList] of methodPathMap.entries()) {
145
+ if (routeList.length > 1) {
146
+ const [method] = methodPath.split(' ', 2);
147
+ conflicts.push({
148
+ type: 'duplicate',
149
+ routes: routeList.map((r) => ({ method, path: r.path, filename: r.filename })),
150
+ message: `Duplicate route: ${methodPath} defined in ${routeList.length} files`,
151
+ });
152
+ }
153
+ }
154
+
155
+ // Check for ambiguous parameter routes (same method, different param names)
156
+ // e.g., GET /users/:id and GET /users/:userId
157
+ const methodGroups = new Map<string, Array<{ path: string; filename: string }>>();
158
+ for (const route of routes) {
159
+ const method = route.method.toUpperCase();
160
+ if (!methodGroups.has(method)) {
161
+ methodGroups.set(method, []);
162
+ }
163
+ methodGroups.get(method)!.push({ path: route.path, filename: route.filename });
164
+ }
165
+
166
+ for (const [method, routeList] of methodGroups.entries()) {
167
+ // Normalize params to check for conflicts
168
+ const normalized = routeList.map((r) => ({
169
+ ...r,
170
+ normalizedPath: r.path.replace(/:[^/]+/g, ':param'),
171
+ }));
172
+
173
+ const pathMap = new Map<string, Array<{ path: string; filename: string }>>();
174
+ for (const route of normalized) {
175
+ if (!pathMap.has(route.normalizedPath)) {
176
+ pathMap.set(route.normalizedPath, []);
177
+ }
178
+ pathMap.get(route.normalizedPath)!.push({ path: route.path, filename: route.filename });
179
+ }
180
+
181
+ for (const [normalizedPath, paths] of pathMap.entries()) {
182
+ if (paths.length > 1 && normalizedPath.includes(':param')) {
183
+ // Check if the actual param names differ
184
+ const uniquePaths = new Set(paths.map((p) => p.path));
185
+ if (uniquePaths.size > 1) {
186
+ conflicts.push({
187
+ type: 'ambiguous-param',
188
+ routes: paths.map(({ path, filename }) => ({ method, path, filename })),
189
+ message: `Ambiguous param routes: ${method} ${Array.from(uniquePaths).join(', ')}`,
190
+ });
191
+ }
192
+ }
193
+ }
194
+ }
195
+
196
+ return conflicts;
197
+ }
@@ -37,13 +37,13 @@ export interface RouteInfo {
37
37
  * @param routes - Array of route information
38
38
  */
39
39
  export function generateRouteRegistry(srcDir: string, routes: RouteInfo[]): void {
40
- // Filter routes by type and validator presence
41
- const apiRoutes = routes.filter((r) => r.hasValidator && r.routeType === 'api');
42
- const websocketRoutes = routes.filter((r) => r.hasValidator && r.routeType === 'websocket');
43
- const sseRoutes = routes.filter((r) => r.hasValidator && r.routeType === 'sse');
40
+ // Filter routes by type (include ALL routes, not just those with validators)
41
+ const apiRoutes = routes.filter((r) => r.routeType === 'api');
42
+ const websocketRoutes = routes.filter((r) => r.routeType === 'websocket');
43
+ const sseRoutes = routes.filter((r) => r.routeType === 'sse');
44
44
 
45
45
  if (apiRoutes.length === 0 && websocketRoutes.length === 0 && sseRoutes.length === 0) {
46
- // No typed routes, skip generation
46
+ // No routes, skip generation
47
47
  return;
48
48
  }
49
49
 
@@ -52,11 +52,15 @@ export function generateRouteRegistry(srcDir: string, routes: RouteInfo[]): void
52
52
  const agentImports = new Map<string, string>(); // Maps agent variable to unique import name
53
53
  const schemaImports = new Set<string>(); // Track which schema variables we've seen
54
54
 
55
- // Combine all typed routes for import collection
56
- const allTypedRoutes = [...apiRoutes, ...websocketRoutes, ...sseRoutes];
55
+ // Combine all routes for import collection
56
+ const allRoutes = [...apiRoutes, ...websocketRoutes, ...sseRoutes];
57
57
 
58
- // First pass: collect all unique agents and schema variables
59
- allTypedRoutes.forEach((route) => {
58
+ // First pass: collect all unique agents and schema variables (only for routes with validators)
59
+ allRoutes.forEach((route) => {
60
+ // Skip routes without validators - they won't need imports
61
+ if (!route.hasValidator) {
62
+ return;
63
+ }
60
64
  // If this route uses an agent, import it directly
61
65
  if (route.agentVariable && route.agentImportPath && !agentImports.has(route.agentVariable)) {
62
66
  // Resolve the import path (could be @agent/hello, ../shared, etc.)
@@ -90,7 +94,11 @@ export function generateRouteRegistry(srcDir: string, routes: RouteInfo[]): void
90
94
 
91
95
  // Import schema variables from route files
92
96
  const routeFileImports = new Map<string, Set<string>>(); // Maps route file to schema variables
93
- allTypedRoutes.forEach((route) => {
97
+ allRoutes.forEach((route) => {
98
+ // Only import schemas for routes with validators
99
+ if (!route.hasValidator) {
100
+ return;
101
+ }
94
102
  if (route.inputSchemaVariable || route.outputSchemaVariable) {
95
103
  const filename = route.filename.replace(/\\/g, '/');
96
104
  const importPath = `../${filename.replace(/\.ts$/, '')}`;
@@ -120,6 +128,14 @@ export function generateRouteRegistry(srcDir: string, routes: RouteInfo[]): void
120
128
  const generateRouteEntry = (route: RouteInfo): string => {
121
129
  const routeKey = route.path; // Use path only for websocket/sse, or METHOD path for API
122
130
 
131
+ // If route doesn't have a validator, use never for schemas
132
+ if (!route.hasValidator) {
133
+ return ` '${routeKey}': {
134
+ inputSchema: never;
135
+ outputSchema: never;
136
+ };`;
137
+ }
138
+
123
139
  // If we have an agent variable, we can infer types from it
124
140
  if (route.agentVariable) {
125
141
  const importName = agentImports.get(route.agentVariable)!;
@@ -6,7 +6,14 @@ import { tmpdir } from 'node:os';
6
6
  import { createSubcommand } from '../../types';
7
7
  import * as tui from '../../tui';
8
8
  import { saveProjectDir, getDefaultConfigDir } from '../../config';
9
- import { runSteps, stepSuccess, stepSkipped, stepError, Step, ProgressCallback } from '../../steps';
9
+ import {
10
+ runSteps,
11
+ stepSuccess,
12
+ stepSkipped,
13
+ stepError,
14
+ type Step,
15
+ type StepContext,
16
+ } from '../../steps';
10
17
  import { bundle } from '../build/bundler';
11
18
  import { loadBuildMetadata, getStreamURL } from '../../config';
12
19
  import {
@@ -179,8 +186,9 @@ export const deploySubcommand = createSubcommand({
179
186
  if (!deployment) {
180
187
  return stepError('deployment was null');
181
188
  }
189
+ let capturedOutput: string[] = [];
182
190
  try {
183
- await bundle({
191
+ const bundleResult = await bundle({
184
192
  rootDir: resolve(projectDir),
185
193
  dev: false,
186
194
  deploymentId: deployment.id,
@@ -195,23 +203,28 @@ export const deploySubcommand = createSubcommand({
195
203
  region: project.region,
196
204
  logger: ctx.logger,
197
205
  });
206
+ capturedOutput = bundleResult.output;
198
207
  build = await loadBuildMetadata(join(projectDir, '.agentuity'));
199
208
  instructions = await projectDeploymentUpdate(
200
209
  apiClient,
201
210
  deployment.id,
202
211
  build
203
212
  );
204
- return stepSuccess();
213
+ return stepSuccess(capturedOutput.length > 0 ? capturedOutput : undefined);
205
214
  } catch (ex) {
206
215
  const _ex = ex as Error;
207
- return stepError(_ex.message ?? 'Error building your project');
216
+ return stepError(
217
+ _ex.message ?? 'Error building your project',
218
+ _ex,
219
+ capturedOutput.length > 0 ? capturedOutput : undefined
220
+ );
208
221
  }
209
222
  },
210
223
  },
211
224
  {
212
- type: 'progress',
213
225
  label: 'Encrypt and Upload Deployment',
214
- run: async (progress: ProgressCallback) => {
226
+ run: async (stepCtx: StepContext) => {
227
+ const progress = stepCtx.progress;
215
228
  if (!deployment) {
216
229
  return stepError('deployment was null');
217
230
  }
@@ -15,6 +15,10 @@ const DeploymentShowResponseSchema = z.object({
15
15
  tags: z.array(z.string()).describe('Deployment tags'),
16
16
  customDomains: z.array(z.string()).optional().describe('Custom domains'),
17
17
  cloudRegion: z.string().optional().describe('Cloud region'),
18
+ resourceDb: z.string().nullable().optional().describe('the database name'),
19
+ resourceStorage: z.string().nullable().optional().describe('the storage name'),
20
+ deploymentLogsURL: z.string().nullable().optional().describe('the url to the deployment logs'),
21
+ buildLogsURL: z.string().nullable().optional().describe('the url to the build logs'),
18
22
  metadata: z
19
23
  .object({
20
24
  git: z
@@ -87,27 +91,51 @@ export const showSubcommand = createSubcommand({
87
91
 
88
92
  // Skip TUI output in JSON mode
89
93
  if (!options.json) {
90
- console.log(tui.bold('ID: ') + deployment.id);
91
- console.log(tui.bold('Project: ') + projectId);
92
- console.log(tui.bold('State: ') + (deployment.state || 'unknown'));
93
- console.log(tui.bold('Active: ') + (deployment.active ? 'Yes' : 'No'));
94
- console.log(tui.bold('Created: ') + new Date(deployment.createdAt).toLocaleString());
94
+ const maxWidth = 18;
95
+ console.log(tui.bold('ID:'.padEnd(maxWidth)) + deployment.id);
96
+ console.log(tui.bold('Project:'.padEnd(maxWidth)) + projectId);
97
+ console.log(tui.bold('State:'.padEnd(maxWidth)) + (deployment.state || 'unknown'));
98
+ console.log(tui.bold('Active:'.padEnd(maxWidth)) + (deployment.active ? 'Yes' : 'No'));
99
+ console.log(
100
+ tui.bold('Created:'.padEnd(maxWidth)) +
101
+ new Date(deployment.createdAt).toLocaleString()
102
+ );
95
103
  if (deployment.updatedAt) {
96
104
  console.log(
97
- tui.bold('Updated: ') + new Date(deployment.updatedAt).toLocaleString()
105
+ tui.bold('Updated:'.padEnd(maxWidth)) +
106
+ new Date(deployment.updatedAt).toLocaleString()
98
107
  );
99
108
  }
100
109
  if (deployment.message) {
101
- console.log(tui.bold('Message: ') + deployment.message);
110
+ console.log(tui.bold('Message:'.padEnd(maxWidth)) + deployment.message);
102
111
  }
103
112
  if (deployment.tags.length > 0) {
104
- console.log(tui.bold('Tags: ') + deployment.tags.join(', '));
113
+ console.log(tui.bold('Tags:'.padEnd(maxWidth)) + deployment.tags.join(', '));
105
114
  }
106
115
  if (deployment.customDomains && deployment.customDomains.length > 0) {
107
- console.log(tui.bold('Domains: ') + deployment.customDomains.join(', '));
116
+ console.log(
117
+ tui.bold('Domains:'.padEnd(maxWidth)) + deployment.customDomains.join(', ')
118
+ );
108
119
  }
109
120
  if (deployment.cloudRegion) {
110
- console.log(tui.bold('Region: ') + deployment.cloudRegion);
121
+ console.log(tui.bold('Region:'.padEnd(maxWidth)) + deployment.cloudRegion);
122
+ }
123
+ if (deployment.resourceDb) {
124
+ console.log(tui.bold('Database:'.padEnd(maxWidth)) + deployment.resourceDb);
125
+ }
126
+ if (deployment.resourceStorage) {
127
+ console.log(tui.bold('Storage:'.padEnd(maxWidth)) + deployment.resourceStorage);
128
+ }
129
+ if (deployment.deploymentLogsURL) {
130
+ console.log(
131
+ tui.bold('Deployment Logs:'.padEnd(maxWidth)) +
132
+ tui.link(deployment.deploymentLogsURL)
133
+ );
134
+ }
135
+ if (deployment.buildLogsURL) {
136
+ console.log(
137
+ tui.bold('Build Logs:'.padEnd(maxWidth)) + tui.link(deployment.buildLogsURL)
138
+ );
111
139
  }
112
140
 
113
141
  // Git metadata
@@ -153,6 +181,10 @@ export const showSubcommand = createSubcommand({
153
181
  customDomains: deployment.customDomains ?? undefined,
154
182
  cloudRegion: deployment.cloudRegion ?? undefined,
155
183
  metadata: deployment.metadata ?? undefined,
184
+ resourceDb: deployment.resourceDb ?? undefined,
185
+ resourceStorage: deployment.resourceStorage ?? undefined,
186
+ deploymentLogsURL: deployment.deploymentLogsURL ?? undefined,
187
+ buildLogsURL: deployment.buildLogsURL ?? undefined,
156
188
  };
157
189
  } catch (ex) {
158
190
  tui.fatal(`Failed to show deployment: ${ex}`);
@@ -63,17 +63,9 @@ export const agentsSubcommand = createSubcommand({
63
63
  const queryParams = deploymentId ? `?deploymentId=${deploymentId}` : '';
64
64
 
65
65
  const response = options.json
66
- ? await apiClient.request(
67
- 'GET',
68
- `/cli/agent/${projectId}${queryParams}`,
69
- AgentsResponseSchema
70
- )
66
+ ? await apiClient.get(`/cli/agent/${projectId}${queryParams}`, AgentsResponseSchema)
71
67
  : await tui.spinner('Fetching agents', async () => {
72
- return apiClient.request(
73
- 'GET',
74
- `/cli/agent/${projectId}${queryParams}`,
75
- AgentsResponseSchema
76
- );
68
+ return apiClient.get(`/cli/agent/${projectId}${queryParams}`, AgentsResponseSchema);
77
69
  });
78
70
 
79
71
  if (!response.success) {