@atlashub/smartstack-cli 3.23.0 → 3.25.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 (36) hide show
  1. package/dist/index.js +5 -0
  2. package/dist/index.js.map +1 -1
  3. package/dist/mcp-entry.mjs +96 -24
  4. package/dist/mcp-entry.mjs.map +1 -1
  5. package/package.json +1 -1
  6. package/templates/mcp-scaffolding/component.tsx.hbs +21 -1
  7. package/templates/skills/apex/references/smartstack-api.md +174 -5
  8. package/templates/skills/apex/references/smartstack-frontend.md +1101 -0
  9. package/templates/skills/apex/references/smartstack-layers.md +81 -5
  10. package/templates/skills/apex/steps/step-01-analyze.md +27 -3
  11. package/templates/skills/apex/steps/step-02-plan.md +5 -1
  12. package/templates/skills/apex/steps/step-03-execute.md +47 -5
  13. package/templates/skills/apex/steps/step-04-validate.md +300 -0
  14. package/templates/skills/apex/steps/step-05-examine.md +7 -0
  15. package/templates/skills/apex/steps/step-07-tests.md +19 -0
  16. package/templates/skills/business-analyse/_shared.md +6 -6
  17. package/templates/skills/business-analyse/patterns/suggestion-catalog.md +1 -1
  18. package/templates/skills/business-analyse/questionnaire/07-ui.md +3 -3
  19. package/templates/skills/business-analyse/references/cadrage-pre-analysis.md +1 -1
  20. package/templates/skills/business-analyse/references/entity-architecture-decision.md +3 -3
  21. package/templates/skills/business-analyse/references/handoff-file-templates.md +13 -5
  22. package/templates/skills/business-analyse/references/spec-auto-inference.md +14 -14
  23. package/templates/skills/business-analyse/steps/step-01-cadrage.md +2 -2
  24. package/templates/skills/business-analyse/steps/step-02-decomposition.md +1 -1
  25. package/templates/skills/business-analyse/steps/step-03a1-setup.md +2 -2
  26. package/templates/skills/business-analyse/steps/step-03b-ui.md +2 -1
  27. package/templates/skills/business-analyse/steps/step-05a-handoff.md +15 -4
  28. package/templates/skills/business-analyse/templates/tpl-frd.md +2 -2
  29. package/templates/skills/business-analyse/templates-frd.md +2 -2
  30. package/templates/skills/efcore/steps/migration/step-02-create.md +14 -1
  31. package/templates/skills/ralph-loop/references/category-rules.md +71 -9
  32. package/templates/skills/ralph-loop/references/compact-loop.md +3 -3
  33. package/templates/skills/ralph-loop/references/core-seed-data.md +10 -0
  34. package/templates/skills/ralph-loop/steps/step-02-execute.md +190 -1
  35. package/templates/skills/validate-feature/steps/step-01-compile.md +4 -1
  36. package/templates/skills/validate-feature/steps/step-05-db-validation.md +86 -1
