@atlashub/smartstack-cli 4.31.0 → 4.33.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 (41) hide show
  1. package/.documentation/commands.html +952 -116
  2. package/.documentation/index.html +2 -2
  3. package/.documentation/init.html +358 -174
  4. package/dist/mcp-entry.mjs +271 -44
  5. package/dist/mcp-entry.mjs.map +1 -1
  6. package/package.json +1 -1
  7. package/templates/mcp-scaffolding/controller.cs.hbs +54 -128
  8. package/templates/project/README.md +19 -0
  9. package/templates/skills/apex/SKILL.md +16 -10
  10. package/templates/skills/apex/_shared.md +1 -1
  11. package/templates/skills/apex/references/checks/architecture-checks.sh +154 -0
  12. package/templates/skills/apex/references/checks/backend-checks.sh +194 -0
  13. package/templates/skills/apex/references/checks/frontend-checks.sh +448 -0
  14. package/templates/skills/apex/references/checks/infrastructure-checks.sh +255 -0
  15. package/templates/skills/apex/references/checks/security-checks.sh +153 -0
  16. package/templates/skills/apex/references/checks/seed-checks.sh +536 -0
  17. package/templates/skills/apex/references/frontend-route-wiring-app-tsx.md +49 -192
  18. package/templates/skills/apex/references/parallel-execution.md +18 -5
  19. package/templates/skills/apex/references/post-checks.md +124 -2156
  20. package/templates/skills/apex/references/smartstack-api.md +160 -957
  21. package/templates/skills/apex/references/smartstack-frontend-compliance.md +23 -1
  22. package/templates/skills/apex/references/smartstack-frontend.md +134 -1022
  23. package/templates/skills/apex/references/smartstack-layers.md +12 -6
  24. package/templates/skills/apex/steps/step-00-init.md +81 -238
  25. package/templates/skills/apex/steps/step-03-execute.md +25 -751
  26. package/templates/skills/apex/steps/step-03a-layer0-domain.md +118 -0
  27. package/templates/skills/apex/steps/step-03b-layer1-seed.md +91 -0
  28. package/templates/skills/apex/steps/step-03c-layer2-backend.md +240 -0
  29. package/templates/skills/apex/steps/step-03d-layer3-frontend.md +300 -0
  30. package/templates/skills/apex/steps/step-03e-layer4-devdata.md +44 -0
  31. package/templates/skills/apex/steps/step-04-examine.md +70 -150
  32. package/templates/skills/application/references/frontend-i18n-and-output.md +2 -2
  33. package/templates/skills/application/references/frontend-route-naming.md +5 -1
  34. package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +49 -198
  35. package/templates/skills/application/references/frontend-verification.md +11 -11
  36. package/templates/skills/application/steps/step-05-frontend.md +26 -15
  37. package/templates/skills/application/templates-frontend.md +4 -0
  38. package/templates/skills/cli-app-sync/SKILL.md +2 -2
  39. package/templates/skills/cli-app-sync/references/comparison-map.md +1 -1
  40. package/templates/skills/controller/references/controller-code-templates.md +70 -67
  41. package/templates/skills/controller/references/mcp-scaffold-workflow.md +5 -1
