@agentuity/cli 1.0.39 → 1.0.40

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 (37) hide show
  1. package/dist/cmd/build/app-router-detector.d.ts +42 -0
  2. package/dist/cmd/build/app-router-detector.d.ts.map +1 -0
  3. package/dist/cmd/build/app-router-detector.js +253 -0
  4. package/dist/cmd/build/app-router-detector.js.map +1 -0
  5. package/dist/cmd/build/ast.d.ts +11 -1
  6. package/dist/cmd/build/ast.d.ts.map +1 -1
  7. package/dist/cmd/build/ast.js +273 -29
  8. package/dist/cmd/build/ast.js.map +1 -1
  9. package/dist/cmd/build/entry-generator.d.ts.map +1 -1
  10. package/dist/cmd/build/entry-generator.js +23 -16
  11. package/dist/cmd/build/entry-generator.js.map +1 -1
  12. package/dist/cmd/build/vite/registry-generator.d.ts.map +1 -1
  13. package/dist/cmd/build/vite/registry-generator.js +37 -13
  14. package/dist/cmd/build/vite/registry-generator.js.map +1 -1
  15. package/dist/cmd/build/vite/route-discovery.d.ts +17 -3
  16. package/dist/cmd/build/vite/route-discovery.d.ts.map +1 -1
  17. package/dist/cmd/build/vite/route-discovery.js +91 -3
  18. package/dist/cmd/build/vite/route-discovery.js.map +1 -1
  19. package/dist/cmd/build/vite-bundler.d.ts.map +1 -1
  20. package/dist/cmd/build/vite-bundler.js +3 -0
  21. package/dist/cmd/build/vite-bundler.js.map +1 -1
  22. package/dist/cmd/dev/index.d.ts.map +1 -1
  23. package/dist/cmd/dev/index.js +30 -0
  24. package/dist/cmd/dev/index.js.map +1 -1
  25. package/dist/utils/route-migration.d.ts +61 -0
  26. package/dist/utils/route-migration.d.ts.map +1 -0
  27. package/dist/utils/route-migration.js +662 -0
  28. package/dist/utils/route-migration.js.map +1 -0
  29. package/package.json +6 -6
  30. package/src/cmd/build/app-router-detector.ts +350 -0
  31. package/src/cmd/build/ast.ts +339 -36
  32. package/src/cmd/build/entry-generator.ts +23 -16
  33. package/src/cmd/build/vite/registry-generator.ts +38 -13
  34. package/src/cmd/build/vite/route-discovery.ts +151 -3
  35. package/src/cmd/build/vite-bundler.ts +4 -0
  36. package/src/cmd/dev/index.ts +34 -0
  37. package/src/utils/route-migration.ts +793 -0
@@ -1068,9 +1068,96 @@ function resolveImportPath(fromDir, importPath) {
1068
1068
  }
1069
1069
  return null;
1070
1070
  }
1071
- export async function parseRoute(rootDir, filename, projectId, deploymentId, visitedFiles, mountedSubrouters) {
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
+ }
1072
1159
  // Track visited files to prevent infinite recursion
1073
- const visited = visitedFiles ?? new Set();
1160
+ const visited = options.visitedFiles ?? new Set();
1074
1161
  const resolvedFilename = resolve(filename);
1075
1162
  if (visited.has(resolvedFilename)) {
1076
1163
  return []; // Already parsed this file, avoid infinite loop
@@ -1159,6 +1246,9 @@ export async function parseRoute(rootDir, filename, projectId, deploymentId, vis
1159
1246
  message: `could not find default export for ${filename} using ${rootDir}`,
1160
1247
  });
1161
1248
  }
