@atlashub/smartstack-cli 3.23.0 → 3.24.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.
- package/dist/mcp-entry.mjs +56 -15
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/mcp-scaffolding/component.tsx.hbs +21 -1
- package/templates/skills/apex/references/smartstack-api.md +31 -5
- package/templates/skills/apex/references/smartstack-frontend.md +1081 -0
- package/templates/skills/apex/references/smartstack-layers.md +81 -5
- package/templates/skills/apex/steps/step-01-analyze.md +27 -3
- package/templates/skills/apex/steps/step-02-plan.md +5 -1
- package/templates/skills/apex/steps/step-03-execute.md +43 -3
- package/templates/skills/apex/steps/step-04-validate.md +159 -0
- package/templates/skills/apex/steps/step-05-examine.md +7 -0
- package/templates/skills/apex/steps/step-07-tests.md +19 -0
- package/templates/skills/business-analyse/_shared.md +6 -6
- package/templates/skills/business-analyse/patterns/suggestion-catalog.md +1 -1
- package/templates/skills/business-analyse/questionnaire/07-ui.md +3 -3
- package/templates/skills/business-analyse/references/cadrage-pre-analysis.md +1 -1
- package/templates/skills/business-analyse/references/entity-architecture-decision.md +3 -3
- package/templates/skills/business-analyse/references/handoff-file-templates.md +13 -5
- package/templates/skills/business-analyse/references/spec-auto-inference.md +14 -14
- package/templates/skills/business-analyse/steps/step-01-cadrage.md +2 -2
- package/templates/skills/business-analyse/steps/step-02-decomposition.md +1 -1
- package/templates/skills/business-analyse/steps/step-03a1-setup.md +2 -2
- package/templates/skills/business-analyse/steps/step-03b-ui.md +2 -1
- package/templates/skills/business-analyse/steps/step-05a-handoff.md +15 -4
- package/templates/skills/business-analyse/templates/tpl-frd.md +2 -2
- package/templates/skills/business-analyse/templates-frd.md +2 -2
- package/templates/skills/ralph-loop/references/category-rules.md +45 -7
- package/templates/skills/ralph-loop/references/compact-loop.md +2 -2
- package/templates/skills/ralph-loop/references/core-seed-data.md +10 -0
- package/templates/skills/ralph-loop/steps/step-02-execute.md +110 -1
- package/templates/skills/validate-feature/steps/step-05-db-validation.md +86 -1
package/dist/mcp-entry.mjs
CHANGED
|
@@ -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,7 +57010,12 @@ 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("");
|
|
56986
57019
|
result.instructions.push("const contextRoutes: ContextRouteExtensions = {");
|
|
56987
57020
|
for (const [context, applications] of Object.entries(routeTree)) {
|
|
56988
57021
|
result.instructions.push(` ${context}: [`);
|
|
@@ -56991,7 +57024,7 @@ async function scaffoldRoutes(input, config2) {
|
|
|
56991
57024
|
const modulePath = route.navRoute.split(".").slice(1).join("/");
|
|
56992
57025
|
const pageEntry = pageFiles.get(route.navRoute);
|
|
56993
57026
|
const component = pageEntry?.[0]?.componentName || `${route.navRoute.split(".").map(capitalize).join("")}Page`;
|
|
56994
|
-
result.instructions.push(` { path: '${modulePath}', element:
|
|
57027
|
+
result.instructions.push(` { path: '${modulePath}', element: <Suspense fallback={<PageLoader />}><${component} /></Suspense> },`);
|
|
56995
57028
|
}
|
|
56996
57029
|
}
|
|
56997
57030
|
result.instructions.push(" ],");
|
|
@@ -57004,7 +57037,8 @@ async function scaffoldRoutes(input, config2) {
|
|
|
57004
57037
|
result.instructions.push("");
|
|
57005
57038
|
result.instructions.push('### Pattern B: JSX Routes (if App.tsx uses `<Route path="/{context}" element={<{Layout} />}>`)');
|
|
57006
57039
|
result.instructions.push("");
|
|
57007
|
-
result.instructions.push("Insert `<Route>` children INSIDE the appropriate Layout wrapper
|
|
57040
|
+
result.instructions.push("Insert `<Route>` children INSIDE the appropriate Layout wrapper.");
|
|
57041
|
+
result.instructions.push("**IMPORTANT:** Use `<Suspense fallback={<PageLoader />}>` for lazy-loaded pages.");
|
|
57008
57042
|
result.instructions.push("");
|
|
57009
57043
|
for (const [context, applications] of Object.entries(routeTree)) {
|
|
57010
57044
|
const layoutName = getLayoutName(context);
|
|
@@ -57016,7 +57050,7 @@ async function scaffoldRoutes(input, config2) {
|
|
|
57016
57050
|
const modulePath = route.navRoute.split(".").slice(1).join("/");
|
|
57017
57051
|
const pageEntry = pageFiles.get(route.navRoute);
|
|
57018
57052
|
const component = pageEntry?.[0]?.componentName || `${route.navRoute.split(".").map(capitalize).join("")}Page`;
|
|
57019
|
-
result.instructions.push(`<Route path="${modulePath}" element={
|
|
57053
|
+
result.instructions.push(`<Route path="${modulePath}" element={<Suspense fallback={<PageLoader />}><${component} /></Suspense>} />`);
|
|
57020
57054
|
}
|
|
57021
57055
|
}
|
|
57022
57056
|
result.instructions.push("```");
|
|
@@ -57255,27 +57289,30 @@ function generateRouterConfig(routes, includeGuards) {
|
|
|
57255
57289
|
" */",
|
|
57256
57290
|
"",
|
|
57257
57291
|
"import { createBrowserRouter, RouteObject } from 'react-router-dom';",
|
|
57258
|
-
"import {
|
|
57292
|
+
"import { lazy, Suspense } from 'react';",
|
|
57293
|
+
"import { ROUTES } from './navRoutes.generated';",
|
|
57294
|
+
"import { PageLoader } from '@/components/ui/PageLoader';"
|
|
57259
57295
|
];
|
|
57260
57296
|
if (includeGuards) {
|
|
57261
57297
|
lines.push("import { ProtectedRoute, PermissionGuard } from './guards';");
|
|
57262
57298
|
}
|
|
57263
57299
|
const contexts = Object.keys(routeTree);
|
|
57264
57300
|
for (const context of contexts) {
|
|
57265
|
-
|
|
57301
|
+
const name = `${capitalize(context)}Layout`;
|
|
57302
|
+
lines.push(`const ${name} = lazy(() => import('../layouts/${name}').then(m => ({ default: m.${name} })));`);
|
|
57266
57303
|
}
|
|
57267
57304
|
lines.push("");
|
|
57268
|
-
lines.push("// Page imports - customize
|
|
57305
|
+
lines.push("// Page imports - lazy loaded (customize paths)");
|
|
57269
57306
|
for (const route of routes) {
|
|
57270
57307
|
const pageName = route.navRoute.split(".").map(capitalize).join("");
|
|
57271
|
-
lines.push(`//
|
|
57308
|
+
lines.push(`// const ${pageName}Page = lazy(() => import('../pages/${pageName}Page').then(m => ({ default: m.${pageName}Page })));`);
|
|
57272
57309
|
}
|
|
57273
57310
|
lines.push("");
|
|
57274
57311
|
lines.push("const routes: RouteObject[] = [");
|
|
57275
57312
|
for (const [context, applications] of Object.entries(routeTree)) {
|
|
57276
57313
|
lines.push(" {");
|
|
57277
57314
|
lines.push(` path: '${context}',`);
|
|
57278
|
-
lines.push(` element:
|
|
57315
|
+
lines.push(` element: <Suspense fallback={<PageLoader />}><${capitalize(context)}Layout /></Suspense>,`);
|
|
57279
57316
|
lines.push(" children: [");
|
|
57280
57317
|
for (const [app, modules] of Object.entries(applications)) {
|
|
57281
57318
|
lines.push(" {");
|
|
@@ -57503,7 +57540,9 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
|
|
|
57503
57540
|
"",
|
|
57504
57541
|
"import type { RouteObject } from 'react-router-dom';",
|
|
57505
57542
|
"import type { ContextRouteExtensions } from '@atlashub/smartstack';",
|
|
57506
|
-
"import {
|
|
57543
|
+
"import { lazy, Suspense } from 'react';",
|
|
57544
|
+
"import { Navigate } from 'react-router-dom';",
|
|
57545
|
+
"import { PageLoader } from '@/components/ui/PageLoader';"
|
|
57507
57546
|
];
|
|
57508
57547
|
if (includeGuards) {
|
|
57509
57548
|
lines.push("import { ROUTES } from './navRoutes.generated';");
|
|
@@ -57516,7 +57555,9 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
|
|
|
57516
57555
|
if (pageEntries) {
|
|
57517
57556
|
for (const entry of pageEntries) {
|
|
57518
57557
|
if (!importedComponents.has(entry.componentName)) {
|
|
57519
|
-
lines.push(`
|
|
57558
|
+
lines.push(`const ${entry.componentName} = lazy(() =>`);
|
|
57559
|
+
lines.push(` import('${entry.importPath}').then(m => ({ default: m.${entry.componentName} }))`);
|
|
57560
|
+
lines.push(");");
|
|
57520
57561
|
importedComponents.add(entry.componentName);
|
|
57521
57562
|
}
|
|
57522
57563
|
}
|
|
@@ -57525,7 +57566,7 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
|
|
|
57525
57566
|
for (const route of routes) {
|
|
57526
57567
|
if (!pageFiles.has(route.navRoute)) {
|
|
57527
57568
|
const pageName = route.navRoute.split(".").map(capitalize).join("") + "Page";
|
|
57528
|
-
lines.push(`// TODO:
|
|
57569
|
+
lines.push(`// TODO: const ${pageName} = lazy(() => import('@/pages/...').then(m => ({ default: m.${pageName} })));`);
|
|
57529
57570
|
}
|
|
57530
57571
|
}
|
|
57531
57572
|
lines.push("");
|
|
@@ -57554,7 +57595,7 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
|
|
|
57554
57595
|
lines.push(` {`);
|
|
57555
57596
|
lines.push(` path: '${modulePath}',`);
|
|
57556
57597
|
if (hasRealPage) {
|
|
57557
|
-
lines.push(` element: <PermissionGuard permissions={ROUTES['${route.navRoute}'].permissions}><${component} /></PermissionGuard>,`);
|
|
57598
|
+
lines.push(` element: <Suspense fallback={<PageLoader />}><PermissionGuard permissions={ROUTES['${route.navRoute}'].permissions}><${component} /></PermissionGuard></Suspense>,`);
|
|
57558
57599
|
} else {
|
|
57559
57600
|
lines.push(` element: <PermissionGuard permissions={ROUTES['${route.navRoute}'].permissions}><div>TODO: ${component}</div></PermissionGuard>,`);
|
|
57560
57601
|
}
|
|
@@ -57562,7 +57603,7 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
|
|
|
57562
57603
|
} else {
|
|
57563
57604
|
lines.push(` {`);
|
|
57564
57605
|
lines.push(` path: '${modulePath}',`);
|
|
57565
|
-
lines.push(` element: ${hasRealPage ?
|
|
57606
|
+
lines.push(` element: ${hasRealPage ? `<Suspense fallback={<PageLoader />}><${component} /></Suspense>` : `<div>TODO: ${component}</div>`},`);
|
|
57566
57607
|
lines.push(` },`);
|
|
57567
57608
|
}
|
|
57568
57609
|
}
|
|
@@ -57579,7 +57620,7 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
|
|
|
57579
57620
|
lines.push(` {`);
|
|
57580
57621
|
lines.push(` path: '${fullPath}',`);
|
|
57581
57622
|
if (hasRealPage) {
|
|
57582
|
-
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>,`);
|
|
57583
57624
|
} else {
|
|
57584
57625
|
lines.push(` element: <PermissionGuard permissions={ROUTES['${route.navRoute}'].permissions}><div>TODO: ${component}</div></PermissionGuard>,`);
|
|
57585
57626
|
}
|
|
@@ -57587,7 +57628,7 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
|
|
|
57587
57628
|
} else {
|
|
57588
57629
|
lines.push(` {`);
|
|
57589
57630
|
lines.push(` path: '${fullPath}',`);
|
|
57590
|
-
lines.push(` element: ${hasRealPage ?
|
|
57631
|
+
lines.push(` element: ${hasRealPage ? `<Suspense fallback={<PageLoader />}><${component} /></Suspense>` : `<div>TODO: ${component}</div>`},`);
|
|
57591
57632
|
lines.push(` },`);
|
|
57592
57633
|
}
|
|
57593
57634
|
}
|