@@ -25984,6 +25984,7 @@ var init_types3 = __esm({
25984
25984
  entityProperties: external_exports.array(EntityPropertySchema).optional().describe("Entity properties for DTO/Validator generation"),
25985
25985
  navRoute: external_exports.string().optional().describe('Navigation route path for controller (e.g., "administration.users"). Required for controllers.'),
25986
25986
  navRouteSuffix: external_exports.string().optional().describe('Optional suffix for NavRoute (e.g., "dashboard" for sub-resources)'),
25987
+ customSegment: external_exports.string().optional().describe('Optional custom URL segment for NavRoute (e.g., "user/dashboard")'),
25987
25988
  withHierarchyFunction: external_exports.boolean().optional().describe("For entity type with self-reference (ParentId): generate TVF SQL script for hierarchy traversal"),
25988
25989
  hierarchyDirection: external_exports.enum(["ancestors", "descendants", "both"]).optional().describe("Direction for hierarchy traversal function (default: both)"),
25989
25990
  isPersonRole: external_exports.boolean().optional().describe("If true, entity is a Person Extension linked to User via UserId FK"),
@@ -26101,7 +26102,7 @@ var init_types3 = __esm({
26101
26102
  includeLayouts: external_exports.boolean().default(true).describe("Generate layout components"),
26102
26103
  includeGuards: external_exports.boolean().default(true).describe("Include route guards for permissions"),
26103
26104
  generateRegistry: external_exports.boolean().default(true).describe("Generate navRoutes.generated.ts"),
26104
- outputFormat: external_exports.enum(["standalone", "applicationRoutes", "clientRoutes"]).default("applicationRoutes").describe('Output format: "standalone" generates createBrowserRouter(), "applicationRoutes" generates exportable RouteObject[] arrays for App.tsx integration (note: "clientRoutes" is a deprecated alias for "applicationRoutes")'),
26105
+ outputFormat: external_exports.enum(["standalone", "applicationRoutes", "clientRoutes", "componentRegistry"]).default("componentRegistry").describe('Output format: "componentRegistry" (v3.7+) generates PageRegistry.register() calls for DynamicRouter. "applicationRoutes" (deprecated) generates RouteObject[] arrays for App.tsx. "standalone" generates createBrowserRouter(). "clientRoutes" is a deprecated alias for "applicationRoutes".'),
26105
26106
  dryRun: external_exports.boolean().default(false).describe("Preview without writing files")
26106
26107
  }).optional()
26107
26108
  });
@@ -27984,6 +27985,60 @@ async function validateFrontendRoutes(structure, _config, result) {
27984
27985
  return;
27985
27986
  }
27986
27987
  const appContent = await readText(appFiles[0]);
