@atlashub/smartstack-mcp 1.5.1 → 1.6.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 +1310 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/frontend/api-client.ts.hbs +116 -0
- package/templates/frontend/nav-routes.ts.hbs +133 -0
- package/templates/frontend/routes.tsx.hbs +134 -0
package/dist/index.js
CHANGED
|
@@ -82,10 +82,10 @@ import { stat, mkdir, readFile, writeFile, cp, rm } from "fs/promises";
|
|
|
82
82
|
import path from "path";
|
|
83
83
|
import { glob } from "glob";
|
|
84
84
|
var FileSystemError = class extends Error {
|
|
85
|
-
constructor(message, operation,
|
|
85
|
+
constructor(message, operation, path21, cause) {
|
|
86
86
|
super(message);
|
|
87
87
|
this.operation = operation;
|
|
88
|
-
this.path =
|
|
88
|
+
this.path = path21;
|
|
89
89
|
this.cause = cause;
|
|
90
90
|
this.name = "FileSystemError";
|
|
91
91
|
}
|
|
@@ -491,6 +491,35 @@ var SuggestTestScenariosInputSchema = z.object({
|
|
|
491
491
|
name: z.string().min(1).describe("Component name or file path"),
|
|
492
492
|
depth: z.enum(["basic", "comprehensive", "security-focused"]).default("comprehensive").describe("Depth of analysis")
|
|
493
493
|
});
|
|
494
|
+
var ScaffoldApiClientInputSchema = z.object({
|
|
495
|
+
navRoute: z.string().min(1).describe('NavRoute path (e.g., "platform.administration.users")'),
|
|
496
|
+
name: z.string().min(1).describe('Entity name in PascalCase (e.g., "User", "Order")'),
|
|
497
|
+
methods: z.array(z.enum(["getAll", "getById", "create", "update", "delete", "search", "export"])).default(["getAll", "getById", "create", "update", "delete"]).describe("API methods to generate"),
|
|
498
|
+
options: z.object({
|
|
499
|
+
outputPath: z.string().optional().describe("Custom output path for generated files"),
|
|
500
|
+
includeTypes: z.boolean().default(true).describe("Generate TypeScript types"),
|
|
501
|
+
includeHook: z.boolean().default(true).describe("Generate React Query hook"),
|
|
502
|
+
dryRun: z.boolean().default(false).describe("Preview without writing files")
|
|
503
|
+
}).optional()
|
|
504
|
+
});
|
|
505
|
+
var ScaffoldRoutesInputSchema = z.object({
|
|
506
|
+
source: z.enum(["controllers", "navigation", "manual"]).default("controllers").describe("Source for route discovery: controllers (scan NavRoute attributes), navigation (from DB), manual (from config)"),
|
|
507
|
+
scope: z.enum(["all", "platform", "business", "extensions"]).default("all").describe("Scope of routes to generate"),
|
|
508
|
+
options: z.object({
|
|
509
|
+
outputPath: z.string().optional().describe("Custom output path"),
|
|
510
|
+
includeLayouts: z.boolean().default(true).describe("Generate layout components"),
|
|
511
|
+
includeGuards: z.boolean().default(true).describe("Include route guards for permissions"),
|
|
512
|
+
generateRegistry: z.boolean().default(true).describe("Generate navRoutes.generated.ts"),
|
|
513
|
+
dryRun: z.boolean().default(false).describe("Preview without writing files")
|
|
514
|
+
}).optional()
|
|
515
|
+
});
|
|
516
|
+
var ValidateFrontendRoutesInputSchema = z.object({
|
|
517
|
+
scope: z.enum(["api-clients", "routes", "registry", "all"]).default("all").describe("Scope of validation"),
|
|
518
|
+
options: z.object({
|
|
519
|
+
fix: z.boolean().default(false).describe("Auto-fix minor issues"),
|
|
520
|
+
strict: z.boolean().default(false).describe("Fail on warnings")
|
|
521
|
+
}).optional()
|
|
522
|
+
});
|
|
494
523
|
|
|
495
524
|
// src/lib/detector.ts
|
|
496
525
|
import path4 from "path";
|
|
@@ -6321,6 +6350,1253 @@ function getTypeEmoji(type) {
|
|
|
6321
6350
|
}
|
|
6322
6351
|
}
|
|
6323
6352
|
|
|
6353
|
+
// src/tools/scaffold-api-client.ts
|
|
6354
|
+
import path14 from "path";
|
|
6355
|
+
var scaffoldApiClientTool = {
|
|
6356
|
+
name: "scaffold_api_client",
|
|
6357
|
+
description: `Generate TypeScript API client with NavRoute integration.
|
|
6358
|
+
|
|
6359
|
+
Creates:
|
|
6360
|
+
- Type-safe API service with CRUD methods
|
|
6361
|
+
- TypeScript interfaces for request/response
|
|
6362
|
+
- React Query hook (optional)
|
|
6363
|
+
- Integration with navRoutes.generated.ts registry
|
|
6364
|
+
|
|
6365
|
+
Example:
|
|
6366
|
+
scaffold_api_client navRoute="platform.administration.users" name="User"
|
|
6367
|
+
|
|
6368
|
+
The generated client automatically resolves the API path from the NavRoute registry,
|
|
6369
|
+
ensuring frontend routes stay synchronized with backend NavRoute attributes.`,
|
|
6370
|
+
inputSchema: {
|
|
6371
|
+
type: "object",
|
|
6372
|
+
properties: {
|
|
6373
|
+
navRoute: {
|
|
6374
|
+
type: "string",
|
|
6375
|
+
description: 'NavRoute path (e.g., "platform.administration.users")'
|
|
6376
|
+
},
|
|
6377
|
+
name: {
|
|
6378
|
+
type: "string",
|
|
6379
|
+
description: 'Entity name in PascalCase (e.g., "User", "Order")'
|
|
6380
|
+
},
|
|
6381
|
+
methods: {
|
|
6382
|
+
type: "array",
|
|
6383
|
+
items: {
|
|
6384
|
+
type: "string",
|
|
6385
|
+
enum: ["getAll", "getById", "create", "update", "delete", "search", "export"]
|
|
6386
|
+
},
|
|
6387
|
+
default: ["getAll", "getById", "create", "update", "delete"],
|
|
6388
|
+
description: "API methods to generate"
|
|
6389
|
+
},
|
|
6390
|
+
options: {
|
|
6391
|
+
type: "object",
|
|
6392
|
+
properties: {
|
|
6393
|
+
outputPath: { type: "string", description: "Custom output path" },
|
|
6394
|
+
includeTypes: { type: "boolean", default: true, description: "Generate TypeScript types" },
|
|
6395
|
+
includeHook: { type: "boolean", default: true, description: "Generate React Query hook" },
|
|
6396
|
+
dryRun: { type: "boolean", default: false, description: "Preview without writing" }
|
|
6397
|
+
}
|
|
6398
|
+
}
|
|
6399
|
+
},
|
|
6400
|
+
required: ["navRoute", "name"]
|
|
6401
|
+
}
|
|
6402
|
+
};
|
|
6403
|
+
async function handleScaffoldApiClient(args, config) {
|
|
6404
|
+
const input = ScaffoldApiClientInputSchema.parse(args);
|
|
6405
|
+
logger.info("Scaffolding API client", { navRoute: input.navRoute, name: input.name });
|
|
6406
|
+
const result = await scaffoldApiClient(input, config);
|
|
6407
|
+
return formatResult4(result, input);
|
|
6408
|
+
}
|
|
6409
|
+
async function scaffoldApiClient(input, config) {
|
|
6410
|
+
const result = {
|
|
6411
|
+
success: true,
|
|
6412
|
+
files: [],
|
|
6413
|
+
instructions: []
|
|
6414
|
+
};
|
|
6415
|
+
const { navRoute, name, methods, options } = input;
|
|
6416
|
+
const dryRun = options?.dryRun ?? false;
|
|
6417
|
+
const includeTypes = options?.includeTypes ?? true;
|
|
6418
|
+
const includeHook = options?.includeHook ?? true;
|
|
6419
|
+
const nameLower = name.charAt(0).toLowerCase() + name.slice(1);
|
|
6420
|
+
const apiPath = navRouteToApiPath(navRoute);
|
|
6421
|
+
const projectRoot = config.smartstack.projectPath;
|
|
6422
|
+
const structure = await findSmartStackStructure(projectRoot);
|
|
6423
|
+
const webPath = structure.web || path14.join(projectRoot, "web", "smartstack-web");
|
|
6424
|
+
const servicesPath = options?.outputPath || path14.join(webPath, "src", "services", "api");
|
|
6425
|
+
const hooksPath = path14.join(webPath, "src", "hooks");
|
|
6426
|
+
const typesPath = path14.join(webPath, "src", "types");
|
|
6427
|
+
const apiClientContent = generateApiClient(name, nameLower, navRoute, apiPath, methods);
|
|
6428
|
+
const apiClientFile = path14.join(servicesPath, `${nameLower}.ts`);
|
|
6429
|
+
if (!dryRun) {
|
|
6430
|
+
await ensureDirectory(servicesPath);
|
|
6431
|
+
await writeText(apiClientFile, apiClientContent);
|
|
6432
|
+
}
|
|
6433
|
+
result.files.push({ path: apiClientFile, content: apiClientContent, type: "created" });
|
|
6434
|
+
if (includeTypes) {
|
|
6435
|
+
const typesContent = generateTypes(name);
|
|
6436
|
+
const typesFile = path14.join(typesPath, `${nameLower}.ts`);
|
|
6437
|
+
if (!dryRun) {
|
|
6438
|
+
await ensureDirectory(typesPath);
|
|
6439
|
+
await writeText(typesFile, typesContent);
|
|
6440
|
+
}
|
|
6441
|
+
result.files.push({ path: typesFile, content: typesContent, type: "created" });
|
|
6442
|
+
}
|
|
6443
|
+
if (includeHook) {
|
|
6444
|
+
const hookContent = generateHook(name, nameLower, methods);
|
|
6445
|
+
const hookFile = path14.join(hooksPath, `use${name}.ts`);
|
|
6446
|
+
if (!dryRun) {
|
|
6447
|
+
await ensureDirectory(hooksPath);
|
|
6448
|
+
await writeText(hookFile, hookContent);
|
|
6449
|
+
}
|
|
6450
|
+
result.files.push({ path: hookFile, content: hookContent, type: "created" });
|
|
6451
|
+
}
|
|
6452
|
+
result.instructions.push(`Import the API client: import { ${nameLower}Api } from './services/api/${nameLower}';`);
|
|
6453
|
+
if (includeHook) {
|
|
6454
|
+
result.instructions.push(`Import the hook: import { use${name}, use${name}List } from './hooks/use${name}';`);
|
|
6455
|
+
}
|
|
6456
|
+
result.instructions.push(`Ensure navRoutes.generated.ts includes route: "${navRoute}"`);
|
|
6457
|
+
return result;
|
|
6458
|
+
}
|
|
6459
|
+
function navRouteToApiPath(navRoute) {
|
|
6460
|
+
return `/api/${navRoute.replace(/\./g, "/")}`;
|
|
6461
|
+
}
|
|
6462
|
+
function generateApiClient(name, nameLower, navRoute, apiPath, methods) {
|
|
6463
|
+
const template = `/**
|
|
6464
|
+
* ${name} API Client
|
|
6465
|
+
*
|
|
6466
|
+
* Auto-generated by SmartStack MCP - DO NOT EDIT MANUALLY
|
|
6467
|
+
* NavRoute: ${navRoute}
|
|
6468
|
+
* API Path: ${apiPath}
|
|
6469
|
+
*/
|
|
6470
|
+
|
|
6471
|
+
import { getRoute } from '../routes/navRoutes.generated';
|
|
6472
|
+
import { apiClient } from '../lib/apiClient';
|
|
6473
|
+
import type {
|
|
6474
|
+
${name},
|
|
6475
|
+
${name}CreateRequest,
|
|
6476
|
+
${name}UpdateRequest,
|
|
6477
|
+
${name}ListResponse,
|
|
6478
|
+
PaginatedRequest,
|
|
6479
|
+
PaginatedResponse
|
|
6480
|
+
} from '../types/${nameLower}';
|
|
6481
|
+
|
|
6482
|
+
const ROUTE = getRoute('${navRoute}');
|
|
6483
|
+
|
|
6484
|
+
export const ${nameLower}Api = {
|
|
6485
|
+
${methods.includes("getAll") ? ` /**
|
|
6486
|
+
* Get all ${name}s with pagination
|
|
6487
|
+
*/
|
|
6488
|
+
async getAll(params?: PaginatedRequest): Promise<PaginatedResponse<${name}>> {
|
|
6489
|
+
const response = await apiClient.get<${name}ListResponse>(ROUTE.api, { params });
|
|
6490
|
+
return response.data;
|
|
6491
|
+
},
|
|
6492
|
+
` : ""}
|
|
6493
|
+
${methods.includes("getById") ? ` /**
|
|
6494
|
+
* Get ${name} by ID
|
|
6495
|
+
*/
|
|
6496
|
+
async getById(id: string): Promise<${name}> {
|
|
6497
|
+
const response = await apiClient.get<${name}>(\`\${ROUTE.api}/\${id}\`);
|
|
6498
|
+
return response.data;
|
|
6499
|
+
},
|
|
6500
|
+
` : ""}
|
|
6501
|
+
${methods.includes("create") ? ` /**
|
|
6502
|
+
* Create new ${name}
|
|
6503
|
+
*/
|
|
6504
|
+
async create(data: ${name}CreateRequest): Promise<${name}> {
|
|
6505
|
+
const response = await apiClient.post<${name}>(ROUTE.api, data);
|
|
6506
|
+
return response.data;
|
|
6507
|
+
},
|
|
6508
|
+
` : ""}
|
|
6509
|
+
${methods.includes("update") ? ` /**
|
|
6510
|
+
* Update existing ${name}
|
|
6511
|
+
*/
|
|
6512
|
+
async update(id: string, data: ${name}UpdateRequest): Promise<${name}> {
|
|
6513
|
+
const response = await apiClient.put<${name}>(\`\${ROUTE.api}/\${id}\`, data);
|
|
6514
|
+
return response.data;
|
|
6515
|
+
},
|
|
6516
|
+
` : ""}
|
|
6517
|
+
${methods.includes("delete") ? ` /**
|
|
6518
|
+
* Delete ${name}
|
|
6519
|
+
*/
|
|
6520
|
+
async delete(id: string): Promise<void> {
|
|
6521
|
+
await apiClient.delete(\`\${ROUTE.api}/\${id}\`);
|
|
6522
|
+
},
|
|
6523
|
+
` : ""}
|
|
6524
|
+
${methods.includes("search") ? ` /**
|
|
6525
|
+
* Search ${name}s
|
|
6526
|
+
*/
|
|
6527
|
+
async search(query: string, params?: PaginatedRequest): Promise<PaginatedResponse<${name}>> {
|
|
6528
|
+
const response = await apiClient.get<${name}ListResponse>(\`\${ROUTE.api}/search\`, {
|
|
6529
|
+
params: { q: query, ...params }
|
|
6530
|
+
});
|
|
6531
|
+
return response.data;
|
|
6532
|
+
},
|
|
6533
|
+
` : ""}
|
|
6534
|
+
${methods.includes("export") ? ` /**
|
|
6535
|
+
* Export ${name}s to file
|
|
6536
|
+
*/
|
|
6537
|
+
async export(format: 'csv' | 'xlsx' | 'pdf' = 'xlsx'): Promise<Blob> {
|
|
6538
|
+
const response = await apiClient.get(\`\${ROUTE.api}/export\`, {
|
|
6539
|
+
params: { format },
|
|
6540
|
+
responseType: 'blob'
|
|
6541
|
+
});
|
|
6542
|
+
return response.data;
|
|
6543
|
+
},
|
|
6544
|
+
` : ""}
|
|
6545
|
+
/**
|
|
6546
|
+
* Get the NavRoute for this API
|
|
6547
|
+
*/
|
|
6548
|
+
getRoute() {
|
|
6549
|
+
return ROUTE;
|
|
6550
|
+
},
|
|
6551
|
+
};
|
|
6552
|
+
|
|
6553
|
+
export default ${nameLower}Api;
|
|
6554
|
+
`;
|
|
6555
|
+
return template;
|
|
6556
|
+
}
|
|
6557
|
+
function generateTypes(name) {
|
|
6558
|
+
return `/**
|
|
6559
|
+
* ${name} Types
|
|
6560
|
+
*
|
|
6561
|
+
* Auto-generated by SmartStack MCP - Customize as needed
|
|
6562
|
+
*/
|
|
6563
|
+
|
|
6564
|
+
export interface ${name} {
|
|
6565
|
+
id: string;
|
|
6566
|
+
code: string;
|
|
6567
|
+
name?: string;
|
|
6568
|
+
description?: string;
|
|
6569
|
+
isActive: boolean;
|
|
6570
|
+
createdAt: string;
|
|
6571
|
+
createdBy: string;
|
|
6572
|
+
updatedAt?: string;
|
|
6573
|
+
updatedBy?: string;
|
|
6574
|
+
}
|
|
6575
|
+
|
|
6576
|
+
export interface ${name}CreateRequest {
|
|
6577
|
+
code: string;
|
|
6578
|
+
name?: string;
|
|
6579
|
+
description?: string;
|
|
6580
|
+
}
|
|
6581
|
+
|
|
6582
|
+
export interface ${name}UpdateRequest {
|
|
6583
|
+
code?: string;
|
|
6584
|
+
name?: string;
|
|
6585
|
+
description?: string;
|
|
6586
|
+
isActive?: boolean;
|
|
6587
|
+
}
|
|
6588
|
+
|
|
6589
|
+
export interface ${name}ListResponse {
|
|
6590
|
+
items: ${name}[];
|
|
6591
|
+
totalCount: number;
|
|
6592
|
+
pageSize: number;
|
|
6593
|
+
currentPage: number;
|
|
6594
|
+
totalPages: number;
|
|
6595
|
+
}
|
|
6596
|
+
|
|
6597
|
+
export interface PaginatedRequest {
|
|
6598
|
+
page?: number;
|
|
6599
|
+
pageSize?: number;
|
|
6600
|
+
sortBy?: string;
|
|
6601
|
+
sortDirection?: 'asc' | 'desc';
|
|
6602
|
+
filter?: string;
|
|
6603
|
+
}
|
|
6604
|
+
|
|
6605
|
+
export interface PaginatedResponse<T> {
|
|
6606
|
+
items: T[];
|
|
6607
|
+
totalCount: number;
|
|
6608
|
+
pageSize: number;
|
|
6609
|
+
currentPage: number;
|
|
6610
|
+
totalPages: number;
|
|
6611
|
+
}
|
|
6612
|
+
`;
|
|
6613
|
+
}
|
|
6614
|
+
function generateHook(name, nameLower, methods) {
|
|
6615
|
+
return `/**
|
|
6616
|
+
* ${name} React Query Hooks
|
|
6617
|
+
*
|
|
6618
|
+
* Auto-generated by SmartStack MCP - DO NOT EDIT MANUALLY
|
|
6619
|
+
*/
|
|
6620
|
+
|
|
6621
|
+
import { useQuery, useMutation, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
|
|
6622
|
+
import { ${nameLower}Api } from '../services/api/${nameLower}';
|
|
6623
|
+
import type { ${name}, ${name}CreateRequest, ${name}UpdateRequest, PaginatedRequest, PaginatedResponse } from '../types/${nameLower}';
|
|
6624
|
+
|
|
6625
|
+
const QUERY_KEY = '${nameLower}s';
|
|
6626
|
+
|
|
6627
|
+
${methods.includes("getAll") ? `/**
|
|
6628
|
+
* Hook to fetch paginated ${name} list
|
|
6629
|
+
*/
|
|
6630
|
+
export function use${name}List(
|
|
6631
|
+
params?: PaginatedRequest,
|
|
6632
|
+
options?: Omit<UseQueryOptions<PaginatedResponse<${name}>>, 'queryKey' | 'queryFn'>
|
|
6633
|
+
) {
|
|
6634
|
+
return useQuery({
|
|
6635
|
+
queryKey: [QUERY_KEY, 'list', params],
|
|
6636
|
+
queryFn: () => ${nameLower}Api.getAll(params),
|
|
6637
|
+
...options,
|
|
6638
|
+
});
|
|
6639
|
+
}
|
|
6640
|
+
` : ""}
|
|
6641
|
+
${methods.includes("getById") ? `/**
|
|
6642
|
+
* Hook to fetch single ${name} by ID
|
|
6643
|
+
*/
|
|
6644
|
+
export function use${name}(
|
|
6645
|
+
id: string | undefined,
|
|
6646
|
+
options?: Omit<UseQueryOptions<${name}>, 'queryKey' | 'queryFn'>
|
|
6647
|
+
) {
|
|
6648
|
+
return useQuery({
|
|
6649
|
+
queryKey: [QUERY_KEY, 'detail', id],
|
|
6650
|
+
queryFn: () => ${nameLower}Api.getById(id!),
|
|
6651
|
+
enabled: !!id,
|
|
6652
|
+
...options,
|
|
6653
|
+
});
|
|
6654
|
+
}
|
|
6655
|
+
` : ""}
|
|
6656
|
+
${methods.includes("create") ? `/**
|
|
6657
|
+
* Hook to create new ${name}
|
|
6658
|
+
*/
|
|
6659
|
+
export function use${name}Create(
|
|
6660
|
+
options?: UseMutationOptions<${name}, Error, ${name}CreateRequest>
|
|
6661
|
+
) {
|
|
6662
|
+
const queryClient = useQueryClient();
|
|
6663
|
+
|
|
6664
|
+
return useMutation({
|
|
6665
|
+
mutationFn: (data: ${name}CreateRequest) => ${nameLower}Api.create(data),
|
|
6666
|
+
onSuccess: () => {
|
|
6667
|
+
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
|
6668
|
+
},
|
|
6669
|
+
...options,
|
|
6670
|
+
});
|
|
6671
|
+
}
|
|
6672
|
+
` : ""}
|
|
6673
|
+
${methods.includes("update") ? `/**
|
|
6674
|
+
* Hook to update existing ${name}
|
|
6675
|
+
*/
|
|
6676
|
+
export function use${name}Update(
|
|
6677
|
+
options?: UseMutationOptions<${name}, Error, { id: string; data: ${name}UpdateRequest }>
|
|
6678
|
+
) {
|
|
6679
|
+
const queryClient = useQueryClient();
|
|
6680
|
+
|
|
6681
|
+
return useMutation({
|
|
6682
|
+
mutationFn: ({ id, data }) => ${nameLower}Api.update(id, data),
|
|
6683
|
+
onSuccess: (_, { id }) => {
|
|
6684
|
+
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
|
6685
|
+
queryClient.invalidateQueries({ queryKey: [QUERY_KEY, 'detail', id] });
|
|
6686
|
+
},
|
|
6687
|
+
...options,
|
|
6688
|
+
});
|
|
6689
|
+
}
|
|
6690
|
+
` : ""}
|
|
6691
|
+
${methods.includes("delete") ? `/**
|
|
6692
|
+
* Hook to delete ${name}
|
|
6693
|
+
*/
|
|
6694
|
+
export function use${name}Delete(
|
|
6695
|
+
options?: UseMutationOptions<void, Error, string>
|
|
6696
|
+
) {
|
|
6697
|
+
const queryClient = useQueryClient();
|
|
6698
|
+
|
|
6699
|
+
return useMutation({
|
|
6700
|
+
mutationFn: (id: string) => ${nameLower}Api.delete(id),
|
|
6701
|
+
onSuccess: () => {
|
|
6702
|
+
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
|
6703
|
+
},
|
|
6704
|
+
...options,
|
|
6705
|
+
});
|
|
6706
|
+
}
|
|
6707
|
+
` : ""}
|
|
6708
|
+
${methods.includes("search") ? `/**
|
|
6709
|
+
* Hook to search ${name}s
|
|
6710
|
+
*/
|
|
6711
|
+
export function use${name}Search(
|
|
6712
|
+
query: string,
|
|
6713
|
+
params?: PaginatedRequest,
|
|
6714
|
+
options?: Omit<UseQueryOptions<PaginatedResponse<${name}>>, 'queryKey' | 'queryFn'>
|
|
6715
|
+
) {
|
|
6716
|
+
return useQuery({
|
|
6717
|
+
queryKey: [QUERY_KEY, 'search', query, params],
|
|
6718
|
+
queryFn: () => ${nameLower}Api.search(query, params),
|
|
6719
|
+
enabled: query.length >= 2,
|
|
6720
|
+
...options,
|
|
6721
|
+
});
|
|
6722
|
+
}
|
|
6723
|
+
` : ""}
|
|
6724
|
+
`;
|
|
6725
|
+
}
|
|
6726
|
+
function formatResult4(result, input) {
|
|
6727
|
+
const lines = [];
|
|
6728
|
+
lines.push(`# Scaffold API Client: ${input.name}`);
|
|
6729
|
+
lines.push("");
|
|
6730
|
+
if (input.options?.dryRun) {
|
|
6731
|
+
lines.push("> **DRY RUN** - No files were written");
|
|
6732
|
+
lines.push("");
|
|
6733
|
+
}
|
|
6734
|
+
lines.push(`## NavRoute Integration`);
|
|
6735
|
+
lines.push("");
|
|
6736
|
+
lines.push(`- **NavRoute**: \`${input.navRoute}\``);
|
|
6737
|
+
lines.push(`- **API Path**: \`${navRouteToApiPath(input.navRoute)}\``);
|
|
6738
|
+
lines.push(`- **Methods**: ${input.methods.join(", ")}`);
|
|
6739
|
+
lines.push("");
|
|
6740
|
+
lines.push("## Generated Files");
|
|
6741
|
+
lines.push("");
|
|
6742
|
+
for (const file of result.files) {
|
|
6743
|
+
const relativePath = file.path.replace(/\\/g, "/").split("/src/").pop() || file.path;
|
|
6744
|
+
lines.push(`### ${relativePath}`);
|
|
6745
|
+
lines.push("");
|
|
6746
|
+
lines.push("```typescript");
|
|
6747
|
+
lines.push(file.content.substring(0, 1500) + (file.content.length > 1500 ? "\n// ... (truncated)" : ""));
|
|
6748
|
+
lines.push("```");
|
|
6749
|
+
lines.push("");
|
|
6750
|
+
}
|
|
6751
|
+
lines.push("## Next Steps");
|
|
6752
|
+
lines.push("");
|
|
6753
|
+
for (const instruction of result.instructions) {
|
|
6754
|
+
lines.push(`- ${instruction}`);
|
|
6755
|
+
}
|
|
6756
|
+
lines.push("");
|
|
6757
|
+
lines.push("## Required Setup");
|
|
6758
|
+
lines.push("");
|
|
6759
|
+
lines.push("1. Ensure `navRoutes.generated.ts` exists (run `scaffold_routes`)");
|
|
6760
|
+
lines.push("2. Configure `apiClient` with base URL and auth interceptors");
|
|
6761
|
+
lines.push("3. Install dependencies: `npm install @tanstack/react-query axios`");
|
|
6762
|
+
return lines.join("\n");
|
|
6763
|
+
}
|
|
6764
|
+
|
|
6765
|
+
// src/tools/scaffold-routes.ts
|
|
6766
|
+
import path15 from "path";
|
|
6767
|
+
import { glob as glob2 } from "glob";
|
|
6768
|
+
var scaffoldRoutesTool = {
|
|
6769
|
+
name: "scaffold_routes",
|
|
6770
|
+
description: `Generate React Router configuration from backend NavRoute attributes.
|
|
6771
|
+
|
|
6772
|
+
Creates:
|
|
6773
|
+
- navRoutes.generated.ts: Registry of all routes with API paths and permissions
|
|
6774
|
+
- routes.tsx: React Router configuration with nested routes
|
|
6775
|
+
- Layout components (optional)
|
|
6776
|
+
|
|
6777
|
+
Example:
|
|
6778
|
+
scaffold_routes source="controllers" scope="all"
|
|
6779
|
+
|
|
6780
|
+
Scans backend controllers for [NavRoute("context.application.module")] attributes
|
|
6781
|
+
and generates corresponding frontend routing infrastructure.`,
|
|
6782
|
+
inputSchema: {
|
|
6783
|
+
type: "object",
|
|
6784
|
+
properties: {
|
|
6785
|
+
source: {
|
|
6786
|
+
type: "string",
|
|
6787
|
+
enum: ["controllers", "navigation", "manual"],
|
|
6788
|
+
default: "controllers",
|
|
6789
|
+
description: "Source for route discovery"
|
|
6790
|
+
},
|
|
6791
|
+
scope: {
|
|
6792
|
+
type: "string",
|
|
6793
|
+
enum: ["all", "platform", "business", "extensions"],
|
|
6794
|
+
default: "all",
|
|
6795
|
+
description: "Scope of routes to generate"
|
|
6796
|
+
},
|
|
6797
|
+
options: {
|
|
6798
|
+
type: "object",
|
|
6799
|
+
properties: {
|
|
6800
|
+
outputPath: { type: "string", description: "Custom output path" },
|
|
6801
|
+
includeLayouts: { type: "boolean", default: true },
|
|
6802
|
+
includeGuards: { type: "boolean", default: true },
|
|
6803
|
+
generateRegistry: { type: "boolean", default: true },
|
|
6804
|
+
dryRun: { type: "boolean", default: false }
|
|
6805
|
+
}
|
|
6806
|
+
}
|
|
6807
|
+
}
|
|
6808
|
+
}
|
|
6809
|
+
};
|
|
6810
|
+
async function handleScaffoldRoutes(args, config) {
|
|
6811
|
+
const input = ScaffoldRoutesInputSchema.parse(args);
|
|
6812
|
+
logger.info("Scaffolding routes", { source: input.source, scope: input.scope });
|
|
6813
|
+
const result = await scaffoldRoutes(input, config);
|
|
6814
|
+
return formatResult5(result, input);
|
|
6815
|
+
}
|
|
6816
|
+
async function scaffoldRoutes(input, config) {
|
|
6817
|
+
const result = {
|
|
6818
|
+
success: true,
|
|
6819
|
+
files: [],
|
|
6820
|
+
instructions: []
|
|
6821
|
+
};
|
|
6822
|
+
const { source, scope, options } = input;
|
|
6823
|
+
const dryRun = options?.dryRun ?? false;
|
|
6824
|
+
const includeLayouts = options?.includeLayouts ?? true;
|
|
6825
|
+
const includeGuards = options?.includeGuards ?? true;
|
|
6826
|
+
const generateRegistry = options?.generateRegistry ?? true;
|
|
6827
|
+
const projectRoot = config.smartstack.projectPath;
|
|
6828
|
+
const structure = await findSmartStackStructure(projectRoot);
|
|
6829
|
+
const webPath = structure.web || path15.join(projectRoot, "web", "smartstack-web");
|
|
6830
|
+
const routesPath = options?.outputPath || path15.join(webPath, "src", "routes");
|
|
6831
|
+
const navRoutes = await discoverNavRoutes(structure, scope);
|
|
6832
|
+
if (navRoutes.length === 0) {
|
|
6833
|
+
result.success = false;
|
|
6834
|
+
result.instructions.push("No NavRoute attributes found in controllers");
|
|
6835
|
+
return result;
|
|
6836
|
+
}
|
|
6837
|
+
if (generateRegistry) {
|
|
6838
|
+
const registryContent = generateNavRouteRegistry(navRoutes);
|
|
6839
|
+
const registryFile = path15.join(routesPath, "navRoutes.generated.ts");
|
|
6840
|
+
if (!dryRun) {
|
|
6841
|
+
await ensureDirectory(routesPath);
|
|
6842
|
+
await writeText(registryFile, registryContent);
|
|
6843
|
+
}
|
|
6844
|
+
result.files.push({ path: registryFile, content: registryContent, type: "created" });
|
|
6845
|
+
}
|
|
6846
|
+
const routerContent = generateRouterConfig(navRoutes, includeGuards);
|
|
6847
|
+
const routerFile = path15.join(routesPath, "index.tsx");
|
|
6848
|
+
if (!dryRun) {
|
|
6849
|
+
await ensureDirectory(routesPath);
|
|
6850
|
+
await writeText(routerFile, routerContent);
|
|
6851
|
+
}
|
|
6852
|
+
result.files.push({ path: routerFile, content: routerContent, type: "created" });
|
|
6853
|
+
if (includeLayouts) {
|
|
6854
|
+
const layoutsPath = path15.join(webPath, "src", "layouts");
|
|
6855
|
+
const contexts = [...new Set(navRoutes.map((r) => r.navRoute.split(".")[0]))];
|
|
6856
|
+
for (const context of contexts) {
|
|
6857
|
+
const layoutContent = generateLayout(context);
|
|
6858
|
+
const layoutFile = path15.join(layoutsPath, `${capitalize(context)}Layout.tsx`);
|
|
6859
|
+
if (!dryRun) {
|
|
6860
|
+
await ensureDirectory(layoutsPath);
|
|
6861
|
+
await writeText(layoutFile, layoutContent);
|
|
6862
|
+
}
|
|
6863
|
+
result.files.push({ path: layoutFile, content: layoutContent, type: "created" });
|
|
6864
|
+
}
|
|
6865
|
+
}
|
|
6866
|
+
if (includeGuards) {
|
|
6867
|
+
const guardsContent = generateRouteGuards();
|
|
6868
|
+
const guardsFile = path15.join(routesPath, "guards.tsx");
|
|
6869
|
+
if (!dryRun) {
|
|
6870
|
+
await writeText(guardsFile, guardsContent);
|
|
6871
|
+
}
|
|
6872
|
+
result.files.push({ path: guardsFile, content: guardsContent, type: "created" });
|
|
6873
|
+
}
|
|
6874
|
+
result.instructions.push(`Generated ${navRoutes.length} routes from ${source}`);
|
|
6875
|
+
result.instructions.push('Import routes: import { router } from "./routes";');
|
|
6876
|
+
result.instructions.push("Use with RouterProvider: <RouterProvider router={router} />");
|
|
6877
|
+
return result;
|
|
6878
|
+
}
|
|
6879
|
+
async function discoverNavRoutes(structure, scope) {
|
|
6880
|
+
const routes = [];
|
|
6881
|
+
const apiPath = structure.api;
|
|
6882
|
+
if (!apiPath) {
|
|
6883
|
+
logger.warn("No API project found");
|
|
6884
|
+
return routes;
|
|
6885
|
+
}
|
|
6886
|
+
const controllerFiles = await glob2("**/*Controller.cs", {
|
|
6887
|
+
cwd: apiPath,
|
|
6888
|
+
absolute: true,
|
|
6889
|
+
ignore: ["**/obj/**", "**/bin/**"]
|
|
6890
|
+
});
|
|
6891
|
+
for (const file of controllerFiles) {
|
|
6892
|
+
try {
|
|
6893
|
+
const content = await readText(file);
|
|
6894
|
+
const navRouteMatch = content.match(/\[NavRoute\s*\(\s*"([^"]+)"(?:\s*,\s*Suffix\s*=\s*"([^"]+)")?\s*\)\]/);
|
|
6895
|
+
if (navRouteMatch) {
|
|
6896
|
+
const navRoute = navRouteMatch[1];
|
|
6897
|
+
const suffix = navRouteMatch[2];
|
|
6898
|
+
const context = navRoute.split(".")[0];
|
|
6899
|
+
if (scope !== "all" && context !== scope) {
|
|
6900
|
+
continue;
|
|
6901
|
+
}
|
|
6902
|
+
const controllerMatch = path15.basename(file).match(/(.+)Controller\.cs$/);
|
|
6903
|
+
const controllerName = controllerMatch ? controllerMatch[1] : "Unknown";
|
|
6904
|
+
const methods = [];
|
|
6905
|
+
if (content.includes("[HttpGet]")) methods.push("GET");
|
|
6906
|
+
if (content.includes("[HttpPost]")) methods.push("POST");
|
|
6907
|
+
if (content.includes("[HttpPut]")) methods.push("PUT");
|
|
6908
|
+
if (content.includes("[HttpDelete]")) methods.push("DELETE");
|
|
6909
|
+
if (content.includes("[HttpPatch]")) methods.push("PATCH");
|
|
6910
|
+
const permissions = [];
|
|
6911
|
+
const authorizeMatches = content.matchAll(/\[Authorize\s*\(\s*[^)]*Policy\s*=\s*"([^"]+)"/g);
|
|
6912
|
+
for (const match of authorizeMatches) {
|
|
6913
|
+
permissions.push(match[1]);
|
|
6914
|
+
}
|
|
6915
|
+
const fullNavRoute = suffix ? `${navRoute}.${suffix}` : navRoute;
|
|
6916
|
+
routes.push({
|
|
6917
|
+
navRoute: fullNavRoute,
|
|
6918
|
+
apiPath: `/api/${navRoute.replace(/\./g, "/")}${suffix ? `/${suffix}` : ""}`,
|
|
6919
|
+
webPath: `/${navRoute.replace(/\./g, "/")}${suffix ? `/${suffix}` : ""}`,
|
|
6920
|
+
permissions,
|
|
6921
|
+
controller: controllerName,
|
|
6922
|
+
methods
|
|
6923
|
+
});
|
|
6924
|
+
}
|
|
6925
|
+
} catch {
|
|
6926
|
+
logger.debug(`Failed to parse controller: ${file}`);
|
|
6927
|
+
}
|
|
6928
|
+
}
|
|
6929
|
+
return routes.sort((a, b) => a.navRoute.localeCompare(b.navRoute));
|
|
6930
|
+
}
|
|
6931
|
+
function generateNavRouteRegistry(routes) {
|
|
6932
|
+
const lines = [
|
|
6933
|
+
"/**",
|
|
6934
|
+
" * NavRoute Registry",
|
|
6935
|
+
" *",
|
|
6936
|
+
" * Auto-generated by SmartStack MCP - DO NOT EDIT MANUALLY",
|
|
6937
|
+
" * Run `scaffold_routes` to regenerate",
|
|
6938
|
+
" */",
|
|
6939
|
+
"",
|
|
6940
|
+
"export interface NavRoute {",
|
|
6941
|
+
" navRoute: string;",
|
|
6942
|
+
" api: string;",
|
|
6943
|
+
" web: string;",
|
|
6944
|
+
" permissions: string[];",
|
|
6945
|
+
" controller?: string;",
|
|
6946
|
+
" methods: string[];",
|
|
6947
|
+
"}",
|
|
6948
|
+
"",
|
|
6949
|
+
"export const ROUTES: Record<string, NavRoute> = {"
|
|
6950
|
+
];
|
|
6951
|
+
for (const route of routes) {
|
|
6952
|
+
lines.push(` '${route.navRoute}': {`);
|
|
6953
|
+
lines.push(` navRoute: '${route.navRoute}',`);
|
|
6954
|
+
lines.push(` api: '${route.apiPath}',`);
|
|
6955
|
+
lines.push(` web: '${route.webPath}',`);
|
|
6956
|
+
lines.push(` permissions: [${route.permissions.map((p) => `'${p}'`).join(", ")}],`);
|
|
6957
|
+
if (route.controller) {
|
|
6958
|
+
lines.push(` controller: '${route.controller}',`);
|
|
6959
|
+
}
|
|
6960
|
+
lines.push(` methods: [${route.methods.map((m) => `'${m}'`).join(", ")}],`);
|
|
6961
|
+
lines.push(" },");
|
|
6962
|
+
}
|
|
6963
|
+
lines.push("};");
|
|
6964
|
+
lines.push("");
|
|
6965
|
+
lines.push("/**");
|
|
6966
|
+
lines.push(" * Get route configuration by NavRoute path");
|
|
6967
|
+
lines.push(" */");
|
|
6968
|
+
lines.push("export function getRoute(navRoute: string): NavRoute {");
|
|
6969
|
+
lines.push(" const route = ROUTES[navRoute];");
|
|
6970
|
+
lines.push(" if (!route) {");
|
|
6971
|
+
lines.push(" throw new Error(`Route not found: ${navRoute}`);");
|
|
6972
|
+
lines.push(" }");
|
|
6973
|
+
lines.push(" return route;");
|
|
6974
|
+
lines.push("}");
|
|
6975
|
+
lines.push("");
|
|
6976
|
+
lines.push("/**");
|
|
6977
|
+
lines.push(" * Check if user has permission for route");
|
|
6978
|
+
lines.push(" */");
|
|
6979
|
+
lines.push("export function hasRoutePermission(navRoute: string, userPermissions: string[]): boolean {");
|
|
6980
|
+
lines.push(" const route = ROUTES[navRoute];");
|
|
6981
|
+
lines.push(" if (!route || route.permissions.length === 0) return true;");
|
|
6982
|
+
lines.push(" return route.permissions.some(p => userPermissions.includes(p));");
|
|
6983
|
+
lines.push("}");
|
|
6984
|
+
lines.push("");
|
|
6985
|
+
lines.push("/**");
|
|
6986
|
+
lines.push(" * Get all routes for a context");
|
|
6987
|
+
lines.push(" */");
|
|
6988
|
+
lines.push("export function getRoutesByContext(context: string): NavRoute[] {");
|
|
6989
|
+
lines.push(" return Object.values(ROUTES).filter(r => r.navRoute.startsWith(`${context}.`));");
|
|
6990
|
+
lines.push("}");
|
|
6991
|
+
lines.push("");
|
|
6992
|
+
return lines.join("\n");
|
|
6993
|
+
}
|
|
6994
|
+
function generateRouterConfig(routes, includeGuards) {
|
|
6995
|
+
const routeTree = buildRouteTree(routes);
|
|
6996
|
+
const lines = [
|
|
6997
|
+
"/**",
|
|
6998
|
+
" * React Router Configuration",
|
|
6999
|
+
" *",
|
|
7000
|
+
" * Auto-generated by SmartStack MCP - DO NOT EDIT MANUALLY",
|
|
7001
|
+
" */",
|
|
7002
|
+
"",
|
|
7003
|
+
"import { createBrowserRouter, RouteObject } from 'react-router-dom';",
|
|
7004
|
+
"import { ROUTES } from './navRoutes.generated';"
|
|
7005
|
+
];
|
|
7006
|
+
if (includeGuards) {
|
|
7007
|
+
lines.push("import { ProtectedRoute, PermissionGuard } from './guards';");
|
|
7008
|
+
}
|
|
7009
|
+
const contexts = Object.keys(routeTree);
|
|
7010
|
+
for (const context of contexts) {
|
|
7011
|
+
lines.push(`import { ${capitalize(context)}Layout } from '../layouts/${capitalize(context)}Layout';`);
|
|
7012
|
+
}
|
|
7013
|
+
lines.push("");
|
|
7014
|
+
lines.push("// Page imports - customize these paths");
|
|
7015
|
+
for (const route of routes) {
|
|
7016
|
+
const pageName = route.navRoute.split(".").map(capitalize).join("");
|
|
7017
|
+
lines.push(`// import { ${pageName}Page } from '../pages/${pageName}Page';`);
|
|
7018
|
+
}
|
|
7019
|
+
lines.push("");
|
|
7020
|
+
lines.push("const routes: RouteObject[] = [");
|
|
7021
|
+
for (const [context, applications] of Object.entries(routeTree)) {
|
|
7022
|
+
lines.push(" {");
|
|
7023
|
+
lines.push(` path: '${context}',`);
|
|
7024
|
+
lines.push(` element: <${capitalize(context)}Layout />,`);
|
|
7025
|
+
lines.push(" children: [");
|
|
7026
|
+
for (const [app, modules] of Object.entries(applications)) {
|
|
7027
|
+
lines.push(" {");
|
|
7028
|
+
lines.push(` path: '${app}',`);
|
|
7029
|
+
lines.push(" children: [");
|
|
7030
|
+
for (const route of modules) {
|
|
7031
|
+
const modulePath = route.navRoute.split(".").slice(2).join("/");
|
|
7032
|
+
const pageName = route.navRoute.split(".").map(capitalize).join("");
|
|
7033
|
+
if (includeGuards && route.permissions.length > 0) {
|
|
7034
|
+
lines.push(" {");
|
|
7035
|
+
lines.push(` path: '${modulePath || ""}',`);
|
|
7036
|
+
lines.push(` element: (`);
|
|
7037
|
+
lines.push(` <PermissionGuard permissions={ROUTES['${route.navRoute}'].permissions}>`);
|
|
7038
|
+
lines.push(` {/* <${pageName}Page /> */}`);
|
|
7039
|
+
lines.push(` <div>TODO: ${pageName}Page</div>`);
|
|
7040
|
+
lines.push(` </PermissionGuard>`);
|
|
7041
|
+
lines.push(` ),`);
|
|
7042
|
+
lines.push(" },");
|
|
7043
|
+
} else {
|
|
7044
|
+
lines.push(" {");
|
|
7045
|
+
lines.push(` path: '${modulePath || ""}',`);
|
|
7046
|
+
lines.push(` element: <div>TODO: ${pageName}Page</div>,`);
|
|
7047
|
+
lines.push(" },");
|
|
7048
|
+
}
|
|
7049
|
+
}
|
|
7050
|
+
lines.push(" ],");
|
|
7051
|
+
lines.push(" },");
|
|
7052
|
+
}
|
|
7053
|
+
lines.push(" ],");
|
|
7054
|
+
lines.push(" },");
|
|
7055
|
+
}
|
|
7056
|
+
lines.push("];");
|
|
7057
|
+
lines.push("");
|
|
7058
|
+
lines.push("export const router = createBrowserRouter(routes);");
|
|
7059
|
+
lines.push("");
|
|
7060
|
+
lines.push("export default router;");
|
|
7061
|
+
lines.push("");
|
|
7062
|
+
return lines.join("\n");
|
|
7063
|
+
}
|
|
7064
|
+
function buildRouteTree(routes) {
|
|
7065
|
+
const tree = {};
|
|
7066
|
+
for (const route of routes) {
|
|
7067
|
+
const parts = route.navRoute.split(".");
|
|
7068
|
+
const context = parts[0];
|
|
7069
|
+
const app = parts[1] || "default";
|
|
7070
|
+
if (!tree[context]) {
|
|
7071
|
+
tree[context] = {};
|
|
7072
|
+
}
|
|
7073
|
+
if (!tree[context][app]) {
|
|
7074
|
+
tree[context][app] = [];
|
|
7075
|
+
}
|
|
7076
|
+
tree[context][app].push(route);
|
|
7077
|
+
}
|
|
7078
|
+
return tree;
|
|
7079
|
+
}
|
|
7080
|
+
function generateLayout(context) {
|
|
7081
|
+
const contextCapitalized = capitalize(context);
|
|
7082
|
+
return `/**
|
|
7083
|
+
* ${contextCapitalized} Layout
|
|
7084
|
+
*
|
|
7085
|
+
* Auto-generated by SmartStack MCP - Customize as needed
|
|
7086
|
+
*/
|
|
7087
|
+
|
|
7088
|
+
import React from 'react';
|
|
7089
|
+
import { Outlet, Link, useLocation } from 'react-router-dom';
|
|
7090
|
+
import { ROUTES, getRoutesByContext } from '../routes/navRoutes.generated';
|
|
7091
|
+
|
|
7092
|
+
export const ${contextCapitalized}Layout: React.FC = () => {
|
|
7093
|
+
const location = useLocation();
|
|
7094
|
+
const contextRoutes = getRoutesByContext('${context}');
|
|
7095
|
+
|
|
7096
|
+
return (
|
|
7097
|
+
<div className="flex h-screen bg-gray-100">
|
|
7098
|
+
{/* Sidebar */}
|
|
7099
|
+
<aside className="w-64 bg-white shadow-sm">
|
|
7100
|
+
<div className="p-4 border-b">
|
|
7101
|
+
<h1 className="text-xl font-semibold text-gray-900">${contextCapitalized}</h1>
|
|
7102
|
+
</div>
|
|
7103
|
+
<nav className="p-4">
|
|
7104
|
+
<ul className="space-y-2">
|
|
7105
|
+
{contextRoutes.map((route) => (
|
|
7106
|
+
<li key={route.navRoute}>
|
|
7107
|
+
<Link
|
|
7108
|
+
to={route.web}
|
|
7109
|
+
className={\`block px-3 py-2 rounded-md \${
|
|
7110
|
+
location.pathname === route.web
|
|
7111
|
+
? 'bg-blue-50 text-blue-700'
|
|
7112
|
+
: 'text-gray-700 hover:bg-gray-50'
|
|
7113
|
+
}\`}
|
|
7114
|
+
>
|
|
7115
|
+
{route.navRoute.split('.').pop()}
|
|
7116
|
+
</Link>
|
|
7117
|
+
</li>
|
|
7118
|
+
))}
|
|
7119
|
+
</ul>
|
|
7120
|
+
</nav>
|
|
7121
|
+
</aside>
|
|
7122
|
+
|
|
7123
|
+
{/* Main content */}
|
|
7124
|
+
<main className="flex-1 overflow-auto">
|
|
7125
|
+
<div className="p-6">
|
|
7126
|
+
<Outlet />
|
|
7127
|
+
</div>
|
|
7128
|
+
</main>
|
|
7129
|
+
</div>
|
|
7130
|
+
);
|
|
7131
|
+
};
|
|
7132
|
+
|
|
7133
|
+
export default ${contextCapitalized}Layout;
|
|
7134
|
+
`;
|
|
7135
|
+
}
|
|
7136
|
+
function generateRouteGuards() {
|
|
7137
|
+
return `/**
|
|
7138
|
+
* Route Guards
|
|
7139
|
+
*
|
|
7140
|
+
* Auto-generated by SmartStack MCP - Customize as needed
|
|
7141
|
+
*/
|
|
7142
|
+
|
|
7143
|
+
import React from 'react';
|
|
7144
|
+
import { Navigate, useLocation } from 'react-router-dom';
|
|
7145
|
+
|
|
7146
|
+
interface ProtectedRouteProps {
|
|
7147
|
+
children: React.ReactNode;
|
|
7148
|
+
redirectTo?: string;
|
|
7149
|
+
}
|
|
7150
|
+
|
|
7151
|
+
interface PermissionGuardProps {
|
|
7152
|
+
children: React.ReactNode;
|
|
7153
|
+
permissions: string[];
|
|
7154
|
+
fallback?: React.ReactNode;
|
|
7155
|
+
}
|
|
7156
|
+
|
|
7157
|
+
/**
|
|
7158
|
+
* Protect routes that require authentication
|
|
7159
|
+
*/
|
|
7160
|
+
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
|
7161
|
+
children,
|
|
7162
|
+
redirectTo = '/login'
|
|
7163
|
+
}) => {
|
|
7164
|
+
const location = useLocation();
|
|
7165
|
+
|
|
7166
|
+
// TODO: Replace with your auth hook
|
|
7167
|
+
const isAuthenticated = true; // useAuth().isAuthenticated;
|
|
7168
|
+
|
|
7169
|
+
if (!isAuthenticated) {
|
|
7170
|
+
return <Navigate to={redirectTo} state={{ from: location }} replace />;
|
|
7171
|
+
}
|
|
7172
|
+
|
|
7173
|
+
return <>{children}</>;
|
|
7174
|
+
};
|
|
7175
|
+
|
|
7176
|
+
/**
|
|
7177
|
+
* Guard routes based on user permissions
|
|
7178
|
+
*/
|
|
7179
|
+
export const PermissionGuard: React.FC<PermissionGuardProps> = ({
|
|
7180
|
+
children,
|
|
7181
|
+
permissions,
|
|
7182
|
+
fallback
|
|
7183
|
+
}) => {
|
|
7184
|
+
// TODO: Replace with your auth hook
|
|
7185
|
+
const userPermissions: string[] = []; // useAuth().permissions;
|
|
7186
|
+
|
|
7187
|
+
const hasPermission = permissions.length === 0 ||
|
|
7188
|
+
permissions.some(p => userPermissions.includes(p));
|
|
7189
|
+
|
|
7190
|
+
if (!hasPermission) {
|
|
7191
|
+
if (fallback) return <>{fallback}</>;
|
|
7192
|
+
return (
|
|
7193
|
+
<div className="flex items-center justify-center h-64">
|
|
7194
|
+
<div className="text-center">
|
|
7195
|
+
<h2 className="text-xl font-semibold text-gray-900">Access Denied</h2>
|
|
7196
|
+
<p className="mt-2 text-gray-600">
|
|
7197
|
+
You don't have permission to access this page.
|
|
7198
|
+
</p>
|
|
7199
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
7200
|
+
Required: {permissions.join(', ')}
|
|
7201
|
+
</p>
|
|
7202
|
+
</div>
|
|
7203
|
+
</div>
|
|
7204
|
+
);
|
|
7205
|
+
}
|
|
7206
|
+
|
|
7207
|
+
return <>{children}</>;
|
|
7208
|
+
};
|
|
7209
|
+
`;
|
|
7210
|
+
}
|
|
7211
|
+
function capitalize(str) {
|
|
7212
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
7213
|
+
}
|
|
7214
|
+
function formatResult5(result, input) {
|
|
7215
|
+
const lines = [];
|
|
7216
|
+
lines.push("# Scaffold Routes");
|
|
7217
|
+
lines.push("");
|
|
7218
|
+
if (input.options?.dryRun) {
|
|
7219
|
+
lines.push("> **DRY RUN** - No files were written");
|
|
7220
|
+
lines.push("");
|
|
7221
|
+
}
|
|
7222
|
+
lines.push("## Configuration");
|
|
7223
|
+
lines.push("");
|
|
7224
|
+
lines.push(`- **Source**: ${input.source}`);
|
|
7225
|
+
lines.push(`- **Scope**: ${input.scope}`);
|
|
7226
|
+
lines.push("");
|
|
7227
|
+
lines.push("## Generated Files");
|
|
7228
|
+
lines.push("");
|
|
7229
|
+
for (const file of result.files) {
|
|
7230
|
+
const relativePath = file.path.replace(/\\/g, "/").split("/src/").pop() || file.path;
|
|
7231
|
+
lines.push(`### ${relativePath}`);
|
|
7232
|
+
lines.push("");
|
|
7233
|
+
lines.push("```tsx");
|
|
7234
|
+
lines.push(file.content.substring(0, 2e3) + (file.content.length > 2e3 ? "\n// ... (truncated)" : ""));
|
|
7235
|
+
lines.push("```");
|
|
7236
|
+
lines.push("");
|
|
7237
|
+
}
|
|
7238
|
+
lines.push("## Instructions");
|
|
7239
|
+
lines.push("");
|
|
7240
|
+
for (const instruction of result.instructions) {
|
|
7241
|
+
lines.push(`- ${instruction}`);
|
|
7242
|
+
}
|
|
7243
|
+
return lines.join("\n");
|
|
7244
|
+
}
|
|
7245
|
+
|
|
7246
|
+
// src/tools/validate-frontend-routes.ts
|
|
7247
|
+
import path16 from "path";
|
|
7248
|
+
import { glob as glob3 } from "glob";
|
|
7249
|
+
var validateFrontendRoutesTool = {
|
|
7250
|
+
name: "validate_frontend_routes",
|
|
7251
|
+
description: `Validate frontend routes against backend NavRoute attributes.
|
|
7252
|
+
|
|
7253
|
+
Checks:
|
|
7254
|
+
- navRoutes.generated.ts exists and is up-to-date
|
|
7255
|
+
- API clients use correct NavRoute paths
|
|
7256
|
+
- React Router configuration matches backend routes
|
|
7257
|
+
- Permission configurations are synchronized
|
|
7258
|
+
|
|
7259
|
+
Example:
|
|
7260
|
+
validate_frontend_routes scope="all"
|
|
7261
|
+
|
|
7262
|
+
Reports issues and provides actionable recommendations for synchronization.`,
|
|
7263
|
+
inputSchema: {
|
|
7264
|
+
type: "object",
|
|
7265
|
+
properties: {
|
|
7266
|
+
scope: {
|
|
7267
|
+
type: "string",
|
|
7268
|
+
enum: ["api-clients", "routes", "registry", "all"],
|
|
7269
|
+
default: "all",
|
|
7270
|
+
description: "Scope of validation"
|
|
7271
|
+
},
|
|
7272
|
+
options: {
|
|
7273
|
+
type: "object",
|
|
7274
|
+
properties: {
|
|
7275
|
+
fix: { type: "boolean", default: false, description: "Auto-fix minor issues" },
|
|
7276
|
+
strict: { type: "boolean", default: false, description: "Fail on warnings" }
|
|
7277
|
+
}
|
|
7278
|
+
}
|
|
7279
|
+
}
|
|
7280
|
+
}
|
|
7281
|
+
};
|
|
7282
|
+
async function handleValidateFrontendRoutes(args, config) {
|
|
7283
|
+
const input = ValidateFrontendRoutesInputSchema.parse(args);
|
|
7284
|
+
logger.info("Validating frontend routes", { scope: input.scope });
|
|
7285
|
+
const result = await validateFrontendRoutes(input, config);
|
|
7286
|
+
return formatResult6(result, input);
|
|
7287
|
+
}
|
|
7288
|
+
async function validateFrontendRoutes(input, config) {
|
|
7289
|
+
const result = {
|
|
7290
|
+
valid: true,
|
|
7291
|
+
registry: {
|
|
7292
|
+
exists: false,
|
|
7293
|
+
routeCount: 0,
|
|
7294
|
+
outdated: []
|
|
7295
|
+
},
|
|
7296
|
+
apiClients: {
|
|
7297
|
+
total: 0,
|
|
7298
|
+
valid: 0,
|
|
7299
|
+
issues: []
|
|
7300
|
+
},
|
|
7301
|
+
routes: {
|
|
7302
|
+
total: 0,
|
|
7303
|
+
orphaned: [],
|
|
7304
|
+
missing: []
|
|
7305
|
+
},
|
|
7306
|
+
recommendations: []
|
|
7307
|
+
};
|
|
7308
|
+
const { scope } = input;
|
|
7309
|
+
const projectRoot = config.smartstack.projectPath;
|
|
7310
|
+
const structure = await findSmartStackStructure(projectRoot);
|
|
7311
|
+
const webPath = structure.web || path16.join(projectRoot, "web", "smartstack-web");
|
|
7312
|
+
const backendRoutes = await discoverBackendNavRoutes(structure);
|
|
7313
|
+
if (scope === "all" || scope === "registry") {
|
|
7314
|
+
await validateRegistry(webPath, backendRoutes, result);
|
|
7315
|
+
}
|
|
7316
|
+
if (scope === "all" || scope === "api-clients") {
|
|
7317
|
+
await validateApiClients(webPath, backendRoutes, result);
|
|
7318
|
+
}
|
|
7319
|
+
if (scope === "all" || scope === "routes") {
|
|
7320
|
+
await validateRoutes(webPath, backendRoutes, result);
|
|
7321
|
+
}
|
|
7322
|
+
generateRecommendations2(result);
|
|
7323
|
+
result.valid = result.apiClients.issues.filter((i) => i.severity === "error").length === 0 && result.routes.missing.length === 0 && result.registry.exists;
|
|
7324
|
+
return result;
|
|
7325
|
+
}
|
|
7326
|
+
async function discoverBackendNavRoutes(structure) {
|
|
7327
|
+
const routes = [];
|
|
7328
|
+
const apiPath = structure.api;
|
|
7329
|
+
if (!apiPath) {
|
|
7330
|
+
return routes;
|
|
7331
|
+
}
|
|
7332
|
+
const controllerFiles = await glob3("**/*Controller.cs", {
|
|
7333
|
+
cwd: apiPath,
|
|
7334
|
+
absolute: true,
|
|
7335
|
+
ignore: ["**/obj/**", "**/bin/**"]
|
|
7336
|
+
});
|
|
7337
|
+
for (const file of controllerFiles) {
|
|
7338
|
+
try {
|
|
7339
|
+
const content = await readText(file);
|
|
7340
|
+
const navRouteMatch = content.match(/\[NavRoute\s*\(\s*"([^"]+)"(?:\s*,\s*Suffix\s*=\s*"([^"]+)")?\s*\)\]/);
|
|
7341
|
+
if (navRouteMatch) {
|
|
7342
|
+
const navRoute = navRouteMatch[1];
|
|
7343
|
+
const suffix = navRouteMatch[2];
|
|
7344
|
+
const fullNavRoute = suffix ? `${navRoute}.${suffix}` : navRoute;
|
|
7345
|
+
const controllerMatch = path16.basename(file).match(/(.+)Controller\.cs$/);
|
|
7346
|
+
const controllerName = controllerMatch ? controllerMatch[1] : "Unknown";
|
|
7347
|
+
const methods = [];
|
|
7348
|
+
if (content.includes("[HttpGet]")) methods.push("GET");
|
|
7349
|
+
if (content.includes("[HttpPost]")) methods.push("POST");
|
|
7350
|
+
if (content.includes("[HttpPut]")) methods.push("PUT");
|
|
7351
|
+
if (content.includes("[HttpDelete]")) methods.push("DELETE");
|
|
7352
|
+
const permissions = [];
|
|
7353
|
+
const authorizeMatches = content.matchAll(/\[Authorize\s*\(\s*[^)]*Policy\s*=\s*"([^"]+)"/g);
|
|
7354
|
+
for (const match of authorizeMatches) {
|
|
7355
|
+
permissions.push(match[1]);
|
|
7356
|
+
}
|
|
7357
|
+
routes.push({
|
|
7358
|
+
navRoute: fullNavRoute,
|
|
7359
|
+
apiPath: `/api/${navRoute.replace(/\./g, "/")}${suffix ? `/${suffix}` : ""}`,
|
|
7360
|
+
webPath: `/${navRoute.replace(/\./g, "/")}${suffix ? `/${suffix}` : ""}`,
|
|
7361
|
+
permissions,
|
|
7362
|
+
controller: controllerName,
|
|
7363
|
+
methods
|
|
7364
|
+
});
|
|
7365
|
+
}
|
|
7366
|
+
} catch {
|
|
7367
|
+
logger.debug(`Failed to parse controller: ${file}`);
|
|
7368
|
+
}
|
|
7369
|
+
}
|
|
7370
|
+
return routes;
|
|
7371
|
+
}
|
|
7372
|
+
async function validateRegistry(webPath, backendRoutes, result) {
|
|
7373
|
+
const registryPath = path16.join(webPath, "src", "routes", "navRoutes.generated.ts");
|
|
7374
|
+
if (!await fileExists(registryPath)) {
|
|
7375
|
+
result.registry.exists = false;
|
|
7376
|
+
result.recommendations.push("Run `scaffold_routes` to generate navRoutes.generated.ts");
|
|
7377
|
+
return;
|
|
7378
|
+
}
|
|
7379
|
+
result.registry.exists = true;
|
|
7380
|
+
try {
|
|
7381
|
+
const content = await readText(registryPath);
|
|
7382
|
+
const routeMatches = content.matchAll(/'([a-z.]+)':\s*\{/g);
|
|
7383
|
+
const registryRoutes = /* @__PURE__ */ new Set();
|
|
7384
|
+
for (const match of routeMatches) {
|
|
7385
|
+
registryRoutes.add(match[1]);
|
|
7386
|
+
}
|
|
7387
|
+
result.registry.routeCount = registryRoutes.size;
|
|
7388
|
+
for (const backendRoute of backendRoutes) {
|
|
7389
|
+
if (!registryRoutes.has(backendRoute.navRoute)) {
|
|
7390
|
+
result.registry.outdated.push(backendRoute.navRoute);
|
|
7391
|
+
}
|
|
7392
|
+
}
|
|
7393
|
+
for (const registryRoute of registryRoutes) {
|
|
7394
|
+
if (!backendRoutes.find((r) => r.navRoute === registryRoute)) {
|
|
7395
|
+
result.registry.outdated.push(`${registryRoute} (removed from backend)`);
|
|
7396
|
+
}
|
|
7397
|
+
}
|
|
7398
|
+
} catch {
|
|
7399
|
+
result.registry.exists = false;
|
|
7400
|
+
}
|
|
7401
|
+
}
|
|
7402
|
+
async function validateApiClients(webPath, backendRoutes, result) {
|
|
7403
|
+
const servicesPath = path16.join(webPath, "src", "services", "api");
|
|
7404
|
+
const clientFiles = await glob3("**/*.ts", {
|
|
7405
|
+
cwd: servicesPath,
|
|
7406
|
+
absolute: true,
|
|
7407
|
+
ignore: ["**/index.ts"]
|
|
7408
|
+
});
|
|
7409
|
+
result.apiClients.total = clientFiles.length;
|
|
7410
|
+
for (const file of clientFiles) {
|
|
7411
|
+
try {
|
|
7412
|
+
const content = await readText(file);
|
|
7413
|
+
const relativePath = path16.relative(webPath, file);
|
|
7414
|
+
const usesRegistry = content.includes("getRoute('") || content.includes('getRoute("');
|
|
7415
|
+
if (!usesRegistry) {
|
|
7416
|
+
const hardcodedMatch = content.match(/apiClient\.(get|post|put|delete)\s*[<(]\s*['"`]([^'"`]+)['"`]/);
|
|
7417
|
+
if (hardcodedMatch) {
|
|
7418
|
+
result.apiClients.issues.push({
|
|
7419
|
+
type: "invalid-path",
|
|
7420
|
+
severity: "warning",
|
|
7421
|
+
file: relativePath,
|
|
7422
|
+
message: `Hardcoded API path: ${hardcodedMatch[2]}`,
|
|
7423
|
+
suggestion: "Use getRoute() from navRoutes.generated.ts instead"
|
|
7424
|
+
});
|
|
7425
|
+
}
|
|
7426
|
+
} else {
|
|
7427
|
+
const navRouteMatch = content.match(/getRoute\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/);
|
|
7428
|
+
if (navRouteMatch) {
|
|
7429
|
+
const navRoute = navRouteMatch[1];
|
|
7430
|
+
const backendRoute = backendRoutes.find((r) => r.navRoute === navRoute);
|
|
7431
|
+
if (!backendRoute) {
|
|
7432
|
+
result.apiClients.issues.push({
|
|
7433
|
+
type: "missing-route",
|
|
7434
|
+
severity: "error",
|
|
7435
|
+
file: relativePath,
|
|
7436
|
+
navRoute,
|
|
7437
|
+
message: `NavRoute "${navRoute}" not found in backend controllers`,
|
|
7438
|
+
suggestion: "Verify the NavRoute path or update the backend controller"
|
|
7439
|
+
});
|
|
7440
|
+
} else {
|
|
7441
|
+
result.apiClients.valid++;
|
|
7442
|
+
}
|
|
7443
|
+
}
|
|
7444
|
+
}
|
|
7445
|
+
} catch {
|
|
7446
|
+
logger.debug(`Failed to parse API client: ${file}`);
|
|
7447
|
+
}
|
|
7448
|
+
}
|
|
7449
|
+
}
|
|
7450
|
+
async function validateRoutes(webPath, backendRoutes, result) {
|
|
7451
|
+
const routesPath = path16.join(webPath, "src", "routes", "index.tsx");
|
|
7452
|
+
if (!await fileExists(routesPath)) {
|
|
7453
|
+
result.routes.total = 0;
|
|
7454
|
+
result.routes.missing = backendRoutes.map((r) => r.navRoute);
|
|
7455
|
+
return;
|
|
7456
|
+
}
|
|
7457
|
+
try {
|
|
7458
|
+
const content = await readText(routesPath);
|
|
7459
|
+
const pathMatches = content.matchAll(/path:\s*['"`]([^'"`]+)['"`]/g);
|
|
7460
|
+
const frontendPaths = /* @__PURE__ */ new Set();
|
|
7461
|
+
for (const match of pathMatches) {
|
|
7462
|
+
frontendPaths.add(match[1]);
|
|
7463
|
+
}
|
|
7464
|
+
result.routes.total = frontendPaths.size;
|
|
7465
|
+
for (const backendRoute of backendRoutes) {
|
|
7466
|
+
const webPath2 = backendRoute.webPath.replace(/^\//, "");
|
|
7467
|
+
const parts = webPath2.split("/");
|
|
7468
|
+
let found = false;
|
|
7469
|
+
for (const pathPart of parts) {
|
|
7470
|
+
if (frontendPaths.has(pathPart)) {
|
|
7471
|
+
found = true;
|
|
7472
|
+
break;
|
|
7473
|
+
}
|
|
7474
|
+
}
|
|
7475
|
+
if (!found && parts.length > 0) {
|
|
7476
|
+
result.routes.missing.push(backendRoute.navRoute);
|
|
7477
|
+
}
|
|
7478
|
+
}
|
|
7479
|
+
for (const frontendPath of frontendPaths) {
|
|
7480
|
+
if (frontendPath === "*" || frontendPath === "" || frontendPath.startsWith(":")) {
|
|
7481
|
+
continue;
|
|
7482
|
+
}
|
|
7483
|
+
const matchingBackend = backendRoutes.find(
|
|
7484
|
+
(r) => r.webPath.includes(frontendPath) || r.navRoute.includes(frontendPath)
|
|
7485
|
+
);
|
|
7486
|
+
if (!matchingBackend) {
|
|
7487
|
+
result.routes.orphaned.push(frontendPath);
|
|
7488
|
+
}
|
|
7489
|
+
}
|
|
7490
|
+
} catch {
|
|
7491
|
+
result.routes.total = 0;
|
|
7492
|
+
result.routes.missing = backendRoutes.map((r) => r.navRoute);
|
|
7493
|
+
}
|
|
7494
|
+
}
|
|
7495
|
+
function generateRecommendations2(result) {
|
|
7496
|
+
if (!result.registry.exists) {
|
|
7497
|
+
result.recommendations.push('Run `scaffold_routes source="controllers"` to generate route registry');
|
|
7498
|
+
} else if (result.registry.outdated.length > 0) {
|
|
7499
|
+
result.recommendations.push(`Registry is outdated: ${result.registry.outdated.length} routes need sync. Run \`scaffold_routes\``);
|
|
7500
|
+
}
|
|
7501
|
+
if (result.apiClients.issues.length > 0) {
|
|
7502
|
+
const hardcoded = result.apiClients.issues.filter((i) => i.type === "invalid-path").length;
|
|
7503
|
+
const missing = result.apiClients.issues.filter((i) => i.type === "missing-route").length;
|
|
7504
|
+
if (hardcoded > 0) {
|
|
7505
|
+
result.recommendations.push(`${hardcoded} API clients use hardcoded paths. Migrate to getRoute()`);
|
|
7506
|
+
}
|
|
7507
|
+
if (missing > 0) {
|
|
7508
|
+
result.recommendations.push(`${missing} API clients reference non-existent NavRoutes`);
|
|
7509
|
+
}
|
|
7510
|
+
}
|
|
7511
|
+
if (result.routes.missing.length > 0) {
|
|
7512
|
+
result.recommendations.push(`${result.routes.missing.length} backend routes have no frontend counterpart`);
|
|
7513
|
+
}
|
|
7514
|
+
if (result.routes.orphaned.length > 0) {
|
|
7515
|
+
result.recommendations.push(`${result.routes.orphaned.length} frontend routes have no backend NavRoute`);
|
|
7516
|
+
}
|
|
7517
|
+
if (result.valid && result.recommendations.length === 0) {
|
|
7518
|
+
result.recommendations.push("All routes are synchronized between frontend and backend");
|
|
7519
|
+
}
|
|
7520
|
+
}
|
|
7521
|
+
function formatResult6(result, _input) {
|
|
7522
|
+
const lines = [];
|
|
7523
|
+
const statusIcon = result.valid ? "\u2705" : "\u274C";
|
|
7524
|
+
lines.push(`# Frontend Route Validation ${statusIcon}`);
|
|
7525
|
+
lines.push("");
|
|
7526
|
+
lines.push("## Summary");
|
|
7527
|
+
lines.push("");
|
|
7528
|
+
lines.push(`| Metric | Value |`);
|
|
7529
|
+
lines.push(`|--------|-------|`);
|
|
7530
|
+
lines.push(`| Valid | ${result.valid ? "Yes" : "No"} |`);
|
|
7531
|
+
lines.push(`| Registry Exists | ${result.registry.exists ? "Yes" : "No"} |`);
|
|
7532
|
+
lines.push(`| Registry Routes | ${result.registry.routeCount} |`);
|
|
7533
|
+
lines.push(`| API Clients | ${result.apiClients.valid}/${result.apiClients.total} valid |`);
|
|
7534
|
+
lines.push(`| Frontend Routes | ${result.routes.total} |`);
|
|
7535
|
+
lines.push(`| Missing Routes | ${result.routes.missing.length} |`);
|
|
7536
|
+
lines.push(`| Orphaned Routes | ${result.routes.orphaned.length} |`);
|
|
7537
|
+
lines.push("");
|
|
7538
|
+
if (result.registry.outdated.length > 0) {
|
|
7539
|
+
lines.push("## Outdated Registry Entries");
|
|
7540
|
+
lines.push("");
|
|
7541
|
+
for (const route of result.registry.outdated) {
|
|
7542
|
+
lines.push(`- \`${route}\``);
|
|
7543
|
+
}
|
|
7544
|
+
lines.push("");
|
|
7545
|
+
}
|
|
7546
|
+
if (result.apiClients.issues.length > 0) {
|
|
7547
|
+
lines.push("## API Client Issues");
|
|
7548
|
+
lines.push("");
|
|
7549
|
+
for (const issue of result.apiClients.issues) {
|
|
7550
|
+
const icon = issue.severity === "error" ? "\u274C" : "\u26A0\uFE0F";
|
|
7551
|
+
lines.push(`### ${icon} ${issue.file}`);
|
|
7552
|
+
lines.push("");
|
|
7553
|
+
lines.push(`- **Type**: ${issue.type}`);
|
|
7554
|
+
lines.push(`- **Message**: ${issue.message}`);
|
|
7555
|
+
if (issue.navRoute) {
|
|
7556
|
+
lines.push(`- **NavRoute**: \`${issue.navRoute}\``);
|
|
7557
|
+
}
|
|
7558
|
+
lines.push(`- **Suggestion**: ${issue.suggestion}`);
|
|
7559
|
+
lines.push("");
|
|
7560
|
+
}
|
|
7561
|
+
}
|
|
7562
|
+
if (result.routes.missing.length > 0) {
|
|
7563
|
+
lines.push("## Missing Frontend Routes");
|
|
7564
|
+
lines.push("");
|
|
7565
|
+
lines.push("These backend NavRoutes have no corresponding frontend route:");
|
|
7566
|
+
lines.push("");
|
|
7567
|
+
for (const route of result.routes.missing) {
|
|
7568
|
+
lines.push(`- \`${route}\``);
|
|
7569
|
+
}
|
|
7570
|
+
lines.push("");
|
|
7571
|
+
}
|
|
7572
|
+
if (result.routes.orphaned.length > 0) {
|
|
7573
|
+
lines.push("## Orphaned Frontend Routes");
|
|
7574
|
+
lines.push("");
|
|
7575
|
+
lines.push("These frontend routes have no corresponding backend NavRoute:");
|
|
7576
|
+
lines.push("");
|
|
7577
|
+
for (const route of result.routes.orphaned) {
|
|
7578
|
+
lines.push(`- \`${route}\``);
|
|
7579
|
+
}
|
|
7580
|
+
lines.push("");
|
|
7581
|
+
}
|
|
7582
|
+
lines.push("## Recommendations");
|
|
7583
|
+
lines.push("");
|
|
7584
|
+
for (const rec of result.recommendations) {
|
|
7585
|
+
lines.push(`- ${rec}`);
|
|
7586
|
+
}
|
|
7587
|
+
lines.push("");
|
|
7588
|
+
lines.push("## Commands");
|
|
7589
|
+
lines.push("");
|
|
7590
|
+
lines.push("```bash");
|
|
7591
|
+
lines.push("# Regenerate route registry");
|
|
7592
|
+
lines.push('scaffold_routes source="controllers"');
|
|
7593
|
+
lines.push("");
|
|
7594
|
+
lines.push("# Generate API client for a specific NavRoute");
|
|
7595
|
+
lines.push('scaffold_api_client navRoute="platform.administration.users" name="User"');
|
|
7596
|
+
lines.push("```");
|
|
7597
|
+
return lines.join("\n");
|
|
7598
|
+
}
|
|
7599
|
+
|
|
6324
7600
|
// src/resources/conventions.ts
|
|
6325
7601
|
var conventionsResourceTemplate = {
|
|
6326
7602
|
uri: "smartstack://conventions",
|
|
@@ -7307,7 +8583,7 @@ Run specific or all checks:
|
|
|
7307
8583
|
}
|
|
7308
8584
|
|
|
7309
8585
|
// src/resources/project-info.ts
|
|
7310
|
-
import
|
|
8586
|
+
import path17 from "path";
|
|
7311
8587
|
var projectInfoResourceTemplate = {
|
|
7312
8588
|
uri: "smartstack://project",
|
|
7313
8589
|
name: "SmartStack Project Info",
|
|
@@ -7344,16 +8620,16 @@ async function getProjectInfoResource(config) {
|
|
|
7344
8620
|
lines.push("```");
|
|
7345
8621
|
lines.push(`${projectInfo.name}/`);
|
|
7346
8622
|
if (structure.domain) {
|
|
7347
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
8623
|
+
lines.push(`\u251C\u2500\u2500 ${path17.basename(structure.domain)}/ # Domain layer (entities)`);
|
|
7348
8624
|
}
|
|
7349
8625
|
if (structure.application) {
|
|
7350
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
8626
|
+
lines.push(`\u251C\u2500\u2500 ${path17.basename(structure.application)}/ # Application layer (services)`);
|
|
7351
8627
|
}
|
|
7352
8628
|
if (structure.infrastructure) {
|
|
7353
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
8629
|
+
lines.push(`\u251C\u2500\u2500 ${path17.basename(structure.infrastructure)}/ # Infrastructure (EF Core)`);
|
|
7354
8630
|
}
|
|
7355
8631
|
if (structure.api) {
|
|
7356
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
8632
|
+
lines.push(`\u251C\u2500\u2500 ${path17.basename(structure.api)}/ # API layer (controllers)`);
|
|
7357
8633
|
}
|
|
7358
8634
|
if (structure.web) {
|
|
7359
8635
|
lines.push(`\u2514\u2500\u2500 web/smartstack-web/ # React frontend`);
|
|
@@ -7366,8 +8642,8 @@ async function getProjectInfoResource(config) {
|
|
|
7366
8642
|
lines.push("| Project | Path |");
|
|
7367
8643
|
lines.push("|---------|------|");
|
|
7368
8644
|
for (const csproj of projectInfo.csprojFiles) {
|
|
7369
|
-
const name =
|
|
7370
|
-
const relativePath =
|
|
8645
|
+
const name = path17.basename(csproj, ".csproj");
|
|
8646
|
+
const relativePath = path17.relative(projectPath, csproj);
|
|
7371
8647
|
lines.push(`| ${name} | \`${relativePath}\` |`);
|
|
7372
8648
|
}
|
|
7373
8649
|
lines.push("");
|
|
@@ -7377,10 +8653,10 @@ async function getProjectInfoResource(config) {
|
|
|
7377
8653
|
cwd: structure.migrations,
|
|
7378
8654
|
ignore: ["*.Designer.cs"]
|
|
7379
8655
|
});
|
|
7380
|
-
const migrations = migrationFiles.map((f) =>
|
|
8656
|
+
const migrations = migrationFiles.map((f) => path17.basename(f)).filter((f) => !f.includes("ModelSnapshot") && !f.includes(".Designer.")).sort();
|
|
7381
8657
|
lines.push("## EF Core Migrations");
|
|
7382
8658
|
lines.push("");
|
|
7383
|
-
lines.push(`**Location**: \`${
|
|
8659
|
+
lines.push(`**Location**: \`${path17.relative(projectPath, structure.migrations)}\``);
|
|
7384
8660
|
lines.push(`**Total Migrations**: ${migrations.length}`);
|
|
7385
8661
|
lines.push("");
|
|
7386
8662
|
if (migrations.length > 0) {
|
|
@@ -7415,11 +8691,11 @@ async function getProjectInfoResource(config) {
|
|
|
7415
8691
|
lines.push("dotnet build");
|
|
7416
8692
|
lines.push("");
|
|
7417
8693
|
lines.push("# Run API");
|
|
7418
|
-
lines.push(`cd ${structure.api ?
|
|
8694
|
+
lines.push(`cd ${structure.api ? path17.relative(projectPath, structure.api) : "SmartStack.Api"}`);
|
|
7419
8695
|
lines.push("dotnet run");
|
|
7420
8696
|
lines.push("");
|
|
7421
8697
|
lines.push("# Run frontend");
|
|
7422
|
-
lines.push(`cd ${structure.web ?
|
|
8698
|
+
lines.push(`cd ${structure.web ? path17.relative(projectPath, structure.web) : "web/smartstack-web"}`);
|
|
7423
8699
|
lines.push("npm run dev");
|
|
7424
8700
|
lines.push("");
|
|
7425
8701
|
lines.push("# Create migration");
|
|
@@ -7442,7 +8718,7 @@ async function getProjectInfoResource(config) {
|
|
|
7442
8718
|
}
|
|
7443
8719
|
|
|
7444
8720
|
// src/resources/api-endpoints.ts
|
|
7445
|
-
import
|
|
8721
|
+
import path18 from "path";
|
|
7446
8722
|
var apiEndpointsResourceTemplate = {
|
|
7447
8723
|
uri: "smartstack://api/",
|
|
7448
8724
|
name: "SmartStack API Endpoints",
|
|
@@ -7467,7 +8743,7 @@ async function getApiEndpointsResource(config, endpointFilter) {
|
|
|
7467
8743
|
}
|
|
7468
8744
|
async function parseController(filePath, _rootPath) {
|
|
7469
8745
|
const content = await readText(filePath);
|
|
7470
|
-
const fileName =
|
|
8746
|
+
const fileName = path18.basename(filePath, ".cs");
|
|
7471
8747
|
const controllerName = fileName.replace("Controller", "");
|
|
7472
8748
|
const endpoints = [];
|
|
7473
8749
|
const routeMatch = content.match(/\[Route\s*\(\s*"([^"]+)"\s*\)\]/);
|
|
@@ -7614,7 +8890,7 @@ function getMethodEmoji(method) {
|
|
|
7614
8890
|
}
|
|
7615
8891
|
|
|
7616
8892
|
// src/resources/db-schema.ts
|
|
7617
|
-
import
|
|
8893
|
+
import path19 from "path";
|
|
7618
8894
|
var dbSchemaResourceTemplate = {
|
|
7619
8895
|
uri: "smartstack://schema/",
|
|
7620
8896
|
name: "SmartStack Database Schema",
|
|
@@ -7704,7 +8980,7 @@ async function parseEntity(filePath, rootPath, _config) {
|
|
|
7704
8980
|
tableName,
|
|
7705
8981
|
properties,
|
|
7706
8982
|
relationships,
|
|
7707
|
-
file:
|
|
8983
|
+
file: path19.relative(rootPath, filePath)
|
|
7708
8984
|
};
|
|
7709
8985
|
}
|
|
7710
8986
|
async function enrichFromConfigurations(entities, infrastructurePath, _config) {
|
|
@@ -7850,7 +9126,7 @@ function formatSchema(entities, filter, _config) {
|
|
|
7850
9126
|
}
|
|
7851
9127
|
|
|
7852
9128
|
// src/resources/entities.ts
|
|
7853
|
-
import
|
|
9129
|
+
import path20 from "path";
|
|
7854
9130
|
var entitiesResourceTemplate = {
|
|
7855
9131
|
uri: "smartstack://entities/",
|
|
7856
9132
|
name: "SmartStack Entities",
|
|
@@ -7910,7 +9186,7 @@ async function parseEntitySummary(filePath, rootPath, config) {
|
|
|
7910
9186
|
hasSoftDelete,
|
|
7911
9187
|
hasRowVersion,
|
|
7912
9188
|
file: filePath,
|
|
7913
|
-
relativePath:
|
|
9189
|
+
relativePath: path20.relative(rootPath, filePath)
|
|
7914
9190
|
};
|
|
7915
9191
|
}
|
|
7916
9192
|
function inferTableInfo(entityName, config) {
|
|
@@ -8102,7 +9378,11 @@ async function createServer() {
|
|
|
8102
9378
|
scaffoldTestsTool,
|
|
8103
9379
|
analyzeTestCoverageTool,
|
|
8104
9380
|
validateTestConventionsTool,
|
|
8105
|
-
suggestTestScenariosTool
|
|
9381
|
+
suggestTestScenariosTool,
|
|
9382
|
+
// Frontend Route Tools
|
|
9383
|
+
scaffoldApiClientTool,
|
|
9384
|
+
scaffoldRoutesTool,
|
|
9385
|
+
validateFrontendRoutesTool
|
|
8106
9386
|
]
|
|
8107
9387
|
};
|
|
8108
9388
|
});
|
|
@@ -8141,6 +9421,16 @@ async function createServer() {
|
|
|
8141
9421
|
case "suggest_test_scenarios":
|
|
8142
9422
|
result = await handleSuggestTestScenarios(args, config);
|
|
8143
9423
|
break;
|
|
9424
|
+
// Frontend Route Tools
|
|
9425
|
+
case "scaffold_api_client":
|
|
9426
|
+
result = await handleScaffoldApiClient(args ?? {}, config);
|
|
9427
|
+
break;
|
|
9428
|
+
case "scaffold_routes":
|
|
9429
|
+
result = await handleScaffoldRoutes(args ?? {}, config);
|
|
9430
|
+
break;
|
|
9431
|
+
case "validate_frontend_routes":
|
|
9432
|
+
result = await handleValidateFrontendRoutes(args ?? {}, config);
|
|
9433
|
+
break;
|
|
8144
9434
|
default:
|
|
8145
9435
|
throw new Error(`Unknown tool: ${name}`);
|
|
8146
9436
|
}
|