@@ -34500,6 +34500,8 @@ public interface I{{name}}Service
34500
34500
  const implementationTemplate = `using System.Threading;
34501
34501
  using System.Threading.Tasks;
34502
34502
  using System.Collections.Generic;
34503
+ using System.Linq;
34504
+ using Microsoft.EntityFrameworkCore;
34503
34505
  using Microsoft.Extensions.Logging;
34504
34506
  using SmartStack.Application.Common.Interfaces.Identity;
34505
34507
  using SmartStack.Application.Common.Interfaces.Persistence;
@@ -34509,6 +34511,7 @@ namespace {{implNamespace}};
34509
34511
  /// <summary>
34510
34512
  /// Service implementation for {{name}} operations.
34511
34513
  /// IMPORTANT: All queries MUST filter by _currentUser.TenantId for multi-tenant isolation.
34514
+ /// IMPORTANT: GetAllAsync MUST support search parameter for frontend EntityLookup component.
34512
34515
  /// </summary>
34513
34516
  public class {{name}}Service : I{{name}}Service
34514
34517
  {
@@ -34532,6 +34535,10 @@ public class {{name}}Service : I{{name}}Service
34532
34535
  {
34533
34536
  _logger.LogInformation("Executing {{this}} for tenant {TenantId}", _currentUser.TenantId);
34534
34537
  // TODO: Implement {{this}} \u2014 ALL queries must filter by _currentUser.TenantId
34538
+ // IMPORTANT: GetAllAsync MUST accept (string? search, int page, int pageSize) parameters
34539
+ // to enable EntityLookup search on the frontend. Example:
34540
+ // if (!string.IsNullOrWhiteSpace(search))
34541
+ // query = query.Where(x => x.Name.Contains(search) || x.Code.Contains(search));
34535
34542
  await Task.CompletedTask;
34536
34543
  throw new NotImplementedException();
34537
34544
  }
@@ -35344,6 +35351,27 @@ export function use{{name}}(options: Use{{name}}Options = {}) {
35344
35351
  const componentImportPath = hierarchy.context && hierarchy.module ? `@/components/${hierarchy.context.toLowerCase()}/${hierarchy.module.toLowerCase()}/${name}` : `./components/${name}`;
35345
35352
  result.instructions.push(`import { ${name} } from '${componentImportPath}';`);
35346
35353
  result.instructions.push(`import { use${name} } from '@/hooks/use${name}';`);
35354
+ result.instructions.push("");
35355
+ result.instructions.push("### IMPORTANT: Foreign Key Fields in Forms");
35356
+ result.instructions.push("If this entity has FK fields (e.g., EmployeeId, DepartmentId):");
35357
+ result.instructions.push("- NEVER render FK Guid fields as plain text inputs");
35358
+ result.instructions.push("- ALWAYS use EntityLookup component from @/components/ui/EntityLookup");
35359
+ result.instructions.push("- EntityLookup provides searchable dropdown with API-backed search");
35360
+ result.instructions.push("- See smartstack-frontend.md section 6 for the full EntityLookup pattern");
35361
+ result.instructions.push("");
35362
+ result.instructions.push("Example:");
35363
+ result.instructions.push("```tsx");
35364
+ result.instructions.push('import { EntityLookup } from "@/components/ui/EntityLookup";');
35365
+ result.instructions.push("");
35366
+ result.instructions.push("<EntityLookup");
35367
+ result.instructions.push(' apiEndpoint="/api/{context}/{app}/{related-entity}"');
35368
+ result.instructions.push(" value={formData.relatedEntityId}");
35369
+ result.instructions.push(' onChange={(id) => handleChange("relatedEntityId", id)}');
35370
+ result.instructions.push(' label="Related Entity"');
35371
+ result.instructions.push(" mapOption={(item) => ({ id: item.id, label: item.name, sublabel: item.code })}");
35372
+ result.instructions.push(" required");
35373
+ result.instructions.push("/>");
35374
+ result.instructions.push("```");
35347
35375
  }