27988
+ const registryFiles = await findFiles("**/componentRegistry.generated.ts", { cwd: structure.web });
27989
+ const usesDynamicRouter = registryFiles.length > 0;
27990
+ if (usesDynamicRouter) {
27991
+ const registryContent = await readText(registryFiles[0]);
27992
+ const registeredKeys = /* @__PURE__ */ new Set();
27993
+ const registerMatches = registryContent.matchAll(/PageRegistry\.register\s*\(\s*['"]([^'"]+)['"]/g);
27994
+ for (const match2 of registerMatches) {
27995
+ registeredKeys.add(match2[1]);
27996
+ }
27997
+ const seedFiles2 = await findFiles("**/*NavigationSeedData.cs", { cwd: structure.root });
27998
+ const seedNavRoutes2 = [];
27999
+ for (const file of seedFiles2) {
28000
+ const content = await readText(file);
28001
+ const routeMatches = content.matchAll(/(?:route:\s*|Route\s*=\s*)["']([^"']+)["']/g);
28002
+ for (const match2 of routeMatches) {
28003
+ seedNavRoutes2.push({
28004
+ route: match2[1],
28005
+ file: path8.relative(structure.root, file)
28006
+ });
28007
+ }
28008
+ }
28009
+ const missingRoutes2 = [];
28010
+ for (const nav of seedNavRoutes2) {
28011
+ const segments = nav.route.split("/").filter(Boolean);
28012
+ const componentKey = segments.join(".");
28013
+ if (!registeredKeys.has(componentKey)) {
28014
+ missingRoutes2.push(nav);
28015
+ }
28016
+ }
28017
+ if (missingRoutes2.length > 0) {
28018
+ for (const missing of missingRoutes2.slice(0, 5)) {
28019
+ result.errors.push({
28020
+ type: "error",
28021
+ category: "frontend-routes",
28022
+ message: `Navigation route "${missing.route}" from seed data has no PageRegistry.register() entry`,
28023
+ file: path8.relative(structure.root, registryFiles[0]),
28024
+ suggestion: `Run scaffold_routes (outputFormat: "componentRegistry") to regenerate (defined in ${missing.file})`
28025
+ });
28026
+ }
28027
+ if (missingRoutes2.length > 5) {
28028
+ result.errors.push({
28029
+ type: "error",
28030
+ category: "frontend-routes",
28031
+ message: `... and ${missingRoutes2.length - 5} more seed data routes missing from componentRegistry`
28032
+ });
28033
+ }
28034
+ }
28035
+ result.warnings.push({
28036
+ type: "warning",
28037
+ category: "frontend-routes",
28038
+ message: `Frontend routes summary (DynamicRouter): ${registeredKeys.size} registered keys, ${missingRoutes2.length} missing from componentRegistry`
28039
+ });
28040
+ return;
28041
+ }
27987
28042
  const hasApplicationRoutes = appContent.includes("applicationRoutes") || appContent.includes("clientRoutes");
27988
28043
  const hasRouteComponents = /<Route\s/.test(appContent);
27989
28044
  if (!hasApplicationRoutes && !hasRouteComponents) {
@@ -34705,7 +34760,14 @@ function resolveHierarchy(navRoute) {
34705
34760
  domainPath = application;
34706
34761
  }
34707
34762
  const infraPath = domainPath;
34708
- const controllerArea = application;
34763
+ let controllerArea = "";
34764
+ if (segments.length >= 3) {
34765
+ controllerArea = path10.join(application, module, section);
34766
+ } else if (segments.length >= 2) {
34767
+ controllerArea = path10.join(application, module);
34768
+ } else if (segments.length === 1) {
34769
+ controllerArea = application;
34770
+ }
34709
34771
  return { application, module, section, domainPath, infraPath, controllerArea };
34710
34772
  }
34711
34773
  async function handleScaffoldExtension(args, config2) {
@@ -35635,88 +35697,117 @@ GO
35635
35697
  }
35636
35698
  async function scaffoldController(name, options, structure, config2, result, dryRun = false) {
35637
35699
  const hierarchy = resolveHierarchy(options?.navRoute);
35638
- const namespace = options?.namespace || (hierarchy.controllerArea ? `${config2.conventions.namespaces.api}.Controllers.${hierarchy.controllerArea}` : `${config2.conventions.namespaces.api}.Controllers`);
35700
+ const namespaceArea = hierarchy.controllerArea.replace(/[\\/]/g, ".");
35701
+ const namespace = options?.namespace || (namespaceArea ? `${config2.conventions.namespaces.api}.Controllers.${namespaceArea}` : `${config2.conventions.namespaces.api}.Controllers`);
35639
35702
  const navRoute = options?.navRoute;
35640
35703
  const navRouteSuffix = options?.navRouteSuffix;
35641
- const routeAttribute = navRoute ? navRouteSuffix ? `[NavRoute("${navRoute}", Suffix = "${navRouteSuffix}")]` : `[NavRoute("${navRoute}")]` : `[Route("api/[controller]")]`;
35704
+ const customSegment = options?.customSegment;
35705
+ const navRouteParams = [];
35706
+ if (navRoute) {
35707
+ navRouteParams.push(`"${navRoute}"`);
35708
+ if (navRouteSuffix) navRouteParams.push(`Suffix = "${navRouteSuffix}"`);
35709
+ if (customSegment) navRouteParams.push(`CustomSegment = "${customSegment}"`);
35710
+ }
35711
+ const routeAttribute = navRoute ? `[NavRoute(${navRouteParams.join(", ")})]` : `[Route("api/[controller]")]`;
35642
35712
  const navRouteUsing = navRoute ? "using SmartStack.Api.Routing;\n" : "";
35643
- const controllerTemplate = `using Microsoft.AspNetCore.Authorization;
35713
+ const controllerTemplate = `using MediatR;
35644
35714
  using Microsoft.AspNetCore.Mvc;
35645
- using Microsoft.Extensions.Logging;
35646
- ${navRouteUsing}
35715
+ using SmartStack.Api.Authorization;
35716
+ ${navRouteUsing}using SmartStack.Application.Common.Authorization;
35717
+
35647
35718
  namespace {{namespace}};
35648
35719
 
35649
35720
  /// <summary>
35650
35721
  /// API controller for {{name}} operations.
35651
- /// IMPORTANT: Use [RequirePermission] on each endpoint for RBAC enforcement.
35652
35722
  /// </summary>
35653
35723
  [ApiController]
35654
35724
  {{routeAttribute}}
35655
- [Authorize]
35725
+ [Microsoft.AspNetCore.Authorization.Authorize]
35726
+ [Produces("application/json")]
35727
+ [Tags("{{namePlural}}")]
35656
35728
  public class {{name}}Controller : ControllerBase
35657
35729
  {
35658
- private readonly I{{name}}Service _service;
35659
- private readonly ILogger<{{name}}Controller> _logger;
35730
+ private readonly ISender _mediator;
35660
35731
 
35661
- public {{name}}Controller(I{{name}}Service service, ILogger<{{name}}Controller> logger)
35662
- {
35663
- _service = service;
35664
- _logger = logger;
35665
- }
35732
+ public {{name}}Controller(ISender mediator) => _mediator = mediator;
35666
35733
 
35667
35734
  /// <summary>
35668
35735
  /// Get all {{nameLower}}s
35669
35736
  /// </summary>
35670
35737
  [HttpGet]
35671
- // TODO: Add [RequirePermission(Permissions.{Module}.Read)] for RBAC
35672
- public async Task<ActionResult<IEnumerable<object>>> GetAll(CancellationToken ct)
35738
+ [RequirePermission(Permissions.{{module}}.{{namePlural}}.View)]
35739
+ [ProducesResponseType(typeof(List<{{name}}ListDto>), StatusCodes.Status200OK)]
35740
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
35741
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
35742
+ public async Task<ActionResult<List<{{name}}ListDto>>> GetAll(CancellationToken ct)
35673
35743
  {
35674
- // TODO: Call _service.GetAllAsync(ct)
35675
- return Ok(Array.Empty<object>());
35744
+ var result = await _mediator.Send(new Get{{namePlural}}Query(), ct);
35745
+ return Ok(result);
35676
35746
  }
35677
35747
 
35678
35748
  /// <summary>
35679
35749
  /// Get {{nameLower}} by ID
35680
35750
  /// </summary>
35681
35751
  [HttpGet("{id:guid}")]
35682
- // TODO: Add [RequirePermission(Permissions.{Module}.Read)] for RBAC
35683
- public async Task<ActionResult<object>> GetById(Guid id, CancellationToken ct)
35752
+ [RequirePermission(Permissions.{{module}}.{{namePlural}}.View)]
35753
+ [ProducesResponseType(typeof({{name}}DetailDto), StatusCodes.Status200OK)]
35754
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
35755
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
35756
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
35757
+ public async Task<ActionResult<{{name}}DetailDto>> GetById(Guid id, CancellationToken ct)
35684
35758
  {
35685
- // TODO: Call _service.GetByIdAsync(id, ct)
35686
- return NotFound();
35759
+ var result = await _mediator.Send(new Get{{name}}ByIdQuery(id), ct);
35760
+ if (result == null) return NotFound(new { message = "{{name}} not found" });
35761
+ return Ok(result);
35687
35762
  }
35688
35763
 
35689
35764
  /// <summary>
35690
35765
  /// Create new {{nameLower}}
35691
35766
  /// </summary>
35692
35767
  [HttpPost]
35693
- // TODO: Add [RequirePermission(Permissions.{Module}.Create)] for RBAC
35694
- public async Task<ActionResult<object>> Create([FromBody] object request, CancellationToken ct)
35768
+ [RequirePermission(Permissions.{{module}}.{{namePlural}}.Create)]
35769
+ [ProducesResponseType(typeof({{name}}DetailDto), StatusCodes.Status201Created)]
35770
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
35771
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
35772
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
35773
+ public async Task<ActionResult<{{name}}DetailDto>> Create(
35774
+ [FromBody] Create{{name}}Request request, CancellationToken ct)
35695
35775
  {
35696
- // TODO: Call _service.CreateAsync(dto, ct)
35697
- return CreatedAtAction(nameof(GetById), new { id = Guid.NewGuid() }, null);
35776
+ var result = await _mediator.Send(new Create{{name}}Command(request), ct);
35777
+ return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
35698
35778
  }
35699
35779
 
35700
35780
  /// <summary>
35701
35781
  /// Update {{nameLower}}
35702
35782
  /// </summary>
35703
35783
  [HttpPut("{id:guid}")]
35704
- // TODO: Add [RequirePermission(Permissions.{Module}.Update)] for RBAC
35705
- public async Task<ActionResult> Update(Guid id, [FromBody] object request, CancellationToken ct)
35784
+ [RequirePermission(Permissions.{{module}}.{{namePlural}}.Update)]
35785
+ [ProducesResponseType(typeof({{name}}DetailDto), StatusCodes.Status200OK)]
35786
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
35787
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
35788
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
35789
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
35790
+ public async Task<ActionResult<{{name}}DetailDto>> Update(
35791
+ Guid id, [FromBody] Update{{name}}Request request, CancellationToken ct)
35706
35792
  {
35707
- // TODO: Call _service.UpdateAsync(id, dto, ct)
35708
- return NoContent();
35793
+ var result = await _mediator.Send(new Update{{name}}Command(id, request), ct);
35794
+ if (result.IsNotFound) return NotFound(new { message = "{{name}} not found" });
35795
+ return Ok(result.Data);
35709
35796
  }
35710
35797
 
35711
35798
  /// <summary>
35712
35799
  /// Delete {{nameLower}}
35713
35800
  /// </summary>
35714
35801
  [HttpDelete("{id:guid}")]
35715
- // TODO: Add [RequirePermission(Permissions.{Module}.Delete)] for RBAC
35716
- public async Task<ActionResult> Delete(Guid id, CancellationToken ct)
35802
+ [RequirePermission(Permissions.{{module}}.{{namePlural}}.Delete)]
35803
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
35804
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
35805
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
35806
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
35807
+ public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
35717
35808
  {
35718
- // TODO: Call _service.DeleteAsync(id, ct)
35719
- return NoContent();
35809
+ var success = await _mediator.Send(new Delete{{name}}Command(id), ct);
35810
+ return success ? NoContent() : NotFound(new { message = "{{name}} not found" });
35720
35811
  }
35721
35812
  }
35722
35813
  `;
@@ -35724,6 +35815,8 @@ public class {{name}}Controller : ControllerBase
35724
35815
  namespace,
35725
35816
  name,
35726
35817
  nameLower: name.charAt(0).toLowerCase() + name.slice(1),
35818
+ namePlural: name + "s",
35819
+ module: hierarchy.module || name,
35727
35820
  routeAttribute
35728
35821
  };
35729
35822
  const controllerContent = import_handlebars.default.compile(controllerTemplate)(context);
@@ -35739,7 +35832,7 @@ public class {{name}}Controller : ControllerBase
35739
35832
  result.files.push({ path: controllerFilePath, content: controllerContent, type: "created" });
35740
35833
  if (navRoute) {
35741
35834
  result.instructions.push("Controller created with NavRoute (route resolved from DB at startup).");
35742
- result.instructions.push(`NavRoute: ${navRoute}${navRouteSuffix ? ` (Suffix: ${navRouteSuffix})` : ""}`);
35835
+ result.instructions.push(`NavRoute: ${navRoute}${navRouteSuffix ? ` (Suffix: ${navRouteSuffix})` : ""}${customSegment ? ` (CustomSegment: ${customSegment})` : ""}`);
35743
35836
  result.instructions.push("");
35744
35837
  result.instructions.push("NavRoute resolves API routes from Navigation entities in the database.");
35745
35838
  result.instructions.push("Ensure the navigation path exists (seed data required):");
@@ -36818,6 +36911,10 @@ var init_scaffold_extension = __esm({
36818
36911
  type: "string",
36819
36912
  description: 'Optional suffix for NavRoute (e.g., "dashboard" for sub-resources)'
36820
36913
  },
36914
+ customSegment: {
36915
+ type: "string",
36916
+ description: 'Optional custom URL segment for NavRoute (e.g., "user/dashboard")'
36917
+ },
36821
36918
  withHierarchyFunction: {
36822
36919
  type: "boolean",
36823
36920
  description: "For entity type with self-reference (ParentId): generate TVF SQL script for hierarchy traversal"
@@ -57877,11 +57974,36 @@ async function scaffoldRoutes(input, config2) {
57877
57974
  }
57878
57975
  result.files.push({ path: registryFile, content: registryContent, type: "created" });
57879
57976
  }
57880
- const outputFormat = options?.outputFormat ?? "applicationRoutes";
57977
+ const outputFormat = options?.outputFormat ?? "componentRegistry";
57881
57978
  if (outputFormat === "clientRoutes") {
57882
57979
  if (!result.warnings) result.warnings = [];
57883
57980
  result.warnings.push('outputFormat "clientRoutes" is DEPRECATED. Use "applicationRoutes" instead (same behavior, correct naming).');
57884
57981
  }
57982
+ if (outputFormat === "applicationRoutes") {
57983
+ if (!result.warnings) result.warnings = [];
57984
+ result.warnings.push(
57985
+ 'outputFormat "applicationRoutes" is DEPRECATED (v3.7+). Use "componentRegistry" for PageRegistry + DynamicRouter pattern. See: DynamicRouter replaces manual App.tsx wiring.'
57986
+ );
57987
+ }
57988
+ if (outputFormat === "componentRegistry") {
57989
+ const pageFiles = await discoverPageFiles(webPath, navRoutes);
57990
+ const registryContent = generateComponentRegistry(navRoutes, pageFiles, webPath);
57991
+ const registryFile = path19.join(
57992
+ options?.outputPath || path19.join(webPath, "src", "extensions"),
57993
+ "componentRegistry.generated.ts"
57994
+ );
57995
+ if (!dryRun) {
57996
+ await ensureDirectory(path19.dirname(registryFile));
57997
+ await writeText(registryFile, registryContent);
57998
+ }
57999
+ result.files.push({ path: registryFile, content: registryContent, type: "created" });
58000
+ result.instructions.push(`Generated PageRegistry.register() calls for ${navRoutes.length} NavRoutes`);
58001
+ result.instructions.push("");
58002
+ result.instructions.push("## Setup (one-time)");
58003
+ result.instructions.push("1. Ensure `main.tsx` imports `./extensions/componentRegistry.generated`");
58004
+ result.instructions.push("2. Ensure navigation seed data has matching `componentKey` values");
58005
+ result.instructions.push("3. DynamicRouter resolves routes automatically \u2014 no App.tsx wiring needed");
58006
+ }
57885
58007
  if (outputFormat === "applicationRoutes" || outputFormat === "clientRoutes") {
57886
58008
  const pageFiles = await discoverPageFiles(webPath, navRoutes);
57887
58009
  const applicationRoutesContent = generateApplicationRoutesConfig(navRoutes, pageFiles, includeGuards);
@@ -58646,6 +58768,41 @@ function generateApplicationRoutesConfig(routes, pageFiles, includeGuards) {
58646
58768
  lines.push("");
58647
58769
  return lines.join("\n");
58648
58770
  }
58771
+ function generateComponentRegistry(navRoutes, pageFiles, _webPath) {
58772
+ const lines = [];
58773
+ lines.push("/**");
58774
+ lines.push(" * Auto-generated by MCP scaffold_routes \u2014 DO NOT EDIT");
58775
+ lines.push(" * Registers all SmartStack pages into PageRegistry for DynamicRouter resolution.");
58776
+ lines.push(" */");
58777
+ lines.push("import { lazy } from 'react';");
58778
+ lines.push("import { PageRegistry } from '@/extensions/PageRegistry';");
58779
+ lines.push("");
58780
+ const routeTree = buildRouteTree(navRoutes);
58781
+ for (const [app, modules] of Object.entries(routeTree)) {
58782
+ lines.push(`// === ${app} ===`);
58783
+ for (const [, moduleRoutes] of Object.entries(modules)) {
58784
+ for (const route of moduleRoutes) {
58785
+ const discovery = pageFiles.get(route.navRoute);
58786
+ if (!discovery) continue;
58787
+ const componentKey = route.navRoute.split(".").map(toKebabCase).join(".");
58788
+ if (discovery.list) {
58789
+ lines.push(`PageRegistry.register('${componentKey}', lazy(() => import('${discovery.list.importPath}').then(m => ({ default: m.${discovery.list.componentName} }))));`);
58790
+ }
58791
+ if (discovery.detail) {
58792
+ lines.push(`PageRegistry.register('${componentKey}.detail', lazy(() => import('${discovery.detail.importPath}').then(m => ({ default: m.${discovery.detail.componentName} }))));`);
58793
+ }
58794
+ if (discovery.create) {
58795
+ lines.push(`PageRegistry.register('${componentKey}.create', lazy(() => import('${discovery.create.importPath}').then(m => ({ default: m.${discovery.create.componentName} }))));`);
58796
+ }
58797
+ if (discovery.edit) {
58798
+ lines.push(`PageRegistry.register('${componentKey}.edit', lazy(() => import('${discovery.edit.importPath}').then(m => ({ default: m.${discovery.edit.componentName} }))));`);
58799
+ }
58800
+ }
58801
+ }
58802
+ lines.push("");
58803
+ }
58804
+ return lines.join("\n");
58805
+ }
58649
58806
  function getLayoutName(_application) {
58650
58807
  return "AppLayout";
58651
58808
  }