1249
+ // Track the chained init expression (e.g., createRouter().get(...).post(...))
1250
+ // so we can extract routes from it later
1251
+ let chainedInitExpr;
1162
1252
  for (const body of ast.body) {
1163
1253
  if (body.type === 'VariableDeclaration') {
1164
1254
  for (const vardecl of body.declarations) {
@@ -1172,6 +1262,14 @@ export async function parseRoute(rootDir, filename, projectId, deploymentId, vis
1172
1262
  variableName = identifier.name;
1173
1263
  break;
1174
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
+ }
1175
1273
  }
1176
1274
  else if (vardecl.init?.type === 'NewExpression') {
1177
1275
  const newExpr = vardecl.init;
@@ -1193,16 +1291,19 @@ export async function parseRoute(rootDir, filename, projectId, deploymentId, vis
1193
1291
  });
1194
1292
  }
1195
1293
  const rel = toForwardSlash(relative(rootDir, filename));
1196
- // Compute the API mount path using the shared helper
1197
- // This ensures consistency between route type generation (here) and runtime mounting (entry-generator.ts)
1198
- // Examples:
1199
- // src/api/index.ts -> basePath = '/api'
1200
- // src/api/sessions.ts -> basePath = '/api/sessions'
1201
- // src/api/auth/route.ts -> basePath = '/api/auth'
1202
- // src/api/users/profile/route.ts -> basePath = '/api/users/profile'
1203
- const srcDir = join(rootDir, 'src');
1204
- const relativeApiPath = extractRelativeApiPath(filename, srcDir);
1205
- const basePath = computeApiMountPath(relativeApiPath);
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
+ }
1206
1307
  const routes = [];
1207
1308
  try {
1208
1309
  for (const body of ast.body) {
@@ -1268,31 +1369,38 @@ export async function parseRoute(rootDir, filename, projectId, deploymentId, vis
1268
1369
  continue;
1269
1370
  }
1270
1371
  try {
1271
- // Parse sub-router's routes
1272
- const subRoutes = await parseRoute(rootDir, resolvedFile, projectId, deploymentId, visited, mountedSubrouters);
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
+ });
1273
1382
  // Track this file as a mounted sub-router
1274
- if (mountedSubrouters) {
1275
- mountedSubrouters.add(resolve(resolvedFile));
1383
+ if (options.mountedSubrouters) {
1384
+ options.mountedSubrouters.add(resolve(resolvedFile));
1276
1385
  }
1277
- // Compute the sub-router's own basePath so we can strip it
1278
- const subSrcDir = join(rootDir, 'src');
1279
- const subRelativeApiPath = extractRelativeApiPath(resolvedFile, subSrcDir);
1280
- const subBasePath = computeApiMountPath(subRelativeApiPath);
1281
- // The combined mount point for sub-routes
1282
- const combinedBase = joinMountAndRoute(basePath, mountPath);
1283
1386
  for (const subRoute of subRoutes) {
1284
- // Strip the sub-router's own basePath from the route path
1285
- let routeSuffix = subRoute.path;
1286
- if (routeSuffix.startsWith(subBasePath)) {
1287
- routeSuffix = routeSuffix.slice(subBasePath.length) || '/';
1288
- }
1289
- const fullPath = joinMountAndRoute(combinedBase, routeSuffix);
1387
+ // Sub-routes already have the correct full path
1388
+ // (the recursive call used combinedBase as mountPrefix)
1389
+ const fullPath = subRoute.path;
1290
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
+ }
1291
1398
  routes.push({
1292
1399
  ...subRoute,
1293
1400
  id,
1294
1401
  path: fullPath,
1295
- filename: rel, // Keep parent file as the filename since routes are mounted here
1402
+ filename: rel,
1403
+ config: Object.keys(config).length > 0 ? config : undefined,
1296
1404
  });
1297
1405
  }
1298
1406
  }
@@ -1672,6 +1780,142 @@ export async function parseRoute(rootDir, filename, projectId, deploymentId, vis
1672
1780
  }
1673
1781
  }
1674
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
+ }
1675
1919
  }
1676
1920
  catch (error) {
1677
1921
  if (error instanceof InvalidRouterConfigError || error instanceof SchemaNotExportedError) {