35348
35376
  async function scaffoldTest(name, options, structure, config2, result, dryRun = false) {
35349
35377
  const isSystemEntity = options?.isSystemEntity || false;
@@ -56982,16 +57010,46 @@ async function scaffoldRoutes(input, config2) {
56982
57010
  result.instructions.push("");
56983
57011
  result.instructions.push("Add routes to `contextRoutes.{context}[]` with **RELATIVE** paths (no leading `/`):");
56984
57012
  result.instructions.push("");
57013
+ result.instructions.push("**IMPORTANT:** Pages are lazy-loaded. Use `<Suspense fallback={<PageLoader />}>` wrapper.");
57014
+ result.instructions.push("");
56985
57015
  result.instructions.push("```tsx");
57016
+ result.instructions.push("import { lazy, Suspense } from 'react';");
57017
+ result.instructions.push("import { PageLoader } from '@/components/ui/PageLoader';");
57018
+ result.instructions.push("");
57019
+ const importedComponents = /* @__PURE__ */ new Set();
57020
+ for (const [context, applications] of Object.entries(routeTree)) {
57021
+ for (const [, modules] of Object.entries(applications)) {
57022
+ for (const route of modules) {
57023
+ const pageEntry = pageFiles.get(route.navRoute);
57024
+ if (pageEntry) {
57025
+ for (const entry of pageEntry) {
57026
+ if (!importedComponents.has(entry.componentName)) {
57027
+ importedComponents.add(entry.componentName);
57028
+ result.instructions.push(`const ${entry.componentName} = lazy(() =>`);
57029
+ result.instructions.push(` import('${entry.importPath}').then(m => ({ default: m.${entry.componentName} }))`);
57030
+ result.instructions.push(");");
57031
+ }
57032
+ }
57033
+ } else {
57034
+ const component = route.navRoute.split(".").map(capitalize).join("") + "Page";
57035
+ if (!importedComponents.has(component)) {
57036
+ importedComponents.add(component);
57037
+ result.instructions.push(`// TODO: const ${component} = lazy(() => import('@/pages/...'));`);
57038
+ }
57039
+ }
57040
+ }
57041
+ }
57042
+ }
57043
+ result.instructions.push("");
56986
57044
  result.instructions.push("const contextRoutes: ContextRouteExtensions = {");
56987
57045
  for (const [context, applications] of Object.entries(routeTree)) {
56988
57046
  result.instructions.push(` ${context}: [`);
56989
57047
  for (const [, modules] of Object.entries(applications)) {
56990
57048
  for (const route of modules) {
56991
- const modulePath = route.navRoute.split(".").slice(1).join("/");
57049
+ const modulePath = route.navRoute.split(".").slice(1).map(toKebabCase).join("/");
56992
57050
  const pageEntry = pageFiles.get(route.navRoute);
56993
57051
  const component = pageEntry?.[0]?.componentName || `${route.navRoute.split(".").map(capitalize).join("")}Page`;
56994
- result.instructions.push(` { path: '${modulePath}', element: <${component} /> },`);
57052
+ result.instructions.push(` { path: '${modulePath}', element: <Suspense fallback={<PageLoader />}><${component} /></Suspense> },`);
56995
57053
  }
56996
57054
  }
56997
57055
  result.instructions.push(" ],");
@@ -57004,7 +57062,8 @@ async function scaffoldRoutes(input, config2) {
57004
57062
  result.instructions.push("");
57005
57063
  result.instructions.push('### Pattern B: JSX Routes (if App.tsx uses `<Route path="/{context}" element={<{Layout} />}>`)');
57006
57064
  result.instructions.push("");
57007
- result.instructions.push("Insert `<Route>` children INSIDE the appropriate Layout wrapper:");
57065
+ result.instructions.push("Insert `<Route>` children INSIDE the appropriate Layout wrapper.");
57066
+ result.instructions.push("**IMPORTANT:** Use `<Suspense fallback={<PageLoader />}>` for lazy-loaded pages.");
57008
57067
  result.instructions.push("");
57009
57068
  for (const [context, applications] of Object.entries(routeTree)) {
57010
57069
  const layoutName = getLayoutName(context);
@@ -57013,10 +57072,10 @@ async function scaffoldRoutes(input, config2) {
57013
57072
  result.instructions.push("```tsx");
57014
57073
  for (const [, modules] of Object.entries(applications)) {
57015
57074
  for (const route of modules) {
57016
- const modulePath = route.navRoute.split(".").slice(1).join("/");
57075
+ const modulePath = route.navRoute.split(".").slice(1).map(toKebabCase).join("/");
57017
57076
  const pageEntry = pageFiles.get(route.navRoute);
57018
57077
  const component = pageEntry?.[0]?.componentName || `${route.navRoute.split(".").map(capitalize).join("")}Page`;
57019
- result.instructions.push(`<Route path="${modulePath}" element={<${component} />} />`);
57078
+ result.instructions.push(`<Route path="${modulePath}" element={<Suspense fallback={<PageLoader />}><${component} /></Suspense>} />`);
57020
57079
  }
57021
57080
  }
57022
57081
  result.instructions.push("```");
@@ -57116,11 +57175,11 @@ async function discoverNavRoutes(structure, scope, warnings) {
57116
57175
  permissions.push(match2[1]);
57117
57176
  }
57118
57177
  const fullNavRoute = suffix ? `${navRoute}.${suffix}` : navRoute;
57119
- const expectedRoute = `api/${navRoute.replace(/\./g, "/")}${suffix ? `/${suffix}` : ""}`;
57178
+ const expectedRoute = `api/${navRouteToUrlPath(navRoute)}${suffix ? `/${toKebabCase(suffix)}` : ""}`;
57120
57179
  routes.push({
57121
57180
  navRoute: fullNavRoute,
57122
- apiPath: `/api/${navRoute.replace(/\./g, "/")}${suffix ? `/${suffix}` : ""}`,
57123
- webPath: `/${navRoute.replace(/\./g, "/")}${suffix ? `/${suffix}` : ""}`,
57181
+ apiPath: `/api/${navRouteToUrlPath(navRoute)}${suffix ? `/${toKebabCase(suffix)}` : ""}`,
57182
+ webPath: `/${navRouteToUrlPath(navRoute)}${suffix ? `/${toKebabCase(suffix)}` : ""}`,
57124
57183
  permissions,
57125
57184
  controller: controllerName,
57126
57185
  methods
@@ -57255,34 +57314,37 @@ function generateRouterConfig(routes, includeGuards) {
57255
57314
  " */",
57256
57315
  "",
57257
57316
  "import { createBrowserRouter, RouteObject } from 'react-router-dom';",
57258
- "import { ROUTES } from './navRoutes.generated';"
57317
+ "import { lazy, Suspense } from 'react';",
57318
+ "import { ROUTES } from './navRoutes.generated';",
57319
+ "import { PageLoader } from '@/components/ui/PageLoader';"
57259
57320
  ];
57260
57321
  if (includeGuards) {
57261
57322
  lines.push("import { ProtectedRoute, PermissionGuard } from './guards';");
57262
57323
  }
57263
57324
  const contexts = Object.keys(routeTree);
57264
57325
  for (const context of contexts) {
57265
- lines.push(`import { ${capitalize(context)}Layout } from '../layouts/${capitalize(context)}Layout';`);
57326
+ const name = `${capitalize(context)}Layout`;
57327
+ lines.push(`const ${name} = lazy(() => import('../layouts/${name}').then(m => ({ default: m.${name} })));`);
57266
57328
  }
57267
57329
  lines.push("");
57268
- lines.push("// Page imports - customize these paths");
57330
+ lines.push("// Page imports - lazy loaded (customize paths)");
57269
57331
  for (const route of routes) {
57270
57332
  const pageName = route.navRoute.split(".").map(capitalize).join("");
57271
- lines.push(`// import { ${pageName}Page } from '../pages/${pageName}Page';`);
57333
+ lines.push(`// const ${pageName}Page = lazy(() => import('../pages/${pageName}Page').then(m => ({ default: m.${pageName}Page })));`);
57272
57334
  }
57273
57335
  lines.push("");
57274
57336
  lines.push("const routes: RouteObject[] = [");
57275
57337
  for (const [context, applications] of Object.entries(routeTree)) {
57276
57338
  lines.push(" {");
57277
57339
  lines.push(` path: '${context}',`);
57278
- lines.push(` element: <${capitalize(context)}Layout />,`);
57340
+ lines.push(` element: <Suspense fallback={<PageLoader />}><${capitalize(context)}Layout /></Suspense>,`);
57279
57341
  lines.push(" children: [");
57280
57342
  for (const [app, modules] of Object.entries(applications)) {
57281
57343
  lines.push(" {");
57282
57344
  lines.push(` path: '${app}',`);
57283
57345
  lines.push(" children: [");
57284
57346
  for (const route of modules) {
57285
- const modulePath = route.navRoute.split(".").slice(2).join("/");
57347
+ const modulePath = route.navRoute.split(".").slice(2).map(toKebabCase).join("/");
57286
57348
  const pageName = route.navRoute.split(".").map(capitalize).join("");
57287
57349
  if (includeGuards && route.permissions.length > 0) {
57288
57350
  lines.push(" {");
@@ -57503,7 +57565,9 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
57503
57565
  "",
57504
57566
  "import type { RouteObject } from 'react-router-dom';",
57505
57567
  "import type { ContextRouteExtensions } from '@atlashub/smartstack';",
57506
- "import { Navigate } from 'react-router-dom';"
57568
+ "import { lazy, Suspense } from 'react';",
57569
+ "import { Navigate } from 'react-router-dom';",
57570
+ "import { PageLoader } from '@/components/ui/PageLoader';"
57507
57571
  ];
57508
57572
  if (includeGuards) {
57509
57573
  lines.push("import { ROUTES } from './navRoutes.generated';");
@@ -57516,7 +57580,9 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
57516
57580
  if (pageEntries) {
57517
57581
  for (const entry of pageEntries) {
57518
57582
  if (!importedComponents.has(entry.componentName)) {
57519
- lines.push(`import { ${entry.componentName} } from '${entry.importPath}';`);
57583
+ lines.push(`const ${entry.componentName} = lazy(() =>`);
57584
+ lines.push(` import('${entry.importPath}').then(m => ({ default: m.${entry.componentName} }))`);
57585
+ lines.push(");");
57520
57586
  importedComponents.add(entry.componentName);
57521
57587
  }
57522
57588
  }
@@ -57525,7 +57591,7 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
57525
57591
  for (const route of routes) {
57526
57592
  if (!pageFiles.has(route.navRoute)) {
57527
57593
  const pageName = route.navRoute.split(".").map(capitalize).join("") + "Page";
57528
- lines.push(`// TODO: import { ${pageName} } from '@/pages/...';`);
57594
+ lines.push(`// TODO: const ${pageName} = lazy(() => import('@/pages/...').then(m => ({ default: m.${pageName} })));`);
57529
57595
  }
57530
57596
  }
57531
57597
  lines.push("");
@@ -57542,10 +57608,10 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
57542
57608
  lines.push(" {");
57543
57609
  lines.push(` path: '${app}',`);
57544
57610
  lines.push(" children: [");
57545
- const firstModulePath = modules[0].navRoute.split(".").slice(2).join("/");
57611
+ const firstModulePath = modules[0].navRoute.split(".").slice(2).map(toKebabCase).join("/");
57546
57612
  lines.push(` { index: true, element: <Navigate to="${firstModulePath}" replace /> },`);
57547
57613
  for (const route of modules) {
57548
- const modulePath = route.navRoute.split(".").slice(2).join("/");
57614
+ const modulePath = route.navRoute.split(".").slice(2).map(toKebabCase).join("/");
57549
57615
  const pageName = route.navRoute.split(".").map(capitalize).join("") + "Page";
57550
57616
  const pageEntry = pageFiles.get(route.navRoute);
57551
57617
  const component = pageEntry?.[0]?.componentName || pageName;
@@ -57554,7 +57620,7 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
57554
57620
  lines.push(` {`);
57555
57621
  lines.push(` path: '${modulePath}',`);
57556
57622
  if (hasRealPage) {
57557
- lines.push(` element: <PermissionGuard permissions={ROUTES['${route.navRoute}'].permissions}><${component} /></PermissionGuard>,`);
57623
+ lines.push(` element: <Suspense fallback={<PageLoader />}><PermissionGuard permissions={ROUTES['${route.navRoute}'].permissions}><${component} /></PermissionGuard></Suspense>,`);
57558
57624
  } else {
57559
57625
  lines.push(` element: <PermissionGuard permissions={ROUTES['${route.navRoute}'].permissions}><div>TODO: ${component}</div></PermissionGuard>,`);
57560
57626
  }
@@ -57562,7 +57628,7 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
57562
57628
  } else {
57563
57629
  lines.push(` {`);
57564
57630
  lines.push(` path: '${modulePath}',`);
57565
- lines.push(` element: ${hasRealPage ? `<${component} />` : `<div>TODO: ${component}</div>`},`);
57631
+ lines.push(` element: ${hasRealPage ? `<Suspense fallback={<PageLoader />}><${component} /></Suspense>` : `<div>TODO: ${component}</div>`},`);
57566
57632
  lines.push(` },`);
57567
57633
  }
57568
57634
  }
@@ -57570,7 +57636,7 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
57570
57636
  lines.push(" },");
57571
57637
  } else {
57572
57638
  for (const route of modules) {
57573
- const fullPath = route.navRoute.split(".").slice(1).join("/");
57639
+ const fullPath = route.navRoute.split(".").slice(1).map(toKebabCase).join("/");
57574
57640
  const pageName = route.navRoute.split(".").map(capitalize).join("") + "Page";
57575
57641
  const pageEntry = pageFiles.get(route.navRoute);
57576
57642
  const component = pageEntry?.[0]?.componentName || pageName;
@@ -57579,7 +57645,7 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
57579
57645
  lines.push(` {`);
57580
57646
  lines.push(` path: '${fullPath}',`);
57581
57647
  if (hasRealPage) {
57582
- lines.push(` element: <PermissionGuard permissions={ROUTES['${route.navRoute}'].permissions}><${component} /></PermissionGuard>,`);
57648
+ lines.push(` element: <Suspense fallback={<PageLoader />}><PermissionGuard permissions={ROUTES['${route.navRoute}'].permissions}><${component} /></PermissionGuard></Suspense>,`);
57583
57649
  } else {
57584
57650
  lines.push(` element: <PermissionGuard permissions={ROUTES['${route.navRoute}'].permissions}><div>TODO: ${component}</div></PermissionGuard>,`);
57585
57651
  }
@@ -57587,7 +57653,7 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
57587
57653
  } else {
57588
57654
  lines.push(` {`);
57589
57655
  lines.push(` path: '${fullPath}',`);
57590
- lines.push(` element: ${hasRealPage ? `<${component} />` : `<div>TODO: ${component}</div>`},`);
57656
+ lines.push(` element: ${hasRealPage ? `<Suspense fallback={<PageLoader />}><${component} /></Suspense>` : `<div>TODO: ${component}</div>`},`);
57591
57657
  lines.push(` },`);
57592
57658
  }
57593
57659
  }
@@ -57632,6 +57698,12 @@ function getLayoutName(context) {
57632
57698
  };
57633
57699
  return layoutMap[context] || `${capitalize(context)}Layout`;
57634
57700
  }
57701
+ function toKebabCase(segment) {
57702
+ return segment.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
57703
+ }
57704
+ function navRouteToUrlPath(navRoute) {
57705
+ return navRoute.split(".").map(toKebabCase).join("/");
57706
+ }
57635
57707
  function capitalize(str) {
57636
57708
  return str.charAt(0).toUpperCase() + str.slice(1);
57637
57709
  }