@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.
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-entry.mjs +256 -101
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/mcp-scaffolding/frontend/api-client.ts.hbs +1 -0
- package/templates/skills/apex/references/agent-teams-protocol.md +28 -5
- package/templates/skills/apex/references/frontend-route-wiring-app-tsx.md +1 -1
- package/templates/skills/apex/steps/step-02-plan.md +132 -0
- package/templates/skills/apex/steps/step-03-execute.md +71 -0
- package/templates/skills/apex/steps/step-04-examine.md +21 -0
- package/templates/skills/apex/steps/step-05-deep-review.md +11 -0
- package/templates/skills/apex/steps/step-06-resolve.md +24 -5
- package/templates/skills/apex/steps/step-07-tests.md +11 -0
- package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +36 -4
- package/templates/skills/application/references/nav-fallback-procedure.md +6 -4
- package/templates/skills/application/templates-frontend.md +3 -3
- package/templates/skills/controller/steps/step-01-analyze.md +8 -2
- package/templates/skills/documentation/steps/step-03-validate.md +47 -16
- package/templates/mcp-scaffolding/frontend/nav-routes.ts.hbs +0 -133
- package/templates/mcp-scaffolding/frontend/routes.tsx.hbs +0 -126
package/dist/mcp-entry.mjs
CHANGED
|
@@ -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
|
|
27295
|
-
|
|
27296
|
-
const routePath =
|
|
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 '
|
|
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
|
-
|
|
57983
|
-
|
|
57984
|
-
|
|
57985
|
-
|
|
57986
|
-
|
|
57987
|
-
|
|
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
|
|
57993
|
-
|
|
57994
|
-
const navRoute =
|
|
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("
|
|
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 =
|
|
58365
|
-
const
|
|
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
|
-
|
|
58428
|
-
|
|
58429
|
-
|
|
58430
|
-
lines.push(
|
|
58431
|
-
}
|
|
58432
|
-
if (!discovery?.
|
|
58433
|
-
|
|
58434
|
-
|
|
58435
|
-
|
|
58436
|
-
lines.push(
|
|
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 =
|
|
58460
|
-
|
|
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 =
|
|
58467
|
-
|
|
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 =
|
|
58474
|
-
|
|
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 =
|
|
58481
|
-
|
|
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
|
|
58739
|
-
|
|
58740
|
-
const navRoute =
|
|
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(
|
|
58759
|
-
webPath: `/${navRoute.split(".").map(
|
|
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
|
|
58878
|
-
const
|
|
58879
|
-
|
|
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
|
-
|
|
58895
|
-
|
|
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.
|
|
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(
|
|
58979
|
-
const
|
|
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.
|