@atlashub/smartstack-cli 4.14.0 → 4.17.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.
@@ -26779,6 +26779,25 @@ var init_detector = __esm({
26779
26779
  }
26780
26780
  });
26781
26781
 
26782
+ // src/mcp/lib/navroute-parser.ts
26783
+ function extractNavRoutes(content) {
26784
+ const results = [];
26785
+ const regex = /\[NavRoute\s*\(\s*"([^"]+)"(?:\s*,\s*Suffix\s*=\s*"([^"]+)")?\s*\)\]/g;
26786
+ for (const match2 of content.matchAll(regex)) {
26787
+ const navRoute = match2[1];
26788
+ const suffix = match2[2];
26789
+ const fullNavRoute = suffix ? `${navRoute}.${suffix}` : navRoute;
26790
+ results.push({ navRoute, suffix, fullNavRoute });
26791
+ }
26792
+ return results;
26793
+ }
26794
+ var init_navroute_parser = __esm({
26795
+ "src/mcp/lib/navroute-parser.ts"() {
26796
+ "use strict";
26797
+ init_esm_shims();
26798
+ }
26799
+ });
26800
+
26782
26801
  // src/mcp/tools/validate-conventions.ts
26783
26802
  import path8 from "path";
26784
26803
  async function handleValidateConventions(args, config2) {
@@ -27291,9 +27310,9 @@ async function validateControllerRoutes(structure, _config, result) {
27291
27310
  const hasHardcodedRoute = content.includes('[Route("api/[controller]")]') || content.includes('[Route("api/') || /\[Route\s*\(\s*"[^"]+"\s*\)\]/.test(content);
27292
27311
  if (hasNavRoute) {
27293
27312
  navRouteCount++;
27294
- const navRouteMatch = content.match(/\[NavRoute\s*\(\s*"([^"]+)"(?:\s*,\s*Suffix\s*=\s*"([^"]+)")?\s*\)\]/);
27295
- if (navRouteMatch) {
27296
- const routePath = navRouteMatch[1];
27313
+ const parsedNavRoutes = extractNavRoutes(content);
27314
+ for (const parsedNR of parsedNavRoutes) {
27315
+ const routePath = parsedNR.navRoute;
27297
27316
  const parts = routePath.split(".");
27298
27317
  if (parts.length < 2) {
27299
27318
  result.errors.push({
@@ -28392,6 +28411,7 @@ var init_validate_conventions = __esm({
28392
28411
  init_fs();
28393
28412
  init_detector();
28394
28413
  init_logger();
28414
+ init_navroute_parser();
28395
28415
  validateConventionsTool = {
28396
28416
  name: "validate_conventions",
28397
28417
  description: "Validate AtlasHub/SmartStack conventions: SQL schemas (core/extensions), domain table prefixes (auth_, nav_, ai_, etc.), migration naming ({context}_v{version}_{sequence}_*), service interfaces (I*Service), namespace structure, controller routes (NavRoute), permission seeding safety",
@@ -57363,7 +57383,7 @@ async function scaffoldApiClient(input, config2) {
57363
57383
  const servicesPath = options?.outputPath || path18.join(webPath, "src", "services", "api");
57364
57384
  const hooksPath = path18.join(webPath, "src", "hooks");
57365
57385
  const typesPath = path18.join(webPath, "src", "types");
57366
- const apiClientContent = generateApiClient(name, nameLower, navRoute, apiPath, methods);
57386
+ const apiClientContent = generateApiClient(name, nameLower, navRoute, apiPath, methods, servicesPath, webPath);
57367
57387
  const apiClientFile = path18.join(servicesPath, `${nameLower}.ts`);
57368
57388
  if (!dryRun) {
57369
57389
  await ensureDirectory(servicesPath);
@@ -57398,7 +57418,13 @@ async function scaffoldApiClient(input, config2) {
57398
57418
  function navRouteToApiPath(navRoute) {
57399
57419
  return `/api/${navRoute.replace(/\./g, "/")}`;
57400
57420
  }
57401
- function generateApiClient(name, nameLower, navRoute, apiPath, methods) {
57421
+ function generateApiClient(name, nameLower, navRoute, apiPath, methods, servicesDir, webPath) {
57422
+ let routesImportPath = "../routes/navRoutes.generated";
57423
+ if (servicesDir && webPath) {
57424
+ const routesDir = path18.join(webPath, "src", "routes");
57425
+ const relativePath2 = path18.relative(servicesDir, routesDir).replace(/\\/g, "/");
57426
+ routesImportPath = `${relativePath2}/navRoutes.generated`;
57427
+ }
57402
57428
  const template = `/**
57403
57429
  * ${name} API Client
57404
57430
  *
@@ -57407,7 +57433,7 @@ function generateApiClient(name, nameLower, navRoute, apiPath, methods) {
57407
57433
  * API Path: ${apiPath}
57408
57434
  */
57409
57435
 
57410
- import { getRoute } from '../routes/navRoutes.generated';
57436
+ import { getRoute } from '${routesImportPath}';
57411
57437
  import { apiClient } from '../lib/apiClient';
57412
57438
  import type {
57413
57439
  ${name},
@@ -57758,6 +57784,34 @@ ensuring frontend routes stay synchronized with backend NavRoute attributes.`,
57758
57784
  }
57759
57785
  });
57760
57786
 
57787
+ // src/mcp/lib/string-utils.ts
57788
+ function toKebabCase(segment) {
57789
+ return segment.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
57790
+ }
57791
+ function toCamelCase2(segment) {
57792
+ return segment.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
57793
+ }
57794
+ function capitalize(str) {
57795
+ return str.charAt(0).toUpperCase() + str.slice(1);
57796
+ }
57797
+ function singularize(word) {
57798
+ if (word.endsWith("ies")) return word.slice(0, -3) + "y";
57799
+ if (word.endsWith("sses")) return word.slice(0, -2);
57800
+ if (word.endsWith("shes") || word.endsWith("ches") || word.endsWith("xes") || word.endsWith("zes")) return word.slice(0, -2);
57801
+ if (word.endsWith("ses")) return word.slice(0, -1);
57802
+ if (word.endsWith("s") && !word.endsWith("ss")) return word.slice(0, -1);
57803
+ return word;
57804
+ }
57805
+ function navRouteToUrlPath(navRoute) {
57806
+ return navRoute.split(".").map(toKebabCase).join("/");
57807
+ }
57808
+ var init_string_utils = __esm({
57809
+ "src/mcp/lib/string-utils.ts"() {
57810
+ "use strict";
57811
+ init_esm_shims();
57812
+ }
57813
+ });
57814
+
57761
57815
  // src/mcp/tools/scaffold-routes.ts
57762
57816
  import path19 from "path";
57763
57817
  async function handleScaffoldRoutes(args, config2) {
@@ -57808,6 +57862,10 @@ async function scaffoldRoutes(input, config2) {
57808
57862
  result.files.push({ path: registryFile, content: registryContent, type: "created" });
57809
57863
  }
57810
57864
  const outputFormat = options?.outputFormat ?? "applicationRoutes";
57865
+ if (outputFormat === "clientRoutes") {
57866
+ if (!result.warnings) result.warnings = [];
57867
+ result.warnings.push('outputFormat "clientRoutes" is DEPRECATED. Use "applicationRoutes" instead (same behavior, correct naming).');
57868
+ }
57811
57869
  if (outputFormat === "applicationRoutes" || outputFormat === "clientRoutes") {
57812
57870
  const pageFiles = await discoverPageFiles(webPath, navRoutes);
57813
57871
  const applicationRoutesContent = generateApplicationRoutesConfig(navRoutes, pageFiles, includeGuards);
@@ -57920,6 +57978,9 @@ async function scaffoldRoutes(input, config2) {
57920
57978
  result.instructions.push("");
57921
57979
  }
57922
57980
  } else {
57981
+ if (!result.warnings) result.warnings = [];
57982
+ result.warnings.push("STANDALONE MODE: createBrowserRouter() is NOT compatible with SmartStack App.tsx which uses useRoutes() + mergeRoutes()");
57983
+ result.warnings.push("All routes are in TODO commented state \u2014 manual activation required");
57923
57984
  const routerContent = generateRouterConfig(navRoutes, includeGuards);
57924
57985
  const routerFile = path19.join(routesPath, "index.tsx");
57925
57986
  if (!dryRun) {
@@ -57941,6 +58002,7 @@ async function scaffoldRoutes(input, config2) {
57941
58002
  }
57942
58003
  }
57943
58004
  if (includeGuards) {
58005
+ result.warnings.push("Auth guards use stub implementations (ProtectedRoute, PermissionGuard) \u2014 replace with your auth hook");
57944
58006
  const guardsContent = generateRouteGuards();
57945
58007
  const guardsFile = path19.join(routesPath, "guards.tsx");
57946
58008
  if (!dryRun) {
@@ -57978,23 +58040,42 @@ async function discoverNavRoutes(structure, scope, warnings) {
57978
58040
  }
57979
58041
  logger.debug("Scanning API paths", { paths: apiPaths, scope });
57980
58042
  let controllerFiles = [];
58043
+ const isSpecificScope = scope !== "all" && scope !== "core" && scope !== "extensions";
57981
58044
  for (const apiPath of apiPaths) {
57982
- const files = await glob("**/*Controller.cs", {
57983
- cwd: apiPath,
57984
- absolute: true,
57985
- ignore: ["**/obj/**", "**/bin/**"]
57986
- });
57987
- controllerFiles = controllerFiles.concat(files);
58045
+ if (isSpecificScope) {
58046
+ const scopePascal = capitalize(scope);
58047
+ const patterns = [
58048
+ `**/${scope}/**/*Controller.cs`,
58049
+ `**/${scopePascal}/**/*Controller.cs`,
58050
+ `**/${scope}*Controller.cs`,
58051
+ `**/${scopePascal}*Controller.cs`
58052
+ ];
58053
+ for (const pattern of patterns) {
58054
+ const files = await glob(pattern, {
58055
+ cwd: apiPath,
58056
+ absolute: true,
58057
+ ignore: ["**/obj/**", "**/bin/**"]
58058
+ });
58059
+ controllerFiles = controllerFiles.concat(files);
58060
+ }
58061
+ controllerFiles = [...new Set(controllerFiles)];
58062
+ } else {
58063
+ const files = await glob("**/*Controller.cs", {
58064
+ cwd: apiPath,
58065
+ absolute: true,
58066
+ ignore: ["**/obj/**", "**/bin/**"]
58067
+ });
58068
+ controllerFiles = controllerFiles.concat(files);
58069
+ }
57988
58070
  }
57989
58071
  for (const file of controllerFiles) {
57990
58072
  try {
57991
58073
  const content = await readText(file);
57992
- const navRouteMatch = content.match(/\[NavRoute\s*\(\s*"([^"]+)"(?:\s*,\s*Suffix\s*=\s*"([^"]+)")?\s*\)\]/);
57993
- if (navRouteMatch) {
57994
- const navRoute = navRouteMatch[1];
57995
- const suffix = navRouteMatch[2];
58074
+ const parsedNavRoutes = extractNavRoutes(content);
58075
+ for (const parsed of parsedNavRoutes) {
58076
+ const { navRoute, suffix, fullNavRoute } = parsed;
57996
58077
  const application = navRoute.split(".")[0];
57997
- if (scope !== "all" && application !== scope) {
58078
+ if (scope !== "all" && scope !== "core" && scope !== "extensions" && application !== scope) {
57998
58079
  continue;
57999
58080
  }
58000
58081
  const controllerMatch = path19.basename(file).match(/(.+)Controller\.cs$/);
@@ -58010,7 +58091,6 @@ async function discoverNavRoutes(structure, scope, warnings) {
58010
58091
  for (const match2 of authorizeMatches) {
58011
58092
  permissions.push(match2[1]);
58012
58093
  }
58013
- const fullNavRoute = suffix ? `${navRoute}.${suffix}` : navRoute;
58014
58094
  routes.push({
58015
58095
  navRoute: fullNavRoute,
58016
58096
  apiPath: `/api/${navRouteToUrlPath(navRoute)}${suffix ? `/${toKebabCase(suffix)}` : ""}`,
@@ -58357,15 +58437,19 @@ async function discoverPageFiles(webPath, routes) {
58357
58437
  const moduleDir = moduleParts.join("/");
58358
58438
  const pagesDir = path19.join(webPath, "src", "pages", app, moduleDir || "");
58359
58439
  try {
58360
- const files = await glob("*Page.tsx", { cwd: pagesDir, absolute: false });
58440
+ const files = await glob("**/*Page.tsx", { cwd: pagesDir, absolute: false });
58361
58441
  if (files.length > 0) {
58362
58442
  const discovery = {};
58363
58443
  for (const f of files) {
58364
- const componentName = f.replace(".tsx", "");
58365
- const importPath = `@/pages/${app}/${moduleDir ? moduleDir + "/" : ""}${componentName}`;
58444
+ const componentName = path19.basename(f, ".tsx");
58445
+ const subDir = path19.dirname(f);
58446
+ const importBase = subDir !== "." ? `${moduleDir ? moduleDir + "/" : ""}${subDir}/` : `${moduleDir ? moduleDir + "/" : ""}`;
58447
+ const importPath = `@/pages/${app}/${importBase}${componentName}`;
58366
58448
  const entry = { importPath, componentName };
58367
58449
  if (componentName.endsWith("CreatePage")) {
58368
58450
  discovery.create = entry;
58451
+ } else if (componentName.startsWith("Create") && componentName.endsWith("Page")) {
58452
+ discovery.create = entry;
58369
58453
  } else if (componentName.endsWith("EditPage")) {
58370
58454
  discovery.edit = entry;
58371
58455
  } else if (componentName.endsWith("DetailPage")) {
@@ -58423,17 +58507,29 @@ function generateApplicationRoutesConfig(routes, pageFiles, includeGuards) {
58423
58507
  for (const route of routes) {
58424
58508
  const discovery = pageFiles.get(route.navRoute);
58425
58509
  const { entityPlural, entitySingular, basePath } = deriveEntityNames(route.navRoute);
58426
- if (!discovery?.list) {
58427
- lines.push(`// TODO: const ${entityPlural}Page = lazy(() => import('${basePath}/${entityPlural}Page').then(m => ({ default: m.${entityPlural}Page })));`);
58428
- }
58429
- if (!discovery?.create) {
58430
- lines.push(`// TODO: const ${entitySingular}CreatePage = lazy(() => import('${basePath}/${entitySingular}CreatePage').then(m => ({ default: m.${entitySingular}CreatePage })));`);
58431
- }
58432
- if (!discovery?.detail) {
58433
- lines.push(`// TODO: const ${entitySingular}DetailPage = lazy(() => import('${basePath}/${entitySingular}DetailPage').then(m => ({ default: m.${entitySingular}DetailPage })));`);
58434
- }
58435
- if (!discovery?.edit) {
58436
- lines.push(`// TODO: const ${entitySingular}EditPage = lazy(() => import('${basePath}/${entitySingular}EditPage').then(m => ({ default: m.${entitySingular}EditPage })));`);
58510
+ if (!discovery?.list && !importedComponents.has(`${entityPlural}Page`)) {
58511
+ importedComponents.add(`${entityPlural}Page`);
58512
+ lines.push(`const ${entityPlural}Page = lazy(() =>`);
58513
+ lines.push(` import('${basePath}/${entityPlural}Page').then(m => ({ default: m.${entityPlural}Page }))`);
58514
+ lines.push(");");
58515
+ }
58516
+ if (!discovery?.create && !importedComponents.has(`${entitySingular}CreatePage`)) {
58517
+ importedComponents.add(`${entitySingular}CreatePage`);
58518
+ lines.push(`const ${entitySingular}CreatePage = lazy(() =>`);
58519
+ lines.push(` import('${basePath}/${entitySingular}CreatePage').then(m => ({ default: m.${entitySingular}CreatePage }))`);
58520
+ lines.push(");");
58521
+ }
58522
+ if (!discovery?.detail && !importedComponents.has(`${entitySingular}DetailPage`)) {
58523
+ importedComponents.add(`${entitySingular}DetailPage`);
58524
+ lines.push(`const ${entitySingular}DetailPage = lazy(() =>`);
58525
+ lines.push(` import('${basePath}/${entitySingular}DetailPage').then(m => ({ default: m.${entitySingular}DetailPage }))`);
58526
+ lines.push(");");
58527
+ }
58528
+ if (!discovery?.edit && !importedComponents.has(`${entitySingular}EditPage`)) {
58529
+ importedComponents.add(`${entitySingular}EditPage`);
58530
+ lines.push(`const ${entitySingular}EditPage = lazy(() =>`);
58531
+ lines.push(` import('${basePath}/${entitySingular}EditPage').then(m => ({ default: m.${entitySingular}EditPage }))`);
58532
+ lines.push(");");
58437
58533
  }
58438
58534
  }
58439
58535
  lines.push("");
@@ -58456,33 +58552,17 @@ function generateApplicationRoutesConfig(routes, pageFiles, includeGuards) {
58456
58552
  const permGuardClose = permGuardOpen ? "</PermissionGuard>" : "";
58457
58553
  lines.push(` // === ${entityPlural.toLowerCase()} (NavRoute: ${route.navRoute}) ===`);
58458
58554
  const listComp = discovery?.list?.componentName || `${entityPlural}Page`;
58459
- const listElement = discovery?.list ? `<Suspense fallback={<PageLoader />}>${permGuardOpen}<${listComp} />${permGuardClose}</Suspense>` : void 0;
58460
- if (listElement) {
58461
- lines.push(` { path: '${modulePath}', element: ${listElement} },`);
58462
- } else {
58463
- lines.push(` // TODO: { path: '${modulePath}', element: <${listComp} /> },`);
58464
- }
58555
+ const listElement = `<Suspense fallback={<PageLoader />}>${permGuardOpen}<${listComp} />${permGuardClose}</Suspense>`;
58556
+ lines.push(` { path: '${modulePath}', element: ${listElement} },`);
58465
58557
  const createComp = discovery?.create?.componentName || `${entitySingular}CreatePage`;
58466
- const createElement = discovery?.create ? `<Suspense fallback={<PageLoader />}>${permGuardOpen}<${createComp} />${permGuardClose}</Suspense>` : void 0;
58467
- if (createElement) {
58468
- lines.push(` { path: '${modulePath}/create', element: ${createElement} },`);
58469
- } else {
58470
- lines.push(` // TODO: { path: '${modulePath}/create', element: <${createComp} /> },`);
58471
- }
58558
+ const createElement = `<Suspense fallback={<PageLoader />}>${permGuardOpen}<${createComp} />${permGuardClose}</Suspense>`;
58559
+ lines.push(` { path: '${modulePath}/create', element: ${createElement} },`);
58472
58560
  const detailComp = discovery?.detail?.componentName || `${entitySingular}DetailPage`;
58473
- const detailElement = discovery?.detail ? `<Suspense fallback={<PageLoader />}>${permGuardOpen}<${detailComp} />${permGuardClose}</Suspense>` : void 0;
58474
- if (detailElement) {
58475
- lines.push(` { path: '${modulePath}/:id', element: ${detailElement} },`);
58476
- } else {
58477
- lines.push(` // TODO: { path: '${modulePath}/:id', element: <${detailComp} /> },`);
58478
- }
58561
+ const detailElement = `<Suspense fallback={<PageLoader />}>${permGuardOpen}<${detailComp} />${permGuardClose}</Suspense>`;
58562
+ lines.push(` { path: '${modulePath}/:id', element: ${detailElement} },`);
58479
58563
  const editComp = discovery?.edit?.componentName || `${entitySingular}EditPage`;
58480
- const editElement = discovery?.edit ? `<Suspense fallback={<PageLoader />}>${permGuardOpen}<${editComp} />${permGuardClose}</Suspense>` : void 0;
58481
- if (editElement) {
58482
- lines.push(` { path: '${modulePath}/:id/edit', element: ${editElement} },`);
58483
- } else {
58484
- lines.push(` // TODO: { path: '${modulePath}/:id/edit', element: <${editComp} /> },`);
58485
- }
58564
+ const editElement = `<Suspense fallback={<PageLoader />}>${permGuardOpen}<${editComp} />${permGuardClose}</Suspense>`;
58565
+ lines.push(` { path: '${modulePath}/:id/edit', element: ${editElement} },`);
58486
58566
  }
58487
58567
  }
58488
58568
  lines.push("");
@@ -58547,24 +58627,6 @@ function generateApplicationRoutesConfig(routes, pageFiles, includeGuards) {
58547
58627
  function getLayoutName(_application) {
58548
58628
  return "AppLayout";
58549
58629
  }
58550
- function toKebabCase(segment) {
58551
- return segment.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
58552
- }
58553
- function toCamelCase2(segment) {
58554
- return segment.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
58555
- }
58556
- function navRouteToUrlPath(navRoute) {
58557
- return navRoute.split(".").map(toKebabCase).join("/");
58558
- }
58559
- function capitalize(str) {
58560
- return str.charAt(0).toUpperCase() + str.slice(1);
58561
- }
58562
- function singularize(word) {
58563
- if (word.endsWith("ies")) return word.slice(0, -3) + "y";
58564
- if (word.endsWith("ses") || word.endsWith("xes") || word.endsWith("zes") || word.endsWith("ches") || word.endsWith("shes")) return word.slice(0, -2);
58565
- if (word.endsWith("s") && !word.endsWith("ss")) return word.slice(0, -1);
58566
- return word;
58567
- }
58568
58630
  function deriveEntityNames(navRoute) {
58569
58631
  const parts = navRoute.split(".");
58570
58632
  const lastSegment = parts[parts.length - 1];
@@ -58595,11 +58657,23 @@ function formatResult5(result, input) {
58595
58657
  const relativePath2 = file.path.replace(/\\/g, "/").split("/src/").pop() || file.path;
58596
58658
  lines.push(`### ${relativePath2}`);
58597
58659
  lines.push("");
58660
+ if (file.content.length > 2e3) {
58661
+ lines.push(`> **Note:** Content truncated to 2000 chars. Full file written to: \`${relativePath2}\``);
58662
+ lines.push("");
58663
+ }
58598
58664
  lines.push("```tsx");
58599
58665
  lines.push(file.content.substring(0, 2e3) + (file.content.length > 2e3 ? "\n// ... (truncated)" : ""));
58600
58666
  lines.push("```");
58601
58667
  lines.push("");
58602
58668
  }
58669
+ if (result.warnings && result.warnings.length > 0) {
58670
+ lines.push("## Warnings");
58671
+ lines.push("");
58672
+ for (const warning of result.warnings) {
58673
+ lines.push(`> ${warning}`);
58674
+ }
58675
+ lines.push("");
58676
+ }
58603
58677
  lines.push("## Instructions");
58604
58678
  lines.push("");
58605
58679
  for (const instruction of result.instructions) {
@@ -58617,6 +58691,8 @@ var init_scaffold_routes = __esm({
58617
58691
  init_detector();
58618
58692
  init_fs();
58619
58693
  init_esm8();
58694
+ init_string_utils();
58695
+ init_navroute_parser();
58620
58696
  scaffoldRoutesTool = {
58621
58697
  name: "scaffold_routes",
58622
58698
  description: `Generate React Router configuration from backend NavRoute attributes.
@@ -58717,6 +58793,9 @@ async function validateFrontendRoutes2(input, config2) {
58717
58793
  if (scope === "all" || scope === "routes") {
58718
58794
  await validateAppWiring(webPath, backendRoutes, result);
58719
58795
  }
58796
+ if (scope === "all" || scope === "routes") {
58797
+ await validateCrudRoutes(webPath, backendRoutes, result);
58798
+ }
58720
58799
  generateRecommendations2(result);
58721
58800
  result.valid = result.apiClients.issues.filter((i) => i.severity === "error").length === 0 && result.routes.missing.length === 0 && result.registry.exists && result.appWiring.issues.length === 0;
58722
58801
  return result;
@@ -58735,11 +58814,9 @@ async function discoverBackendNavRoutes(structure) {
58735
58814
  for (const file of controllerFiles) {
58736
58815
  try {
58737
58816
  const content = await readText(file);
58738
- const navRouteMatch = content.match(/\[NavRoute\s*\(\s*"([^"]+)"(?:\s*,\s*Suffix\s*=\s*"([^"]+)")?\s*\)\]/);
58739
- if (navRouteMatch) {
58740
- const navRoute = navRouteMatch[1];
58741
- const suffix = navRouteMatch[2];
58742
- const fullNavRoute = suffix ? `${navRoute}.${suffix}` : navRoute;
58817
+ const parsedNavRoutes = extractNavRoutes(content);
58818
+ for (const parsed of parsedNavRoutes) {
58819
+ const { navRoute, suffix, fullNavRoute } = parsed;
58743
58820
  const controllerMatch = path20.basename(file).match(/(.+)Controller\.cs$/);
58744
58821
  const controllerName = controllerMatch ? controllerMatch[1] : "Unknown";
58745
58822
  const methods = [];
@@ -58755,8 +58832,8 @@ async function discoverBackendNavRoutes(structure) {
58755
58832
  }
58756
58833
  routes.push({
58757
58834
  navRoute: fullNavRoute,
58758
- apiPath: `/api/${navRoute.split(".").map(toKebabCase2).join("/")}${suffix ? `/${toKebabCase2(suffix)}` : ""}`,
58759
- webPath: `/${navRoute.split(".").map(toKebabCase2).join("/")}${suffix ? `/${toKebabCase2(suffix)}` : ""}`,
58835
+ apiPath: `/api/${navRoute.split(".").map(toKebabCase).join("/")}${suffix ? `/${toKebabCase(suffix)}` : ""}`,
58836
+ webPath: `/${navRoute.split(".").map(toKebabCase).join("/")}${suffix ? `/${toKebabCase(suffix)}` : ""}`,
58760
58837
  permissions,
58761
58838
  controller: controllerName,
58762
58839
  methods
@@ -58845,6 +58922,43 @@ async function validateApiClients(webPath, backendRoutes, result) {
58845
58922
  logger.debug(`Failed to parse API client: ${file}`);
58846
58923
  }
58847
58924
  }
58925
+ const pagesPath = path20.join(webPath, "src", "pages");
58926
+ try {
58927
+ const pageFiles = await glob("**/*.tsx", {
58928
+ cwd: pagesPath,
58929
+ absolute: true,
58930
+ ignore: ["**/node_modules/**"]
58931
+ });
58932
+ for (const file of pageFiles) {
58933
+ try {
58934
+ const content = await readText(file);
58935
+ const relativePath2 = path20.relative(webPath, file);
58936
+ const hardcodedNavigateMatches = content.matchAll(/navigate\s*\(\s*['"`](\/[^'"`]+)['"`]/g);
58937
+ for (const match2 of hardcodedNavigateMatches) {
58938
+ result.apiClients.issues.push({
58939
+ type: "invalid-path",
58940
+ severity: "warning",
58941
+ file: relativePath2,
58942
+ message: `Hardcoded navigate path: ${match2[1]}`,
58943
+ suggestion: "Use getWebRoute() from navRoutes.generated.ts for type-safe navigation"
58944
+ });
58945
+ }
58946
+ const hardcodedToMatches = content.matchAll(/to\s*=\s*['"`](\/[a-z][^'"`]+)['"`]/g);
58947
+ for (const match2 of hardcodedToMatches) {
58948
+ result.apiClients.issues.push({
58949
+ type: "invalid-path",
58950
+ severity: "warning",
58951
+ file: relativePath2,
58952
+ message: `Hardcoded Link to path: ${match2[1]}`,
58953
+ suggestion: "Use getWebRoute() from navRoutes.generated.ts for type-safe navigation"
58954
+ });
58955
+ }
58956
+ } catch {
58957
+ logger.debug(`Failed to parse page file: ${file}`);
58958
+ }
58959
+ }
58960
+ } catch {
58961
+ }
58848
58962
  }
58849
58963
  async function validateRoutes(webPath, backendRoutes, result) {
58850
58964
  const routeCandidates = ["applicationRoutes.generated.tsx", "clientRoutes.generated.tsx", "index.tsx"];
@@ -58874,16 +58988,9 @@ async function validateRoutes(webPath, backendRoutes, result) {
58874
58988
  }
58875
58989
  result.routes.total = frontendPaths.size;
58876
58990
  for (const backendRoute of backendRoutes) {
58877
- const webPath2 = backendRoute.webPath.replace(/^\//, "");
58878
- const parts = webPath2.split("/");
58879
- let found = false;
58880
- for (const pathPart of parts) {
58881
- if (frontendPaths.has(pathPart)) {
58882
- found = true;
58883
- break;
58884
- }
58885
- }
58886
- if (!found && parts.length > 0) {
58991
+ const modulePath = backendRoute.navRoute.split(".").slice(1).map(toKebabCase).join("/");
58992
+ const found = frontendPaths.has(modulePath) || [...frontendPaths].some((fp) => fp.startsWith(modulePath + "/") || fp === modulePath);
58993
+ if (!found && modulePath.length > 0) {
58887
58994
  result.routes.missing.push(backendRoute.navRoute);
58888
58995
  }
58889
58996
  }
@@ -58891,9 +58998,13 @@ async function validateRoutes(webPath, backendRoutes, result) {
58891
58998
  if (frontendPath === "*" || frontendPath === "" || frontendPath.startsWith(":")) {
58892
58999
  continue;
58893
59000
  }
58894
- const matchingBackend = backendRoutes.find(
58895
- (r) => r.webPath.includes(frontendPath) || r.navRoute.includes(frontendPath)
58896
- );
59001
+ if (frontendPath.endsWith("/create") || frontendPath.endsWith("/edit") || frontendPath.includes(":")) {
59002
+ continue;
59003
+ }
59004
+ const matchingBackend = backendRoutes.find((r) => {
59005
+ const backendModulePath = r.navRoute.split(".").slice(1).map(toKebabCase).join("/");
59006
+ return backendModulePath === frontendPath || frontendPath.startsWith(backendModulePath + "/");
59007
+ });
58897
59008
  if (!matchingBackend) {
58898
59009
  result.routes.orphaned.push(frontendPath);
58899
59010
  }
@@ -58959,7 +59070,7 @@ async function validateAppWiring(webPath, backendRoutes, result) {
58959
59070
  for (const route of backendRoutes) {
58960
59071
  const modulePath = route.webPath.replace(/^\//, "");
58961
59072
  if (modulePath !== modulePath.toLowerCase()) {
58962
- const kebabPath = modulePath.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
59073
+ const kebabPath = modulePath.split("/").map(toKebabCase).join("/");
58963
59074
  caseMismatches.push(
58964
59075
  `Route "${route.navRoute}" uses PascalCase in URL. Found: ${modulePath} \u2192 Expected: ${kebabPath}`
58965
59076
  );
@@ -58975,9 +59086,8 @@ async function validateAppWiring(webPath, backendRoutes, result) {
58975
59086
  );
58976
59087
  }
58977
59088
  for (const route of backendRoutes) {
58978
- const modulePath = route.navRoute.split(".").slice(1).map(toKebabCase2).join("/");
58979
- const lastSegment = route.navRoute.split(".").pop() || "";
58980
- const pathInApp = appContent.includes(`path="${modulePath}"`) || appContent.includes(`path='${modulePath}'`) || appContent.includes(`path="${lastSegment}"`) || appContent.includes(`path='${lastSegment}'`);
59089
+ const modulePath = route.navRoute.split(".").slice(1).map(toKebabCase).join("/");
59090
+ const pathInApp = appContent.includes(`path="${modulePath}"`) || appContent.includes(`path='${modulePath}'`) || appContent.includes(`'${modulePath}'`) || appContent.includes(`"${modulePath}"`);
58981
59091
  if (!pathInApp) {
58982
59092
  result.appWiring.issues.push(
58983
59093
  `Route "${route.navRoute}" (path: ${modulePath}) not wired in App.tsx`
@@ -58985,6 +59095,52 @@ async function validateAppWiring(webPath, backendRoutes, result) {
58985
59095
  }
58986
59096
  }
58987
59097
  }
59098
+ async function validateCrudRoutes(webPath, backendRoutes, result) {
59099
+ const routeCandidates = ["applicationRoutes.generated.tsx", "clientRoutes.generated.tsx"];
59100
+ let routesContent = "";
59101
+ for (const candidate of routeCandidates) {
59102
+ const candidatePath = path20.join(webPath, "src", "routes", candidate);
59103
+ if (await fileExists(candidatePath)) {
59104
+ try {
59105
+ routesContent = await readText(candidatePath);
59106
+ } catch {
59107
+ logger.debug(`Failed to read ${candidatePath}`);
59108
+ }
59109
+ break;
59110
+ }
59111
+ }
59112
+ if (!routesContent) {
59113
+ return;
59114
+ }
59115
+ for (const route of backendRoutes) {
59116
+ const modulePath = route.navRoute.split(".").slice(1).map(toKebabCase).join("/");
59117
+ if (modulePath.length === 0) continue;
59118
+ const crudSuffixes = [
59119
+ { suffix: "/create", label: "create" },
59120
+ { suffix: "/:id", label: "detail" },
59121
+ { suffix: "/:id/edit", label: "edit" }
59122
+ ];
59123
+ for (const { suffix, label } of crudSuffixes) {
59124
+ const fullPath = `${modulePath}${suffix}`;
59125
+ const todoPattern = `// TODO:.*path:\\s*'${fullPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}'`;
59126
+ if (new RegExp(todoPattern).test(routesContent)) {
59127
+ result.appWiring.issues.push(
59128
+ `Route "${route.navRoute}" ${label} route is commented out (// TODO:). Run \`scaffold_routes\` to regenerate active routes.`
59129
+ );
59130
+ }
59131
+ if (label === "create") {
59132
+ const newPath = `${modulePath}/new`;
59133
+ const hasNewRoute = routesContent.includes(`path: '${newPath}'`) || routesContent.includes(`path="${newPath}"`);
59134
+ const hasCreateRoute = routesContent.includes(`path: '${modulePath}/create'`) || routesContent.includes(`path="${modulePath}/create"`);
59135
+ if (hasNewRoute && !hasCreateRoute) {
59136
+ result.appWiring.issues.push(
59137
+ `Route "${route.navRoute}" uses /new instead of /create. SmartStack convention is /create. Run \`scaffold_routes\` to fix.`
59138
+ );
59139
+ }
59140
+ }
59141
+ }
59142
+ }
59143
+ }
58988
59144
  function generateRecommendations2(result) {
58989
59145
  if (!result.registry.exists) {
58990
59146
  result.recommendations.push('Run `scaffold_routes source="controllers"` to generate route registry');
@@ -59019,9 +59175,6 @@ function generateRecommendations2(result) {
59019
59175
  result.recommendations.push("All routes are synchronized between frontend and backend");
59020
59176
  }
59021
59177
  }
59022
- function toKebabCase2(segment) {
59023
- return segment.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
59024
- }
59025
59178
  function formatResult6(result, _input) {
59026
59179
  const lines = [];
59027
59180
  const statusIcon = result.valid ? "\u2705" : "\u274C";
@@ -59130,6 +59283,8 @@ var init_validate_frontend_routes = __esm({
59130
59283
  init_detector();
59131
59284
  init_fs();
59132
59285
  init_esm8();
59286
+ init_string_utils();
59287
+ init_navroute_parser();
59133
59288
  validateFrontendRoutesTool = {
59134
59289
  name: "validate_frontend_routes",
59135
59290
  description: `Validate frontend routes against backend NavRoute attributes.