@@ -58754,7 +58911,7 @@ and generates corresponding frontend routing infrastructure.`,
58754
58911
  includeLayouts: { type: "boolean", default: true },
58755
58912
  includeGuards: { type: "boolean", default: true },
58756
58913
  generateRegistry: { type: "boolean", default: true },
58757
- outputFormat: { type: "string", enum: ["standalone", "applicationRoutes", "clientRoutes"], default: "applicationRoutes", description: "standalone: createBrowserRouter(), applicationRoutes: RouteObject[] arrays for App.tsx (clientRoutes is a deprecated alias)" },
58914
+ outputFormat: { type: "string", enum: ["standalone", "applicationRoutes", "clientRoutes", "componentRegistry"], default: "componentRegistry", description: "componentRegistry (v3.7+): PageRegistry.register() calls for DynamicRouter. applicationRoutes (deprecated): RouteObject[] arrays for App.tsx. standalone: createBrowserRouter(). clientRoutes: deprecated alias for applicationRoutes." },
58758
58915
  dryRun: { type: "boolean", default: false }
58759
58916
  }
58760
58917
  }
@@ -58809,17 +58966,26 @@ async function validateFrontendRoutes2(input, config2) {
58809
58966
  if (scope === "all" || scope === "api-clients") {
58810
58967
  await validateApiClients(webPath, backendRoutes, result);
58811
58968
  }
58812
- if (scope === "all" || scope === "routes") {
58969
+ const componentRegistryPath = path20.join(webPath, "src", "extensions", "componentRegistry.generated.ts");
58970
+ const usesDynamicRouter = await fileExists(componentRegistryPath);
58971
+ if ((scope === "all" || scope === "routes") && !usesDynamicRouter) {
58813
58972
  await validateRoutes(webPath, backendRoutes, result);
58814
58973
  }
58815
- if (scope === "all" || scope === "routes") {
58974
+ if ((scope === "all" || scope === "routes") && !usesDynamicRouter) {
58816
58975
  await validateAppWiring(webPath, backendRoutes, result);
58817
58976
  }
58818
- if (scope === "all" || scope === "routes") {
58977
+ if ((scope === "all" || scope === "routes") && !usesDynamicRouter) {
58819
58978
  await validateCrudRoutes(webPath, backendRoutes, result);
58820
58979
  }
58980
+ if (scope === "all" || scope === "routes") {
58981
+ await validateComponentRegistry(webPath, backendRoutes, result);
58982
+ }
58821
58983
  generateRecommendations2(result);
58822
- 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;
58984
+ if (usesDynamicRouter) {
58985
+ result.valid = result.apiClients.issues.filter((i) => i.severity === "error").length === 0 && result.registry.exists && (!result.componentRegistry || result.componentRegistry.missingKeys.length === 0);
58986
+ } else {
58987
+ 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;
58988
+ }
58823
58989
  return result;
58824
58990
  }
58825
58991
  async function discoverBackendNavRoutes(structure) {
@@ -59084,6 +59250,13 @@ async function validateAppWiring(webPath, backendRoutes, result) {
59084
59250
  const hasRoutesImport = appContent.includes("from './routes") || appContent.includes("from '../routes");
59085
59251
  const hasInlineRoutes = appContent.includes("<Route ");
59086
59252
  result.appWiring.routesImported = hasApplicationRoutesImport || hasRoutesImport || hasInlineRoutes;
59253
+ const hasDynamicRouter = appContent.includes("DynamicRouter") || appContent.includes("<DynamicRouter");
59254
+ const hasComponentRegistry = appContent.includes("componentRegistry.generated");
59255
+ if (hasDynamicRouter || hasComponentRegistry) {
59256
+ result.appWiring.routesImported = true;
59257
+ result.appWiring.issues = [];
59258
+ return;
59259
+ }
59087
59260
  if (!result.appWiring.routesImported) {
59088
59261
  result.appWiring.issues.push("App.tsx does not import any route configuration");
59089
59262
  return;
@@ -59163,6 +59336,39 @@ async function validateCrudRoutes(webPath, backendRoutes, result) {
59163
59336
  }
59164
59337
  }
59165
59338
  }
59339
+ async function validateComponentRegistry(webPath, backendRoutes, result) {
59340
+ const registryPath = path20.join(webPath, "src", "extensions", "componentRegistry.generated.ts");
59341
+ if (!await fileExists(registryPath)) {
59342
+ result.componentRegistry = { exists: false, registeredKeys: [], missingKeys: [] };
59343
+ result.recommendations.push('Run `scaffold_routes outputFormat="componentRegistry"` to generate componentRegistry.generated.ts');
59344
+ return;
59345
+ }
59346
+ const content = await readText(registryPath);
59347
+ const registerCalls = content.matchAll(/PageRegistry\.register\s*\(\s*['"]([^'"]+)['"]/g);
59348
+ const registeredKeys = /* @__PURE__ */ new Set();
59349
+ for (const match2 of registerCalls) {
59350
+ registeredKeys.add(match2[1]);
59351
+ }
59352
+ const missingKeys = [];
59353
+ for (const route of backendRoutes) {
59354
+ const componentKey = route.navRoute.split(".").map(toKebabCase).join(".");
59355
+ if (!registeredKeys.has(componentKey)) {
59356
+ missingKeys.push(componentKey);
59357
+ }
59358
+ }
59359
+ result.componentRegistry = {
59360
+ exists: true,
59361
+ registeredKeys: [...registeredKeys],
59362
+ missingKeys
59363
+ };
59364
+ const mainPath = path20.join(webPath, "src", "main.tsx");
59365
+ if (await fileExists(mainPath)) {
59366
+ const mainContent = await readText(mainPath);
59367
+ if (!mainContent.includes("componentRegistry.generated")) {
59368
+ result.recommendations.push("main.tsx does not import componentRegistry.generated \u2014 pages will not be registered");
59369
+ }
59370
+ }
59371
+ }
59166
59372
  function generateRecommendations2(result) {
59167
59373
  if (!result.registry.exists) {
59168
59374
  result.recommendations.push('Run `scaffold_routes source="controllers"` to generate route registry');
@@ -59185,7 +59391,11 @@ function generateRecommendations2(result) {
59185
59391
  if (result.routes.orphaned.length > 0) {
59186
59392
  result.recommendations.push(`${result.routes.orphaned.length} frontend routes have no backend NavRoute`);
59187
59393
  }
59188
- if (result.appWiring && !result.appWiring.exists) {
59394
+ if (result.componentRegistry && !result.componentRegistry.exists) {
59395
+ result.recommendations.push('No componentRegistry.generated.ts found. Run `scaffold_routes outputFormat="componentRegistry"` to generate it.');
59396
+ } else if (result.componentRegistry && result.componentRegistry.missingKeys.length > 0) {
59397
+ result.recommendations.push(`${result.componentRegistry.missingKeys.length} backend routes missing from componentRegistry. Run \`scaffold_routes\` to regenerate.`);
59398
+ } else if (result.appWiring && !result.appWiring.exists) {
59189
59399
  result.recommendations.push("No App.tsx found. Create App.tsx with route configuration.");
59190
59400
  } else if (result.appWiring && result.appWiring.issues.length > 0) {
59191
59401
  const unwired = result.appWiring.issues.filter((i) => i.includes("not wired")).length;
@@ -59278,6 +59488,23 @@ function formatResult6(result, _input) {
59278
59488
  lines.push("");
59279
59489
  }
59280
59490
  }
59491
+ if (result.componentRegistry) {
59492
+ lines.push("## Component Registry");
59493
+ lines.push("");
59494
+ lines.push(`| Check | Status |`);
59495
+ lines.push(`|-------|--------|`);
59496
+ lines.push(`| Registry Exists | ${result.componentRegistry.exists ? "Yes" : "No"} |`);
59497
+ lines.push(`| Registered Keys | ${result.componentRegistry.registeredKeys.length} |`);
59498
+ lines.push(`| Missing Keys | ${result.componentRegistry.missingKeys.length} |`);
59499
+ lines.push("");
59500
+ if (result.componentRegistry.missingKeys.length > 0) {
59501
+ lines.push("### Missing PageRegistry Entries");
59502
+ for (const key of result.componentRegistry.missingKeys) {
59503
+ lines.push(`- \`${key}\``);
59504
+ }
59505
+ lines.push("");
59506
+ }
59507
+ }
59281
59508
  lines.push("## Recommendations");
59282
59509
  lines.push("");
59283
59510
  for (const rec of result.recommendations) {