@atlashub/smartstack-mcp 1.5.1 → 1.8.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 +1661 -220
- 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
|
@@ -75,17 +75,17 @@ if (envLevel && ["debug", "info", "warn", "error"].includes(envLevel)) {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
// src/config.ts
|
|
78
|
-
import
|
|
78
|
+
import path3 from "path";
|
|
79
79
|
|
|
80
80
|
// src/utils/fs.ts
|
|
81
81
|
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, path22, cause) {
|
|
86
86
|
super(message);
|
|
87
87
|
this.operation = operation;
|
|
88
|
-
this.path =
|
|
88
|
+
this.path = path22;
|
|
89
89
|
this.cause = cause;
|
|
90
90
|
this.name = "FileSystemError";
|
|
91
91
|
}
|
|
@@ -184,6 +184,125 @@ async function findFiles(pattern, options = {}) {
|
|
|
184
184
|
return files;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
// src/utils/dotnet.ts
|
|
188
|
+
import { exec } from "child_process";
|
|
189
|
+
import { promisify } from "util";
|
|
190
|
+
import path2 from "path";
|
|
191
|
+
var execAsync = promisify(exec);
|
|
192
|
+
async function findCsprojFiles(cwd) {
|
|
193
|
+
return findFiles("**/*.csproj", { cwd: cwd || process.cwd() });
|
|
194
|
+
}
|
|
195
|
+
async function hasEfCore(csprojPath) {
|
|
196
|
+
try {
|
|
197
|
+
const content = await readText(csprojPath);
|
|
198
|
+
return content.includes("Microsoft.EntityFrameworkCore");
|
|
199
|
+
} catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function findDbContextName(projectPath) {
|
|
204
|
+
try {
|
|
205
|
+
const csFiles = await findFiles("**/*.cs", { cwd: projectPath });
|
|
206
|
+
for (const file of csFiles) {
|
|
207
|
+
const content = await readText(file);
|
|
208
|
+
const match = content.match(/class\s+(\w+)\s*:\s*(?:\w+,\s*)*DbContext/);
|
|
209
|
+
if (match) {
|
|
210
|
+
return match[1];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function getTargetFramework(csprojPath) {
|
|
219
|
+
try {
|
|
220
|
+
const content = await readText(csprojPath);
|
|
221
|
+
const match = content.match(/<TargetFramework>([^<]+)<\/TargetFramework>/);
|
|
222
|
+
return match ? match[1] : null;
|
|
223
|
+
} catch {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async function detectNamespaces(csprojFiles) {
|
|
228
|
+
if (csprojFiles.length === 0) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
const projectNames = csprojFiles.map((f) => path2.basename(f, ".csproj"));
|
|
232
|
+
const layerSuffixes = ["Domain", "Application", "Infrastructure", "Api", "Web", "Core", "Tests"];
|
|
233
|
+
const baseNamespaces = [];
|
|
234
|
+
for (const name of projectNames) {
|
|
235
|
+
const parts = name.split(".");
|
|
236
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
237
|
+
const part = parts[i];
|
|
238
|
+
if (layerSuffixes.some((s) => s.toLowerCase() === part.toLowerCase())) {
|
|
239
|
+
const baseParts = parts.slice(0, i);
|
|
240
|
+
if (baseParts.length > 0) {
|
|
241
|
+
baseNamespaces.push(baseParts.join("."));
|
|
242
|
+
}
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (baseNamespaces.length === 0) {
|
|
248
|
+
const commonPrefix = findCommonPrefix(projectNames);
|
|
249
|
+
if (commonPrefix && commonPrefix.length > 0) {
|
|
250
|
+
const base = commonPrefix.replace(/\.$/, "");
|
|
251
|
+
if (base) {
|
|
252
|
+
baseNamespaces.push(base);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (baseNamespaces.length === 0) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
const baseNamespace = getMostCommon(baseNamespaces) || baseNamespaces[0];
|
|
260
|
+
return {
|
|
261
|
+
baseNamespace,
|
|
262
|
+
domain: `${baseNamespace}.Domain`,
|
|
263
|
+
application: `${baseNamespace}.Application`,
|
|
264
|
+
infrastructure: `${baseNamespace}.Infrastructure`,
|
|
265
|
+
api: `${baseNamespace}.Api`
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function findCommonPrefix(strings) {
|
|
269
|
+
if (strings.length === 0) return "";
|
|
270
|
+
if (strings.length === 1) {
|
|
271
|
+
const parts = strings[0].split(".");
|
|
272
|
+
if (parts.length > 1) {
|
|
273
|
+
return parts.slice(0, -1).join(".") + ".";
|
|
274
|
+
}
|
|
275
|
+
return strings[0] + ".";
|
|
276
|
+
}
|
|
277
|
+
let prefix = strings[0];
|
|
278
|
+
for (let i = 1; i < strings.length; i++) {
|
|
279
|
+
while (strings[i].indexOf(prefix) !== 0 && prefix.length > 0) {
|
|
280
|
+
prefix = prefix.slice(0, -1);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const lastDot = prefix.lastIndexOf(".");
|
|
284
|
+
if (lastDot > 0 && !prefix.endsWith(".")) {
|
|
285
|
+
prefix = prefix.slice(0, lastDot + 1);
|
|
286
|
+
}
|
|
287
|
+
return prefix;
|
|
288
|
+
}
|
|
289
|
+
function getMostCommon(arr) {
|
|
290
|
+
if (arr.length === 0) return null;
|
|
291
|
+
const counts = /* @__PURE__ */ new Map();
|
|
292
|
+
for (const item of arr) {
|
|
293
|
+
counts.set(item, (counts.get(item) || 0) + 1);
|
|
294
|
+
}
|
|
295
|
+
let maxCount = 0;
|
|
296
|
+
let mostCommon = arr[0];
|
|
297
|
+
for (const [item, count] of counts) {
|
|
298
|
+
if (count > maxCount) {
|
|
299
|
+
maxCount = count;
|
|
300
|
+
mostCommon = item;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return mostCommon;
|
|
304
|
+
}
|
|
305
|
+
|
|
187
306
|
// src/config.ts
|
|
188
307
|
var defaultConfig = {
|
|
189
308
|
version: "1.0.0",
|
|
@@ -218,10 +337,12 @@ var defaultConfig = {
|
|
|
218
337
|
},
|
|
219
338
|
migrationFormat: "{context}_v{version}_{sequence}_{Description}",
|
|
220
339
|
namespaces: {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
340
|
+
// Empty = auto-detect from .csproj files
|
|
341
|
+
// Can be overridden in config file for custom namespaces
|
|
342
|
+
domain: "",
|
|
343
|
+
application: "",
|
|
344
|
+
infrastructure: "",
|
|
345
|
+
api: ""
|
|
225
346
|
},
|
|
226
347
|
servicePattern: {
|
|
227
348
|
interface: "I{Name}Service",
|
|
@@ -268,7 +389,7 @@ async function getConfig() {
|
|
|
268
389
|
if (cachedConfig) {
|
|
269
390
|
return cachedConfig;
|
|
270
391
|
}
|
|
271
|
-
const configPath =
|
|
392
|
+
const configPath = path3.join(process.cwd(), "config", "default-config.json");
|
|
272
393
|
if (await fileExists(configPath)) {
|
|
273
394
|
try {
|
|
274
395
|
const fileConfig = await readJson(configPath);
|
|
@@ -285,6 +406,32 @@ async function getConfig() {
|
|
|
285
406
|
cachedConfig.smartstack.projectPath = resolveProjectPath(
|
|
286
407
|
cachedConfig.smartstack.projectPath
|
|
287
408
|
);
|
|
409
|
+
const namespacesEmpty = !cachedConfig.conventions.namespaces.domain || !cachedConfig.conventions.namespaces.application || !cachedConfig.conventions.namespaces.infrastructure || !cachedConfig.conventions.namespaces.api;
|
|
410
|
+
if (namespacesEmpty && cachedConfig.smartstack.projectPath) {
|
|
411
|
+
try {
|
|
412
|
+
const csprojFiles = await findCsprojFiles(cachedConfig.smartstack.projectPath);
|
|
413
|
+
const detected = await detectNamespaces(csprojFiles);
|
|
414
|
+
if (detected) {
|
|
415
|
+
if (!cachedConfig.conventions.namespaces.domain) {
|
|
416
|
+
cachedConfig.conventions.namespaces.domain = detected.domain;
|
|
417
|
+
}
|
|
418
|
+
if (!cachedConfig.conventions.namespaces.application) {
|
|
419
|
+
cachedConfig.conventions.namespaces.application = detected.application;
|
|
420
|
+
}
|
|
421
|
+
if (!cachedConfig.conventions.namespaces.infrastructure) {
|
|
422
|
+
cachedConfig.conventions.namespaces.infrastructure = detected.infrastructure;
|
|
423
|
+
}
|
|
424
|
+
if (!cachedConfig.conventions.namespaces.api) {
|
|
425
|
+
cachedConfig.conventions.namespaces.api = detected.api;
|
|
426
|
+
}
|
|
427
|
+
logger.info("Namespaces auto-detected from project", { baseNamespace: detected.baseNamespace });
|
|
428
|
+
} else {
|
|
429
|
+
logger.warn("Could not auto-detect namespaces from .csproj files. Configure manually in config file.");
|
|
430
|
+
}
|
|
431
|
+
} catch (error) {
|
|
432
|
+
logger.warn("Failed to auto-detect namespaces", { error });
|
|
433
|
+
}
|
|
434
|
+
}
|
|
288
435
|
if (process.env.SMARTSTACK_API_URL) {
|
|
289
436
|
cachedConfig.smartstack.apiUrl = process.env.SMARTSTACK_API_URL;
|
|
290
437
|
}
|
|
@@ -491,15 +638,47 @@ var SuggestTestScenariosInputSchema = z.object({
|
|
|
491
638
|
name: z.string().min(1).describe("Component name or file path"),
|
|
492
639
|
depth: z.enum(["basic", "comprehensive", "security-focused"]).default("comprehensive").describe("Depth of analysis")
|
|
493
640
|
});
|
|
641
|
+
var ScaffoldApiClientInputSchema = z.object({
|
|
642
|
+
path: z.string().optional().describe("Path to SmartStack project root (defaults to configured project path)"),
|
|
643
|
+
navRoute: z.string().min(1).describe('NavRoute path (e.g., "platform.administration.users")'),
|
|
644
|
+
name: z.string().min(1).describe('Entity name in PascalCase (e.g., "User", "Order")'),
|
|
645
|
+
methods: z.array(z.enum(["getAll", "getById", "create", "update", "delete", "search", "export"])).default(["getAll", "getById", "create", "update", "delete"]).describe("API methods to generate"),
|
|
646
|
+
options: z.object({
|
|
647
|
+
outputPath: z.string().optional().describe("Custom output path for generated files"),
|
|
648
|
+
includeTypes: z.boolean().default(true).describe("Generate TypeScript types"),
|
|
649
|
+
includeHook: z.boolean().default(true).describe("Generate React Query hook"),
|
|
650
|
+
dryRun: z.boolean().default(false).describe("Preview without writing files")
|
|
651
|
+
}).optional()
|
|
652
|
+
});
|
|
653
|
+
var ScaffoldRoutesInputSchema = z.object({
|
|
654
|
+
path: z.string().optional().describe("Path to SmartStack project root (defaults to configured project path)"),
|
|
655
|
+
source: z.enum(["controllers", "navigation", "manual"]).default("controllers").describe("Source for route discovery: controllers (scan NavRoute attributes), navigation (from DB), manual (from config)"),
|
|
656
|
+
scope: z.enum(["all", "platform", "business", "extensions"]).default("all").describe("Scope of routes to generate"),
|
|
657
|
+
options: z.object({
|
|
658
|
+
outputPath: z.string().optional().describe("Custom output path"),
|
|
659
|
+
includeLayouts: z.boolean().default(true).describe("Generate layout components"),
|
|
660
|
+
includeGuards: z.boolean().default(true).describe("Include route guards for permissions"),
|
|
661
|
+
generateRegistry: z.boolean().default(true).describe("Generate navRoutes.generated.ts"),
|
|
662
|
+
dryRun: z.boolean().default(false).describe("Preview without writing files")
|
|
663
|
+
}).optional()
|
|
664
|
+
});
|
|
665
|
+
var ValidateFrontendRoutesInputSchema = z.object({
|
|
666
|
+
path: z.string().optional().describe("Path to SmartStack project root (defaults to configured project path)"),
|
|
667
|
+
scope: z.enum(["api-clients", "routes", "registry", "all"]).default("all").describe("Scope of validation"),
|
|
668
|
+
options: z.object({
|
|
669
|
+
fix: z.boolean().default(false).describe("Auto-fix minor issues"),
|
|
670
|
+
strict: z.boolean().default(false).describe("Fail on warnings")
|
|
671
|
+
}).optional()
|
|
672
|
+
});
|
|
494
673
|
|
|
495
674
|
// src/lib/detector.ts
|
|
496
|
-
import
|
|
675
|
+
import path5 from "path";
|
|
497
676
|
|
|
498
677
|
// src/utils/git.ts
|
|
499
|
-
import { exec } from "child_process";
|
|
500
|
-
import { promisify } from "util";
|
|
501
|
-
import
|
|
502
|
-
var
|
|
678
|
+
import { exec as exec2 } from "child_process";
|
|
679
|
+
import { promisify as promisify2 } from "util";
|
|
680
|
+
import path4 from "path";
|
|
681
|
+
var execAsync2 = promisify2(exec2);
|
|
503
682
|
var GitError = class extends Error {
|
|
504
683
|
constructor(message, command, cwd, cause) {
|
|
505
684
|
super(message);
|
|
@@ -512,7 +691,7 @@ var GitError = class extends Error {
|
|
|
512
691
|
async function git(command, cwd) {
|
|
513
692
|
const options = cwd ? { cwd, maxBuffer: 10 * 1024 * 1024 } : { maxBuffer: 10 * 1024 * 1024 };
|
|
514
693
|
try {
|
|
515
|
-
const { stdout } = await
|
|
694
|
+
const { stdout } = await execAsync2(`git ${command}`, options);
|
|
516
695
|
return stdout.trim();
|
|
517
696
|
} catch (error) {
|
|
518
697
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -526,7 +705,7 @@ async function git(command, cwd) {
|
|
|
526
705
|
}
|
|
527
706
|
}
|
|
528
707
|
async function isGitRepo(cwd) {
|
|
529
|
-
const gitDir =
|
|
708
|
+
const gitDir = path4.join(cwd || process.cwd(), ".git");
|
|
530
709
|
return directoryExists(gitDir);
|
|
531
710
|
}
|
|
532
711
|
async function getCurrentBranch(cwd) {
|
|
@@ -560,51 +739,37 @@ async function getDiff(fromBranch, toBranch, filePath, cwd) {
|
|
|
560
739
|
}
|
|
561
740
|
}
|
|
562
741
|
|
|
563
|
-
// src/
|
|
564
|
-
import {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
const content = await readText(csprojPath);
|
|
573
|
-
return content.includes("Microsoft.EntityFrameworkCore");
|
|
574
|
-
} catch {
|
|
575
|
-
return false;
|
|
742
|
+
// src/lib/detector.ts
|
|
743
|
+
import { glob as glob2 } from "glob";
|
|
744
|
+
async function findWebProjectFolder(projectPath) {
|
|
745
|
+
const webSubfolders = await glob2("web/*/", { cwd: projectPath, absolute: true });
|
|
746
|
+
for (const folder of webSubfolders) {
|
|
747
|
+
const packageJsonPath = path5.join(folder, "package.json");
|
|
748
|
+
if (await fileExists(packageJsonPath)) {
|
|
749
|
+
return folder;
|
|
750
|
+
}
|
|
576
751
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
}
|
|
752
|
+
const webDirect = path5.join(projectPath, "web");
|
|
753
|
+
if (await fileExists(path5.join(webDirect, "package.json"))) {
|
|
754
|
+
return webDirect;
|
|
755
|
+
}
|
|
756
|
+
const alternativeFolders = ["client", "frontend", "ui"];
|
|
757
|
+
for (const folderName of alternativeFolders) {
|
|
758
|
+
const altPath = path5.join(projectPath, folderName);
|
|
759
|
+
if (await fileExists(path5.join(altPath, "package.json"))) {
|
|
760
|
+
return altPath;
|
|
587
761
|
}
|
|
588
|
-
return null;
|
|
589
|
-
} catch {
|
|
590
|
-
return null;
|
|
591
762
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
const content = await readText(csprojPath);
|
|
596
|
-
const match = content.match(/<TargetFramework>([^<]+)<\/TargetFramework>/);
|
|
597
|
-
return match ? match[1] : null;
|
|
598
|
-
} catch {
|
|
599
|
-
return null;
|
|
763
|
+
const srcWeb = path5.join(projectPath, "src", "web");
|
|
764
|
+
if (await fileExists(path5.join(srcWeb, "package.json"))) {
|
|
765
|
+
return srcWeb;
|
|
600
766
|
}
|
|
767
|
+
return null;
|
|
601
768
|
}
|
|
602
|
-
|
|
603
|
-
// src/lib/detector.ts
|
|
604
769
|
async function detectProject(projectPath) {
|
|
605
770
|
logger.debug("Detecting project info", { path: projectPath });
|
|
606
771
|
const info = {
|
|
607
|
-
name:
|
|
772
|
+
name: path5.basename(projectPath),
|
|
608
773
|
version: "0.0.0",
|
|
609
774
|
isGitRepo: false,
|
|
610
775
|
hasDotNet: false,
|
|
@@ -626,22 +791,25 @@ async function detectProject(projectPath) {
|
|
|
626
791
|
for (const csproj of info.csprojFiles) {
|
|
627
792
|
if (await hasEfCore(csproj)) {
|
|
628
793
|
info.hasEfCore = true;
|
|
629
|
-
info.dbContextName = await findDbContextName(
|
|
794
|
+
info.dbContextName = await findDbContextName(path5.dirname(csproj)) || void 0;
|
|
630
795
|
break;
|
|
631
796
|
}
|
|
632
797
|
}
|
|
633
798
|
}
|
|
634
|
-
const
|
|
635
|
-
if (
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
799
|
+
const webFolder = await findWebProjectFolder(projectPath);
|
|
800
|
+
if (webFolder) {
|
|
801
|
+
const packageJsonPath = path5.join(webFolder, "package.json");
|
|
802
|
+
if (await fileExists(packageJsonPath)) {
|
|
803
|
+
try {
|
|
804
|
+
const packageJson = JSON.parse(await readText(packageJsonPath));
|
|
805
|
+
info.hasReact = !!packageJson.dependencies?.react;
|
|
806
|
+
info.version = packageJson.version || info.version;
|
|
807
|
+
} catch {
|
|
808
|
+
}
|
|
641
809
|
}
|
|
642
810
|
}
|
|
643
811
|
if (info.csprojFiles.length > 0) {
|
|
644
|
-
const mainCsproj = info.csprojFiles.find((f) => f.
|
|
812
|
+
const mainCsproj = info.csprojFiles.find((f) => /\.Api\.csproj$/i.test(f)) || info.csprojFiles.find((f) => /Api\.csproj$/i.test(f)) || info.csprojFiles[0];
|
|
645
813
|
const targetFramework = await getTargetFramework(mainCsproj);
|
|
646
814
|
if (targetFramework) {
|
|
647
815
|
logger.debug("Target framework detected", { framework: targetFramework });
|
|
@@ -654,8 +822,8 @@ async function findSmartStackStructure(projectPath) {
|
|
|
654
822
|
const structure = { root: projectPath };
|
|
655
823
|
const csprojFiles = await findCsprojFiles(projectPath);
|
|
656
824
|
for (const csproj of csprojFiles) {
|
|
657
|
-
const projectName =
|
|
658
|
-
const projectDir =
|
|
825
|
+
const projectName = path5.basename(csproj, ".csproj").toLowerCase();
|
|
826
|
+
const projectDir = path5.dirname(csproj);
|
|
659
827
|
if (projectName.includes("domain")) {
|
|
660
828
|
structure.domain = projectDir;
|
|
661
829
|
} else if (projectName.includes("application")) {
|
|
@@ -667,14 +835,14 @@ async function findSmartStackStructure(projectPath) {
|
|
|
667
835
|
}
|
|
668
836
|
}
|
|
669
837
|
if (structure.infrastructure) {
|
|
670
|
-
const migrationsPath =
|
|
838
|
+
const migrationsPath = path5.join(structure.infrastructure, "Persistence", "Migrations");
|
|
671
839
|
if (await directoryExists(migrationsPath)) {
|
|
672
840
|
structure.migrations = migrationsPath;
|
|
673
841
|
}
|
|
674
842
|
}
|
|
675
|
-
const
|
|
676
|
-
if (
|
|
677
|
-
structure.web =
|
|
843
|
+
const webFolder = await findWebProjectFolder(projectPath);
|
|
844
|
+
if (webFolder) {
|
|
845
|
+
structure.web = webFolder;
|
|
678
846
|
}
|
|
679
847
|
return structure;
|
|
680
848
|
}
|
|
@@ -695,7 +863,7 @@ async function findControllerFiles(apiPath) {
|
|
|
695
863
|
}
|
|
696
864
|
|
|
697
865
|
// src/tools/validate-conventions.ts
|
|
698
|
-
import
|
|
866
|
+
import path6 from "path";
|
|
699
867
|
var validateConventionsTool = {
|
|
700
868
|
name: "validate_conventions",
|
|
701
869
|
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)",
|
|
@@ -784,7 +952,7 @@ async function validateTablePrefixes(structure, config, result) {
|
|
|
784
952
|
type: "error",
|
|
785
953
|
category: "tables",
|
|
786
954
|
message: `Table "${tableName}" uses invalid schema "${schemaName}"`,
|
|
787
|
-
file:
|
|
955
|
+
file: path6.relative(structure.root, file),
|
|
788
956
|
suggestion: `Use schema "${config.conventions.schemas.platform}" for SmartStack tables or "${config.conventions.schemas.extensions}" for client extensions`
|
|
789
957
|
});
|
|
790
958
|
}
|
|
@@ -794,7 +962,7 @@ async function validateTablePrefixes(structure, config, result) {
|
|
|
794
962
|
type: "warning",
|
|
795
963
|
category: "tables",
|
|
796
964
|
message: `Table "${tableName}" does not use a standard domain prefix`,
|
|
797
|
-
file:
|
|
965
|
+
file: path6.relative(structure.root, file),
|
|
798
966
|
suggestion: `Consider using a domain prefix: ${validPrefixes.slice(0, 5).join(", ")}, etc.`
|
|
799
967
|
});
|
|
800
968
|
}
|
|
@@ -809,7 +977,7 @@ async function validateTablePrefixes(structure, config, result) {
|
|
|
809
977
|
type: "error",
|
|
810
978
|
category: "tables",
|
|
811
979
|
message: `Table "${tableName}" uses unknown schema constant "${schemaConstant}"`,
|
|
812
|
-
file:
|
|
980
|
+
file: path6.relative(structure.root, file),
|
|
813
981
|
suggestion: `Use SchemaConstants.Core for SmartStack tables or SchemaConstants.Extensions for client extensions`
|
|
814
982
|
});
|
|
815
983
|
}
|
|
@@ -819,7 +987,7 @@ async function validateTablePrefixes(structure, config, result) {
|
|
|
819
987
|
type: "warning",
|
|
820
988
|
category: "tables",
|
|
821
989
|
message: `Table "${tableName}" does not use a standard domain prefix`,
|
|
822
|
-
file:
|
|
990
|
+
file: path6.relative(structure.root, file),
|
|
823
991
|
suggestion: `Consider using a domain prefix: ${validPrefixes.slice(0, 5).join(", ")}, etc.`
|
|
824
992
|
});
|
|
825
993
|
}
|
|
@@ -832,7 +1000,7 @@ async function validateTablePrefixes(structure, config, result) {
|
|
|
832
1000
|
type: "error",
|
|
833
1001
|
category: "tables",
|
|
834
1002
|
message: `Table "${tableName}" is missing schema specification`,
|
|
835
|
-
file:
|
|
1003
|
+
file: path6.relative(structure.root, file),
|
|
836
1004
|
suggestion: `Add schema: .ToTable("${tableName}", SchemaConstants.Core)`
|
|
837
1005
|
});
|
|
838
1006
|
}
|
|
@@ -852,7 +1020,7 @@ async function validateMigrationNaming(structure, _config, result) {
|
|
|
852
1020
|
const migrationPattern = /^(\w+)_v(\d+\.\d+\.\d+)_(\d{3})_(.+)\.cs$/;
|
|
853
1021
|
const designerPattern = /\.Designer\.cs$/;
|
|
854
1022
|
for (const file of migrationFiles) {
|
|
855
|
-
const fileName =
|
|
1023
|
+
const fileName = path6.basename(file);
|
|
856
1024
|
if (designerPattern.test(fileName) || fileName.includes("ModelSnapshot")) {
|
|
857
1025
|
continue;
|
|
858
1026
|
}
|
|
@@ -861,12 +1029,12 @@ async function validateMigrationNaming(structure, _config, result) {
|
|
|
861
1029
|
type: "error",
|
|
862
1030
|
category: "migrations",
|
|
863
1031
|
message: `Migration "${fileName}" does not follow naming convention`,
|
|
864
|
-
file:
|
|
1032
|
+
file: path6.relative(structure.root, file),
|
|
865
1033
|
suggestion: `Expected format: {context}_v{version}_{sequence}_{Description}.cs (e.g., core_v1.0.0_001_CreateAuthUsers.cs)`
|
|
866
1034
|
});
|
|
867
1035
|
}
|
|
868
1036
|
}
|
|
869
|
-
const orderedMigrations = migrationFiles.map((f) =>
|
|
1037
|
+
const orderedMigrations = migrationFiles.map((f) => path6.basename(f)).filter((f) => migrationPattern.test(f) && !f.includes("Designer")).sort();
|
|
870
1038
|
for (let i = 1; i < orderedMigrations.length; i++) {
|
|
871
1039
|
const prev = orderedMigrations[i - 1];
|
|
872
1040
|
const curr = orderedMigrations[i];
|
|
@@ -899,7 +1067,7 @@ async function validateServiceInterfaces(structure, _config, result) {
|
|
|
899
1067
|
});
|
|
900
1068
|
for (const file of serviceFiles) {
|
|
901
1069
|
const content = await readText(file);
|
|
902
|
-
const fileName =
|
|
1070
|
+
const fileName = path6.basename(file, ".cs");
|
|
903
1071
|
if (fileName.startsWith("I")) continue;
|
|
904
1072
|
const expectedInterface = `I${fileName}`;
|
|
905
1073
|
const interfacePattern = new RegExp(`:\\s*${expectedInterface}\\b`);
|
|
@@ -910,7 +1078,7 @@ async function validateServiceInterfaces(structure, _config, result) {
|
|
|
910
1078
|
type: "warning",
|
|
911
1079
|
category: "services",
|
|
912
1080
|
message: `Service "${fileName}" should implement "${expectedInterface}"`,
|
|
913
|
-
file:
|
|
1081
|
+
file: path6.relative(structure.root, file),
|
|
914
1082
|
suggestion: `Create interface ${expectedInterface} and implement it`
|
|
915
1083
|
});
|
|
916
1084
|
}
|
|
@@ -937,7 +1105,7 @@ async function validateNamespaces(structure, config, result) {
|
|
|
937
1105
|
type: "error",
|
|
938
1106
|
category: "namespaces",
|
|
939
1107
|
message: `${layer.name} file has incorrect namespace "${namespace}"`,
|
|
940
|
-
file:
|
|
1108
|
+
file: path6.relative(structure.root, file),
|
|
941
1109
|
suggestion: `Should start with "${layer.expected}"`
|
|
942
1110
|
});
|
|
943
1111
|
}
|
|
@@ -957,7 +1125,7 @@ async function validateEntities(structure, _config, result) {
|
|
|
957
1125
|
const entityFiles = await findFiles("**/*.cs", { cwd: structure.domain });
|
|
958
1126
|
for (const file of entityFiles) {
|
|
959
1127
|
const content = await readText(file);
|
|
960
|
-
const fileName =
|
|
1128
|
+
const fileName = path6.basename(file, ".cs");
|
|
961
1129
|
if (fileName.endsWith("Dto") || fileName.endsWith("Command") || fileName.endsWith("Query") || fileName.endsWith("Handler") || fileName.endsWith("Validator") || fileName.endsWith("Exception") || fileName.startsWith("I")) {
|
|
962
1130
|
continue;
|
|
963
1131
|
}
|
|
@@ -976,7 +1144,7 @@ async function validateEntities(structure, _config, result) {
|
|
|
976
1144
|
type: "warning",
|
|
977
1145
|
category: "entities",
|
|
978
1146
|
message: `Entity "${entityName}" inherits BaseEntity but doesn't implement ITenantEntity`,
|
|
979
|
-
file:
|
|
1147
|
+
file: path6.relative(structure.root, file),
|
|
980
1148
|
suggestion: "Add ITenantEntity interface for multi-tenant support, or use SystemEntity for platform-level entities"
|
|
981
1149
|
});
|
|
982
1150
|
}
|
|
@@ -985,7 +1153,7 @@ async function validateEntities(structure, _config, result) {
|
|
|
985
1153
|
type: "warning",
|
|
986
1154
|
category: "entities",
|
|
987
1155
|
message: `Entity "${entityName}" is missing private constructor for EF Core`,
|
|
988
|
-
file:
|
|
1156
|
+
file: path6.relative(structure.root, file),
|
|
989
1157
|
suggestion: `Add: private ${entityName}() { }`
|
|
990
1158
|
});
|
|
991
1159
|
}
|
|
@@ -994,7 +1162,7 @@ async function validateEntities(structure, _config, result) {
|
|
|
994
1162
|
type: "warning",
|
|
995
1163
|
category: "entities",
|
|
996
1164
|
message: `Entity "${entityName}" is missing factory method`,
|
|
997
|
-
file:
|
|
1165
|
+
file: path6.relative(structure.root, file),
|
|
998
1166
|
suggestion: `Add factory method: public static ${entityName} Create(...)`
|
|
999
1167
|
});
|
|
1000
1168
|
}
|
|
@@ -1033,7 +1201,7 @@ async function validateTenantAwareness(structure, _config, result) {
|
|
|
1033
1201
|
type: "error",
|
|
1034
1202
|
category: "tenants",
|
|
1035
1203
|
message: `Entity "${entityName}" implements ITenantEntity but is missing TenantId property`,
|
|
1036
|
-
file:
|
|
1204
|
+
file: path6.relative(structure.root, file),
|
|
1037
1205
|
suggestion: "Add: public Guid TenantId { get; private set; }"
|
|
1038
1206
|
});
|
|
1039
1207
|
}
|
|
@@ -1043,7 +1211,7 @@ async function validateTenantAwareness(structure, _config, result) {
|
|
|
1043
1211
|
type: "error",
|
|
1044
1212
|
category: "tenants",
|
|
1045
1213
|
message: `Entity "${entityName}" implements ITenantEntity but Create() doesn't require tenantId`,
|
|
1046
|
-
file:
|
|
1214
|
+
file: path6.relative(structure.root, file),
|
|
1047
1215
|
suggestion: "Add tenantId as first parameter: Create(Guid tenantId, ...)"
|
|
1048
1216
|
});
|
|
1049
1217
|
}
|
|
@@ -1055,7 +1223,7 @@ async function validateTenantAwareness(structure, _config, result) {
|
|
|
1055
1223
|
type: "error",
|
|
1056
1224
|
category: "tenants",
|
|
1057
1225
|
message: `System entity "${entityName}" should not have TenantId`,
|
|
1058
|
-
file:
|
|
1226
|
+
file: path6.relative(structure.root, file),
|
|
1059
1227
|
suggestion: "Remove TenantId from system entities"
|
|
1060
1228
|
});
|
|
1061
1229
|
}
|
|
@@ -1066,7 +1234,7 @@ async function validateTenantAwareness(structure, _config, result) {
|
|
|
1066
1234
|
type: "warning",
|
|
1067
1235
|
category: "tenants",
|
|
1068
1236
|
message: `Entity "${entityName}" has TenantId but doesn't implement ITenantEntity`,
|
|
1069
|
-
file:
|
|
1237
|
+
file: path6.relative(structure.root, file),
|
|
1070
1238
|
suggestion: "Add ITenantEntity interface for explicit tenant-awareness"
|
|
1071
1239
|
});
|
|
1072
1240
|
}
|
|
@@ -1121,7 +1289,7 @@ async function validateControllerRoutes(structure, _config, result) {
|
|
|
1121
1289
|
let systemControllerCount = 0;
|
|
1122
1290
|
for (const file of controllerFiles) {
|
|
1123
1291
|
const content = await readText(file);
|
|
1124
|
-
const fileName =
|
|
1292
|
+
const fileName = path6.basename(file, ".cs");
|
|
1125
1293
|
if (systemControllers.includes(fileName)) {
|
|
1126
1294
|
systemControllerCount++;
|
|
1127
1295
|
continue;
|
|
@@ -1139,7 +1307,7 @@ async function validateControllerRoutes(structure, _config, result) {
|
|
|
1139
1307
|
type: "warning",
|
|
1140
1308
|
category: "controllers",
|
|
1141
1309
|
message: `Controller "${fileName}" has NavRoute with insufficient depth: "${routePath}"`,
|
|
1142
|
-
file:
|
|
1310
|
+
file: path6.relative(structure.root, file),
|
|
1143
1311
|
suggestion: 'NavRoute should have at least 2 levels: "context.application" (e.g., "platform.administration")'
|
|
1144
1312
|
});
|
|
1145
1313
|
}
|
|
@@ -1149,7 +1317,7 @@ async function validateControllerRoutes(structure, _config, result) {
|
|
|
1149
1317
|
type: "error",
|
|
1150
1318
|
category: "controllers",
|
|
1151
1319
|
message: `Controller "${fileName}" has NavRoute with uppercase characters: "${routePath}"`,
|
|
1152
|
-
file:
|
|
1320
|
+
file: path6.relative(structure.root, file),
|
|
1153
1321
|
suggestion: 'NavRoute paths must be lowercase (e.g., "platform.administration.users")'
|
|
1154
1322
|
});
|
|
1155
1323
|
}
|
|
@@ -1160,7 +1328,7 @@ async function validateControllerRoutes(structure, _config, result) {
|
|
|
1160
1328
|
type: "warning",
|
|
1161
1329
|
category: "controllers",
|
|
1162
1330
|
message: `Controller "${fileName}" uses hardcoded Route instead of NavRoute`,
|
|
1163
|
-
file:
|
|
1331
|
+
file: path6.relative(structure.root, file),
|
|
1164
1332
|
suggestion: 'Use [NavRoute("context.application.module")] for navigation-based routing'
|
|
1165
1333
|
});
|
|
1166
1334
|
}
|
|
@@ -1228,7 +1396,7 @@ function formatResult(result) {
|
|
|
1228
1396
|
}
|
|
1229
1397
|
|
|
1230
1398
|
// src/tools/check-migrations.ts
|
|
1231
|
-
import
|
|
1399
|
+
import path7 from "path";
|
|
1232
1400
|
var checkMigrationsTool = {
|
|
1233
1401
|
name: "check_migrations",
|
|
1234
1402
|
description: "Analyze EF Core migrations for conflicts, ordering issues, and ModelSnapshot discrepancies between branches",
|
|
@@ -1287,7 +1455,7 @@ async function parseMigrations(migrationsPath, rootPath) {
|
|
|
1287
1455
|
const migrations = [];
|
|
1288
1456
|
const pattern = /^(\w+)_v(\d+\.\d+\.\d+)_(\d{3})_(.+)\.cs$/;
|
|
1289
1457
|
for (const file of files) {
|
|
1290
|
-
const fileName =
|
|
1458
|
+
const fileName = path7.basename(file);
|
|
1291
1459
|
if (fileName.includes(".Designer.") || fileName.includes("ModelSnapshot")) {
|
|
1292
1460
|
continue;
|
|
1293
1461
|
}
|
|
@@ -1302,7 +1470,7 @@ async function parseMigrations(migrationsPath, rootPath) {
|
|
|
1302
1470
|
sequence: match[3],
|
|
1303
1471
|
// Sequence number (001, 002, etc.)
|
|
1304
1472
|
description: match[4],
|
|
1305
|
-
file:
|
|
1473
|
+
file: path7.relative(rootPath, file),
|
|
1306
1474
|
applied: true
|
|
1307
1475
|
// We'd need DB connection to check this
|
|
1308
1476
|
});
|
|
@@ -1313,7 +1481,7 @@ async function parseMigrations(migrationsPath, rootPath) {
|
|
|
1313
1481
|
version: "0.0.0",
|
|
1314
1482
|
sequence: "000",
|
|
1315
1483
|
description: fileName.replace(".cs", ""),
|
|
1316
|
-
file:
|
|
1484
|
+
file: path7.relative(rootPath, file),
|
|
1317
1485
|
applied: true
|
|
1318
1486
|
});
|
|
1319
1487
|
}
|
|
@@ -1403,10 +1571,10 @@ function checkChronologicalOrder(result) {
|
|
|
1403
1571
|
}
|
|
1404
1572
|
async function checkBranchConflicts(result, structure, currentBranch, compareBranch, projectPath) {
|
|
1405
1573
|
if (!structure.migrations) return;
|
|
1406
|
-
const migrationsRelPath =
|
|
1574
|
+
const migrationsRelPath = path7.relative(projectPath, structure.migrations).replace(/\\/g, "/");
|
|
1407
1575
|
const snapshotFiles = await findFiles("*ModelSnapshot.cs", { cwd: structure.migrations });
|
|
1408
1576
|
if (snapshotFiles.length > 0) {
|
|
1409
|
-
const snapshotRelPath =
|
|
1577
|
+
const snapshotRelPath = path7.relative(projectPath, snapshotFiles[0]).replace(/\\/g, "/");
|
|
1410
1578
|
const currentSnapshot = await readText(snapshotFiles[0]);
|
|
1411
1579
|
const compareSnapshot = await getFileFromBranch(compareBranch, snapshotRelPath, projectPath);
|
|
1412
1580
|
if (compareSnapshot && currentSnapshot !== compareSnapshot) {
|
|
@@ -1446,7 +1614,7 @@ async function checkModelSnapshot(result, structure) {
|
|
|
1446
1614
|
result.conflicts.push({
|
|
1447
1615
|
type: "snapshot",
|
|
1448
1616
|
description: "Multiple ModelSnapshot files found",
|
|
1449
|
-
files: snapshotFiles.map((f) =>
|
|
1617
|
+
files: snapshotFiles.map((f) => path7.relative(structure.root, f)),
|
|
1450
1618
|
resolution: "Remove duplicate snapshots, keep only one per DbContext"
|
|
1451
1619
|
});
|
|
1452
1620
|
}
|
|
@@ -1533,7 +1701,7 @@ function formatResult2(result, currentBranch, compareBranch) {
|
|
|
1533
1701
|
|
|
1534
1702
|
// src/tools/scaffold-extension.ts
|
|
1535
1703
|
import Handlebars from "handlebars";
|
|
1536
|
-
import
|
|
1704
|
+
import path8 from "path";
|
|
1537
1705
|
var scaffoldExtensionTool = {
|
|
1538
1706
|
name: "scaffold_extension",
|
|
1539
1707
|
description: "Generate code to extend SmartStack: feature (full-stack), entity, service, controller, component, dto, validator, repository, or test",
|
|
@@ -1862,9 +2030,9 @@ services.AddScoped<I{{name}}Service, {{name}}Service>();
|
|
|
1862
2030
|
const diContent = Handlebars.compile(diTemplate)(context);
|
|
1863
2031
|
const projectRoot = config.smartstack.projectPath;
|
|
1864
2032
|
const basePath = structure.application || projectRoot;
|
|
1865
|
-
const servicesPath =
|
|
1866
|
-
const interfacePath =
|
|
1867
|
-
const implementationPath =
|
|
2033
|
+
const servicesPath = path8.join(basePath, "Services");
|
|
2034
|
+
const interfacePath = path8.join(servicesPath, `I${name}Service.cs`);
|
|
2035
|
+
const implementationPath = path8.join(servicesPath, `${name}Service.cs`);
|
|
1868
2036
|
validatePathSecurity(interfacePath, projectRoot);
|
|
1869
2037
|
validatePathSecurity(implementationPath, projectRoot);
|
|
1870
2038
|
if (!dryRun) {
|
|
@@ -2108,15 +2276,15 @@ public class {{name}}Configuration : IEntityTypeConfiguration<{{name}}>
|
|
|
2108
2276
|
const entityContent = Handlebars.compile(entityTemplate)(context);
|
|
2109
2277
|
const configContent = Handlebars.compile(configTemplate)(context);
|
|
2110
2278
|
const projectRoot = config.smartstack.projectPath;
|
|
2111
|
-
const domainPath = structure.domain ||
|
|
2112
|
-
const infraPath = structure.infrastructure ||
|
|
2113
|
-
const entityFilePath =
|
|
2114
|
-
const configFilePath =
|
|
2279
|
+
const domainPath = structure.domain || path8.join(projectRoot, "Domain");
|
|
2280
|
+
const infraPath = structure.infrastructure || path8.join(projectRoot, "Infrastructure");
|
|
2281
|
+
const entityFilePath = path8.join(domainPath, `${name}.cs`);
|
|
2282
|
+
const configFilePath = path8.join(infraPath, "Persistence", "Configurations", `${name}Configuration.cs`);
|
|
2115
2283
|
validatePathSecurity(entityFilePath, projectRoot);
|
|
2116
2284
|
validatePathSecurity(configFilePath, projectRoot);
|
|
2117
2285
|
if (!dryRun) {
|
|
2118
2286
|
await ensureDirectory(domainPath);
|
|
2119
|
-
await ensureDirectory(
|
|
2287
|
+
await ensureDirectory(path8.join(infraPath, "Persistence", "Configurations"));
|
|
2120
2288
|
await writeText(entityFilePath, entityContent);
|
|
2121
2289
|
await writeText(configFilePath, configContent);
|
|
2122
2290
|
}
|
|
@@ -2230,9 +2398,9 @@ public record Update{{name}}Request();
|
|
|
2230
2398
|
};
|
|
2231
2399
|
const controllerContent = Handlebars.compile(controllerTemplate)(context);
|
|
2232
2400
|
const projectRoot = config.smartstack.projectPath;
|
|
2233
|
-
const apiPath = structure.api ||
|
|
2234
|
-
const controllersPath =
|
|
2235
|
-
const controllerFilePath =
|
|
2401
|
+
const apiPath = structure.api || path8.join(projectRoot, "Api");
|
|
2402
|
+
const controllersPath = path8.join(apiPath, "Controllers");
|
|
2403
|
+
const controllerFilePath = path8.join(controllersPath, `${name}Controller.cs`);
|
|
2236
2404
|
validatePathSecurity(controllerFilePath, projectRoot);
|
|
2237
2405
|
if (!dryRun) {
|
|
2238
2406
|
await ensureDirectory(controllersPath);
|
|
@@ -2412,11 +2580,11 @@ export function use{{name}}(options: Use{{name}}Options = {}) {
|
|
|
2412
2580
|
const componentContent = Handlebars.compile(componentTemplate)(context);
|
|
2413
2581
|
const hookContent = Handlebars.compile(hookTemplate)(context);
|
|
2414
2582
|
const projectRoot = config.smartstack.projectPath;
|
|
2415
|
-
const webPath = structure.web ||
|
|
2416
|
-
const componentsPath = options?.outputPath ||
|
|
2417
|
-
const hooksPath =
|
|
2418
|
-
const componentFilePath =
|
|
2419
|
-
const hookFilePath =
|
|
2583
|
+
const webPath = structure.web || path8.join(projectRoot, "web");
|
|
2584
|
+
const componentsPath = options?.outputPath || path8.join(webPath, "src", "components");
|
|
2585
|
+
const hooksPath = path8.join(webPath, "src", "hooks");
|
|
2586
|
+
const componentFilePath = path8.join(componentsPath, `${name}.tsx`);
|
|
2587
|
+
const hookFilePath = path8.join(hooksPath, `use${name}.ts`);
|
|
2420
2588
|
validatePathSecurity(componentFilePath, projectRoot);
|
|
2421
2589
|
validatePathSecurity(hookFilePath, projectRoot);
|
|
2422
2590
|
if (!dryRun) {
|
|
@@ -2541,8 +2709,8 @@ public class {{name}}ServiceTests
|
|
|
2541
2709
|
isSystemEntity
|
|
2542
2710
|
};
|
|
2543
2711
|
const testContent = Handlebars.compile(serviceTestTemplate2)(context);
|
|
2544
|
-
const testsPath = structure.application ?
|
|
2545
|
-
const testFilePath =
|
|
2712
|
+
const testsPath = structure.application ? path8.join(path8.dirname(structure.application), `${path8.basename(structure.application)}.Tests`, "Services") : path8.join(config.smartstack.projectPath, "Application.Tests", "Services");
|
|
2713
|
+
const testFilePath = path8.join(testsPath, `${name}ServiceTests.cs`);
|
|
2546
2714
|
if (!dryRun) {
|
|
2547
2715
|
await ensureDirectory(testsPath);
|
|
2548
2716
|
await writeText(testFilePath, testContent);
|
|
@@ -2674,10 +2842,10 @@ public record Update{{name}}Dto
|
|
|
2674
2842
|
const createContent = Handlebars.compile(createDtoTemplate)(context);
|
|
2675
2843
|
const updateContent = Handlebars.compile(updateDtoTemplate)(context);
|
|
2676
2844
|
const basePath = structure.application || config.smartstack.projectPath;
|
|
2677
|
-
const dtosPath =
|
|
2678
|
-
const responseFilePath =
|
|
2679
|
-
const createFilePath =
|
|
2680
|
-
const updateFilePath =
|
|
2845
|
+
const dtosPath = path8.join(basePath, "DTOs", name);
|
|
2846
|
+
const responseFilePath = path8.join(dtosPath, `${name}ResponseDto.cs`);
|
|
2847
|
+
const createFilePath = path8.join(dtosPath, `Create${name}Dto.cs`);
|
|
2848
|
+
const updateFilePath = path8.join(dtosPath, `Update${name}Dto.cs`);
|
|
2681
2849
|
if (!dryRun) {
|
|
2682
2850
|
await ensureDirectory(dtosPath);
|
|
2683
2851
|
await writeText(responseFilePath, responseContent);
|
|
@@ -2771,9 +2939,9 @@ public class Update{{name}}DtoValidator : AbstractValidator<Update{{name}}Dto>
|
|
|
2771
2939
|
const createValidatorContent = Handlebars.compile(createValidatorTemplate)(context);
|
|
2772
2940
|
const updateValidatorContent = Handlebars.compile(updateValidatorTemplate)(context);
|
|
2773
2941
|
const basePath = structure.application || config.smartstack.projectPath;
|
|
2774
|
-
const validatorsPath =
|
|
2775
|
-
const createValidatorFilePath =
|
|
2776
|
-
const updateValidatorFilePath =
|
|
2942
|
+
const validatorsPath = path8.join(basePath, "Validators");
|
|
2943
|
+
const createValidatorFilePath = path8.join(validatorsPath, `Create${name}DtoValidator.cs`);
|
|
2944
|
+
const updateValidatorFilePath = path8.join(validatorsPath, `Update${name}DtoValidator.cs`);
|
|
2777
2945
|
if (!dryRun) {
|
|
2778
2946
|
await ensureDirectory(validatorsPath);
|
|
2779
2947
|
await writeText(createValidatorFilePath, createValidatorContent);
|
|
@@ -2906,12 +3074,12 @@ public class {{name}}Repository : I{{name}}Repository
|
|
|
2906
3074
|
const interfaceContent = Handlebars.compile(interfaceTemplate)(context);
|
|
2907
3075
|
const implementationContent = Handlebars.compile(implementationTemplate)(context);
|
|
2908
3076
|
const appPath = structure.application || config.smartstack.projectPath;
|
|
2909
|
-
const infraPath = structure.infrastructure ||
|
|
2910
|
-
const interfaceFilePath =
|
|
2911
|
-
const implementationFilePath =
|
|
3077
|
+
const infraPath = structure.infrastructure || path8.join(config.smartstack.projectPath, "Infrastructure");
|
|
3078
|
+
const interfaceFilePath = path8.join(appPath, "Repositories", `I${name}Repository.cs`);
|
|
3079
|
+
const implementationFilePath = path8.join(infraPath, "Repositories", `${name}Repository.cs`);
|
|
2912
3080
|
if (!dryRun) {
|
|
2913
|
-
await ensureDirectory(
|
|
2914
|
-
await ensureDirectory(
|
|
3081
|
+
await ensureDirectory(path8.join(appPath, "Repositories"));
|
|
3082
|
+
await ensureDirectory(path8.join(infraPath, "Repositories"));
|
|
2915
3083
|
await writeText(interfaceFilePath, interfaceContent);
|
|
2916
3084
|
await writeText(implementationFilePath, implementationContent);
|
|
2917
3085
|
}
|
|
@@ -2936,7 +3104,7 @@ function formatResult3(result, type, name, dryRun = false) {
|
|
|
2936
3104
|
lines.push(dryRun ? "## \u{1F4C4} Files to Generate" : "## \u2705 Files Generated");
|
|
2937
3105
|
lines.push("");
|
|
2938
3106
|
for (const file of result.files) {
|
|
2939
|
-
lines.push(`### ${file.type === "created" ? "\u{1F4C4}" : "\u270F\uFE0F"} ${
|
|
3107
|
+
lines.push(`### ${file.type === "created" ? "\u{1F4C4}" : "\u270F\uFE0F"} ${path8.basename(file.path)}`);
|
|
2940
3108
|
lines.push(`**Path**: \`${file.path}\``);
|
|
2941
3109
|
lines.push("");
|
|
2942
3110
|
lines.push("```" + (file.path.endsWith(".cs") ? "csharp" : "typescript"));
|
|
@@ -2981,7 +3149,7 @@ function formatResult3(result, type, name, dryRun = false) {
|
|
|
2981
3149
|
|
|
2982
3150
|
// src/tools/api-docs.ts
|
|
2983
3151
|
import axios from "axios";
|
|
2984
|
-
import
|
|
3152
|
+
import path9 from "path";
|
|
2985
3153
|
var apiDocsTool = {
|
|
2986
3154
|
name: "api_docs",
|
|
2987
3155
|
description: "Get API documentation for SmartStack endpoints. Can fetch from Swagger/OpenAPI or parse controller files directly.",
|
|
@@ -3086,7 +3254,7 @@ async function parseControllers(structure) {
|
|
|
3086
3254
|
const endpoints = [];
|
|
3087
3255
|
for (const file of controllerFiles) {
|
|
3088
3256
|
const content = await readText(file);
|
|
3089
|
-
const fileName =
|
|
3257
|
+
const fileName = path9.basename(file, ".cs");
|
|
3090
3258
|
const controllerName = fileName.replace("Controller", "");
|
|
3091
3259
|
const routeMatch = content.match(/\[Route\s*\(\s*"([^"]+)"\s*\)\]/);
|
|
3092
3260
|
const baseRoute = routeMatch ? routeMatch[1].replace("[controller]", controllerName.toLowerCase()) : `/api/${controllerName.toLowerCase()}`;
|
|
@@ -3346,7 +3514,7 @@ function formatAsOpenApi(endpoints) {
|
|
|
3346
3514
|
|
|
3347
3515
|
// src/tools/suggest-migration.ts
|
|
3348
3516
|
import { z as z2 } from "zod";
|
|
3349
|
-
import
|
|
3517
|
+
import path10 from "path";
|
|
3350
3518
|
var suggestMigrationTool = {
|
|
3351
3519
|
name: "suggest_migration",
|
|
3352
3520
|
description: "Suggest a migration name following SmartStack conventions ({context}_v{version}_{sequence}_{Description})",
|
|
@@ -3434,13 +3602,13 @@ async function handleSuggestMigration(args, config) {
|
|
|
3434
3602
|
}
|
|
3435
3603
|
async function findExistingMigrations(structure, config, context) {
|
|
3436
3604
|
const migrations = [];
|
|
3437
|
-
const infraPath = structure.infrastructure ||
|
|
3438
|
-
const migrationsPath =
|
|
3605
|
+
const infraPath = structure.infrastructure || path10.join(config.smartstack.projectPath, "Infrastructure");
|
|
3606
|
+
const migrationsPath = path10.join(infraPath, "Migrations");
|
|
3439
3607
|
try {
|
|
3440
3608
|
const migrationFiles = await findFiles("*.cs", { cwd: migrationsPath });
|
|
3441
3609
|
const migrationPattern = /^(\w+)_v(\d+\.\d+\.\d+)_(\d+)_(\w+)\.cs$/;
|
|
3442
3610
|
for (const file of migrationFiles) {
|
|
3443
|
-
const fileName =
|
|
3611
|
+
const fileName = path10.basename(file);
|
|
3444
3612
|
if (fileName.includes(".Designer.") || fileName.includes("ModelSnapshot")) {
|
|
3445
3613
|
continue;
|
|
3446
3614
|
}
|
|
@@ -3483,7 +3651,7 @@ function compareVersions2(a, b) {
|
|
|
3483
3651
|
|
|
3484
3652
|
// src/tools/scaffold-tests.ts
|
|
3485
3653
|
import Handlebars2 from "handlebars";
|
|
3486
|
-
import
|
|
3654
|
+
import path11 from "path";
|
|
3487
3655
|
var scaffoldTestsTool = {
|
|
3488
3656
|
name: "scaffold_tests",
|
|
3489
3657
|
description: "Generate unit, integration, and security tests for SmartStack entities, services, controllers, validators, and repositories. Ensures non-regression and maximum security coverage.",
|
|
@@ -4913,14 +5081,14 @@ async function scaffoldEntityTests(name, options, testTypes, structure, config,
|
|
|
4913
5081
|
};
|
|
4914
5082
|
if (testTypes.includes("unit")) {
|
|
4915
5083
|
const content = Handlebars2.compile(entityTestTemplate)(context);
|
|
4916
|
-
const testPath =
|
|
5084
|
+
const testPath = path11.join(structure.root, "Tests", "Unit", "Domain", `${name}Tests.cs`);
|
|
4917
5085
|
validatePathSecurity(testPath, structure.root);
|
|
4918
5086
|
if (!dryRun) {
|
|
4919
|
-
await ensureDirectory(
|
|
5087
|
+
await ensureDirectory(path11.dirname(testPath));
|
|
4920
5088
|
await writeText(testPath, content);
|
|
4921
5089
|
}
|
|
4922
5090
|
result.files.push({
|
|
4923
|
-
path:
|
|
5091
|
+
path: path11.relative(structure.root, testPath),
|
|
4924
5092
|
content,
|
|
4925
5093
|
type: "created"
|
|
4926
5094
|
});
|
|
@@ -4931,14 +5099,14 @@ async function scaffoldEntityTests(name, options, testTypes, structure, config,
|
|
|
4931
5099
|
nameLower: name.charAt(0).toLowerCase() + name.slice(1),
|
|
4932
5100
|
apiNamespace: config.conventions.namespaces.api
|
|
4933
5101
|
});
|
|
4934
|
-
const securityPath =
|
|
5102
|
+
const securityPath = path11.join(structure.root, "Tests", "Security", `${name}SecurityTests.cs`);
|
|
4935
5103
|
validatePathSecurity(securityPath, structure.root);
|
|
4936
5104
|
if (!dryRun) {
|
|
4937
|
-
await ensureDirectory(
|
|
5105
|
+
await ensureDirectory(path11.dirname(securityPath));
|
|
4938
5106
|
await writeText(securityPath, securityContent);
|
|
4939
5107
|
}
|
|
4940
5108
|
result.files.push({
|
|
4941
|
-
path:
|
|
5109
|
+
path: path11.relative(structure.root, securityPath),
|
|
4942
5110
|
content: securityContent,
|
|
4943
5111
|
type: "created"
|
|
4944
5112
|
});
|
|
@@ -4959,14 +5127,14 @@ async function scaffoldServiceTests(name, options, testTypes, structure, config,
|
|
|
4959
5127
|
};
|
|
4960
5128
|
if (testTypes.includes("unit")) {
|
|
4961
5129
|
const content = Handlebars2.compile(serviceTestTemplate)(context);
|
|
4962
|
-
const testPath =
|
|
5130
|
+
const testPath = path11.join(structure.root, "Tests", "Unit", "Services", `${name}ServiceTests.cs`);
|
|
4963
5131
|
validatePathSecurity(testPath, structure.root);
|
|
4964
5132
|
if (!dryRun) {
|
|
4965
|
-
await ensureDirectory(
|
|
5133
|
+
await ensureDirectory(path11.dirname(testPath));
|
|
4966
5134
|
await writeText(testPath, content);
|
|
4967
5135
|
}
|
|
4968
5136
|
result.files.push({
|
|
4969
|
-
path:
|
|
5137
|
+
path: path11.relative(structure.root, testPath),
|
|
4970
5138
|
content,
|
|
4971
5139
|
type: "created"
|
|
4972
5140
|
});
|
|
@@ -4989,28 +5157,28 @@ async function scaffoldControllerTests(name, options, testTypes, structure, conf
|
|
|
4989
5157
|
};
|
|
4990
5158
|
if (testTypes.includes("integration")) {
|
|
4991
5159
|
const content = Handlebars2.compile(controllerTestTemplate)(context);
|
|
4992
|
-
const testPath =
|
|
5160
|
+
const testPath = path11.join(structure.root, "Tests", "Integration", "Controllers", `${name}ControllerTests.cs`);
|
|
4993
5161
|
validatePathSecurity(testPath, structure.root);
|
|
4994
5162
|
if (!dryRun) {
|
|
4995
|
-
await ensureDirectory(
|
|
5163
|
+
await ensureDirectory(path11.dirname(testPath));
|
|
4996
5164
|
await writeText(testPath, content);
|
|
4997
5165
|
}
|
|
4998
5166
|
result.files.push({
|
|
4999
|
-
path:
|
|
5167
|
+
path: path11.relative(structure.root, testPath),
|
|
5000
5168
|
content,
|
|
5001
5169
|
type: "created"
|
|
5002
5170
|
});
|
|
5003
5171
|
}
|
|
5004
5172
|
if (testTypes.includes("security")) {
|
|
5005
5173
|
const securityContent = Handlebars2.compile(securityTestTemplate)(context);
|
|
5006
|
-
const securityPath =
|
|
5174
|
+
const securityPath = path11.join(structure.root, "Tests", "Security", `${name}SecurityTests.cs`);
|
|
5007
5175
|
validatePathSecurity(securityPath, structure.root);
|
|
5008
5176
|
if (!dryRun) {
|
|
5009
|
-
await ensureDirectory(
|
|
5177
|
+
await ensureDirectory(path11.dirname(securityPath));
|
|
5010
5178
|
await writeText(securityPath, securityContent);
|
|
5011
5179
|
}
|
|
5012
5180
|
result.files.push({
|
|
5013
|
-
path:
|
|
5181
|
+
path: path11.relative(structure.root, securityPath),
|
|
5014
5182
|
content: securityContent,
|
|
5015
5183
|
type: "created"
|
|
5016
5184
|
});
|
|
@@ -5028,14 +5196,14 @@ async function scaffoldValidatorTests(name, options, testTypes, structure, confi
|
|
|
5028
5196
|
};
|
|
5029
5197
|
if (testTypes.includes("unit")) {
|
|
5030
5198
|
const content = Handlebars2.compile(validatorTestTemplate)(context);
|
|
5031
|
-
const testPath =
|
|
5199
|
+
const testPath = path11.join(structure.root, "Tests", "Unit", "Validators", `${name}ValidatorTests.cs`);
|
|
5032
5200
|
validatePathSecurity(testPath, structure.root);
|
|
5033
5201
|
if (!dryRun) {
|
|
5034
|
-
await ensureDirectory(
|
|
5202
|
+
await ensureDirectory(path11.dirname(testPath));
|
|
5035
5203
|
await writeText(testPath, content);
|
|
5036
5204
|
}
|
|
5037
5205
|
result.files.push({
|
|
5038
|
-
path:
|
|
5206
|
+
path: path11.relative(structure.root, testPath),
|
|
5039
5207
|
content,
|
|
5040
5208
|
type: "created"
|
|
5041
5209
|
});
|
|
@@ -5055,14 +5223,14 @@ async function scaffoldRepositoryTests(name, options, testTypes, structure, conf
|
|
|
5055
5223
|
};
|
|
5056
5224
|
if (testTypes.includes("integration")) {
|
|
5057
5225
|
const content = Handlebars2.compile(repositoryTestTemplate)(context);
|
|
5058
|
-
const testPath =
|
|
5226
|
+
const testPath = path11.join(structure.root, "Tests", "Integration", "Repositories", `${name}RepositoryTests.cs`);
|
|
5059
5227
|
validatePathSecurity(testPath, structure.root);
|
|
5060
5228
|
if (!dryRun) {
|
|
5061
|
-
await ensureDirectory(
|
|
5229
|
+
await ensureDirectory(path11.dirname(testPath));
|
|
5062
5230
|
await writeText(testPath, content);
|
|
5063
5231
|
}
|
|
5064
5232
|
result.files.push({
|
|
5065
|
-
path:
|
|
5233
|
+
path: path11.relative(structure.root, testPath),
|
|
5066
5234
|
content,
|
|
5067
5235
|
type: "created"
|
|
5068
5236
|
});
|
|
@@ -5112,7 +5280,7 @@ function formatTestResult(result, _target, name, dryRun) {
|
|
|
5112
5280
|
}
|
|
5113
5281
|
|
|
5114
5282
|
// src/tools/analyze-test-coverage.ts
|
|
5115
|
-
import
|
|
5283
|
+
import path12 from "path";
|
|
5116
5284
|
var analyzeTestCoverageTool = {
|
|
5117
5285
|
name: "analyze_test_coverage",
|
|
5118
5286
|
description: "Analyze test coverage for a SmartStack project. Identifies entities, services, and controllers without tests, calculates coverage ratios, and provides recommendations.",
|
|
@@ -5185,7 +5353,7 @@ async function analyzeEntityCoverage(structure, result) {
|
|
|
5185
5353
|
}
|
|
5186
5354
|
const entityFiles = await findFiles("**/*.cs", { cwd: structure.domain });
|
|
5187
5355
|
const entityNames = extractComponentNames(entityFiles, ["Entity", "Aggregate"]);
|
|
5188
|
-
const testPath =
|
|
5356
|
+
const testPath = path12.join(structure.root, "Tests", "Unit", "Domain");
|
|
5189
5357
|
let testFiles = [];
|
|
5190
5358
|
try {
|
|
5191
5359
|
testFiles = await findFiles("**/*Tests.cs", { cwd: testPath });
|
|
@@ -5214,17 +5382,17 @@ async function analyzeServiceCoverage(structure, result) {
|
|
|
5214
5382
|
}
|
|
5215
5383
|
const serviceFiles = await findFiles("**/I*Service.cs", { cwd: structure.application });
|
|
5216
5384
|
const serviceNames = serviceFiles.map((f) => {
|
|
5217
|
-
const basename =
|
|
5385
|
+
const basename = path12.basename(f, ".cs");
|
|
5218
5386
|
return basename.startsWith("I") ? basename.slice(1) : basename;
|
|
5219
5387
|
}).filter((n) => n.endsWith("Service")).map((n) => n.replace(/Service$/, ""));
|
|
5220
|
-
const testPath =
|
|
5388
|
+
const testPath = path12.join(structure.root, "Tests", "Unit", "Services");
|
|
5221
5389
|
let testFiles = [];
|
|
5222
5390
|
try {
|
|
5223
5391
|
testFiles = await findFiles("**/*ServiceTests.cs", { cwd: testPath });
|
|
5224
5392
|
} catch {
|
|
5225
5393
|
}
|
|
5226
5394
|
const testedServices = testFiles.map((f) => {
|
|
5227
|
-
const basename =
|
|
5395
|
+
const basename = path12.basename(f, ".cs");
|
|
5228
5396
|
return basename.replace(/ServiceTests$/, "");
|
|
5229
5397
|
});
|
|
5230
5398
|
result.services.total = serviceNames.length;
|
|
@@ -5249,17 +5417,17 @@ async function analyzeControllerCoverage(structure, result) {
|
|
|
5249
5417
|
}
|
|
5250
5418
|
const controllerFiles = await findFiles("**/*Controller.cs", { cwd: structure.api });
|
|
5251
5419
|
const controllerNames = controllerFiles.map((f) => {
|
|
5252
|
-
const basename =
|
|
5420
|
+
const basename = path12.basename(f, ".cs");
|
|
5253
5421
|
return basename.replace(/Controller$/, "");
|
|
5254
5422
|
}).filter((n) => n !== "Base");
|
|
5255
|
-
const testPath =
|
|
5423
|
+
const testPath = path12.join(structure.root, "Tests", "Integration", "Controllers");
|
|
5256
5424
|
let testFiles = [];
|
|
5257
5425
|
try {
|
|
5258
5426
|
testFiles = await findFiles("**/*ControllerTests.cs", { cwd: testPath });
|
|
5259
5427
|
} catch {
|
|
5260
5428
|
}
|
|
5261
5429
|
const testedControllers = testFiles.map((f) => {
|
|
5262
|
-
const basename =
|
|
5430
|
+
const basename = path12.basename(f, ".cs");
|
|
5263
5431
|
return basename.replace(/ControllerTests$/, "");
|
|
5264
5432
|
});
|
|
5265
5433
|
result.controllers.total = controllerNames.length;
|
|
@@ -5278,7 +5446,7 @@ async function analyzeControllerCoverage(structure, result) {
|
|
|
5278
5446
|
}
|
|
5279
5447
|
}
|
|
5280
5448
|
function extractComponentNames(files, excludeSuffixes = []) {
|
|
5281
|
-
return files.map((f) =>
|
|
5449
|
+
return files.map((f) => path12.basename(f, ".cs")).filter((name) => {
|
|
5282
5450
|
const excludePatterns = [
|
|
5283
5451
|
"Configuration",
|
|
5284
5452
|
"Extensions",
|
|
@@ -5296,7 +5464,7 @@ function extractComponentNames(files, excludeSuffixes = []) {
|
|
|
5296
5464
|
}
|
|
5297
5465
|
function extractTestedNames(testFiles) {
|
|
5298
5466
|
return testFiles.map((f) => {
|
|
5299
|
-
const basename =
|
|
5467
|
+
const basename = path12.basename(f, ".cs");
|
|
5300
5468
|
return basename.replace(/Tests$/, "");
|
|
5301
5469
|
});
|
|
5302
5470
|
}
|
|
@@ -5434,7 +5602,7 @@ function formatCoverageCell(category) {
|
|
|
5434
5602
|
}
|
|
5435
5603
|
|
|
5436
5604
|
// src/tools/validate-test-conventions.ts
|
|
5437
|
-
import
|
|
5605
|
+
import path13 from "path";
|
|
5438
5606
|
var validateTestConventionsTool = {
|
|
5439
5607
|
name: "validate_test_conventions",
|
|
5440
5608
|
description: "Validate that tests in a SmartStack project follow conventions: naming ({Method}_When{Condition}_Should{Result}), structure (Tests/Unit, Tests/Integration), patterns (AAA), assertions (FluentAssertions), and mocking (Moq).",
|
|
@@ -5497,7 +5665,7 @@ async function handleValidateTestConventions(args, config) {
|
|
|
5497
5665
|
suggestions: [],
|
|
5498
5666
|
autoFixedCount: 0
|
|
5499
5667
|
};
|
|
5500
|
-
const testsPath =
|
|
5668
|
+
const testsPath = path13.join(structure.root, "Tests");
|
|
5501
5669
|
try {
|
|
5502
5670
|
let testFiles = [];
|
|
5503
5671
|
try {
|
|
@@ -5514,7 +5682,7 @@ async function handleValidateTestConventions(args, config) {
|
|
|
5514
5682
|
await validateStructure(testsPath, testFiles, result);
|
|
5515
5683
|
}
|
|
5516
5684
|
for (const testFile of testFiles) {
|
|
5517
|
-
const fullPath =
|
|
5685
|
+
const fullPath = path13.join(testsPath, testFile);
|
|
5518
5686
|
const content = await readText(fullPath);
|
|
5519
5687
|
if (checks.includes("naming")) {
|
|
5520
5688
|
validateNaming(testFile, content, result, input.autoFix);
|
|
@@ -5540,7 +5708,7 @@ async function validateStructure(testsPath, testFiles, result) {
|
|
|
5540
5708
|
const expectedDirs = ["Unit", "Integration"];
|
|
5541
5709
|
const foundDirs = /* @__PURE__ */ new Set();
|
|
5542
5710
|
for (const file of testFiles) {
|
|
5543
|
-
const parts = file.split(
|
|
5711
|
+
const parts = file.split(path13.sep);
|
|
5544
5712
|
if (parts.length > 1) {
|
|
5545
5713
|
foundDirs.add(parts[0]);
|
|
5546
5714
|
}
|
|
@@ -5557,8 +5725,8 @@ async function validateStructure(testsPath, testFiles, result) {
|
|
|
5557
5725
|
});
|
|
5558
5726
|
}
|
|
5559
5727
|
}
|
|
5560
|
-
const hasUnitDomain = testFiles.some((f) => f.includes(
|
|
5561
|
-
const hasIntegrationControllers = testFiles.some((f) => f.includes(
|
|
5728
|
+
const hasUnitDomain = testFiles.some((f) => f.includes(path13.join("Unit", "Domain")));
|
|
5729
|
+
const hasIntegrationControllers = testFiles.some((f) => f.includes(path13.join("Integration", "Controllers")));
|
|
5562
5730
|
if (!hasUnitDomain && testFiles.some((f) => f.includes("Unit"))) {
|
|
5563
5731
|
result.suggestions.push("Consider organizing unit tests into subdirectories: Unit/Domain, Unit/Services, Unit/Validators");
|
|
5564
5732
|
}
|
|
@@ -5818,7 +5986,7 @@ function formatValidationResult(result) {
|
|
|
5818
5986
|
}
|
|
5819
5987
|
|
|
5820
5988
|
// src/tools/suggest-test-scenarios.ts
|
|
5821
|
-
import
|
|
5989
|
+
import path14 from "path";
|
|
5822
5990
|
var suggestTestScenariosTool = {
|
|
5823
5991
|
name: "suggest_test_scenarios",
|
|
5824
5992
|
description: "Analyze source code and suggest test scenarios based on detected methods, parameters, and patterns. Generates comprehensive test case recommendations for SmartStack components.",
|
|
@@ -5915,7 +6083,7 @@ async function findSourceFile(target, name, structure, _config) {
|
|
|
5915
6083
|
pattern = `**/${name}Controller.cs`;
|
|
5916
6084
|
break;
|
|
5917
6085
|
case "file":
|
|
5918
|
-
return
|
|
6086
|
+
return path14.isAbsolute(name) ? name : path14.join(structure.root, name);
|
|
5919
6087
|
default:
|
|
5920
6088
|
return null;
|
|
5921
6089
|
}
|
|
@@ -5924,11 +6092,11 @@ async function findSourceFile(target, name, structure, _config) {
|
|
|
5924
6092
|
const altPattern = `**/*${name}*.cs`;
|
|
5925
6093
|
const altFiles = await findFiles(altPattern, { cwd: searchPath });
|
|
5926
6094
|
if (altFiles.length > 0) {
|
|
5927
|
-
return
|
|
6095
|
+
return path14.join(searchPath, altFiles[0]);
|
|
5928
6096
|
}
|
|
5929
6097
|
return null;
|
|
5930
6098
|
}
|
|
5931
|
-
return
|
|
6099
|
+
return path14.join(searchPath, files[0]);
|
|
5932
6100
|
}
|
|
5933
6101
|
function parseSourceCode(content) {
|
|
5934
6102
|
const methods = [];
|
|
@@ -6321,41 +6489,1300 @@ function getTypeEmoji(type) {
|
|
|
6321
6489
|
}
|
|
6322
6490
|
}
|
|
6323
6491
|
|
|
6324
|
-
// src/
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
name: "
|
|
6328
|
-
description:
|
|
6329
|
-
|
|
6492
|
+
// src/tools/scaffold-api-client.ts
|
|
6493
|
+
import path15 from "path";
|
|
6494
|
+
var scaffoldApiClientTool = {
|
|
6495
|
+
name: "scaffold_api_client",
|
|
6496
|
+
description: `Generate TypeScript API client with NavRoute integration.
|
|
6497
|
+
|
|
6498
|
+
Creates:
|
|
6499
|
+
- Type-safe API service with CRUD methods
|
|
6500
|
+
- TypeScript interfaces for request/response
|
|
6501
|
+
- React Query hook (optional)
|
|
6502
|
+
- Integration with navRoutes.generated.ts registry
|
|
6503
|
+
|
|
6504
|
+
Example:
|
|
6505
|
+
scaffold_api_client navRoute="platform.administration.users" name="User"
|
|
6506
|
+
|
|
6507
|
+
The generated client automatically resolves the API path from the NavRoute registry,
|
|
6508
|
+
ensuring frontend routes stay synchronized with backend NavRoute attributes.`,
|
|
6509
|
+
inputSchema: {
|
|
6510
|
+
type: "object",
|
|
6511
|
+
properties: {
|
|
6512
|
+
path: {
|
|
6513
|
+
type: "string",
|
|
6514
|
+
description: "Path to SmartStack project root (defaults to configured project path)"
|
|
6515
|
+
},
|
|
6516
|
+
navRoute: {
|
|
6517
|
+
type: "string",
|
|
6518
|
+
description: 'NavRoute path (e.g., "platform.administration.users")'
|
|
6519
|
+
},
|
|
6520
|
+
name: {
|
|
6521
|
+
type: "string",
|
|
6522
|
+
description: 'Entity name in PascalCase (e.g., "User", "Order")'
|
|
6523
|
+
},
|
|
6524
|
+
methods: {
|
|
6525
|
+
type: "array",
|
|
6526
|
+
items: {
|
|
6527
|
+
type: "string",
|
|
6528
|
+
enum: ["getAll", "getById", "create", "update", "delete", "search", "export"]
|
|
6529
|
+
},
|
|
6530
|
+
default: ["getAll", "getById", "create", "update", "delete"],
|
|
6531
|
+
description: "API methods to generate"
|
|
6532
|
+
},
|
|
6533
|
+
options: {
|
|
6534
|
+
type: "object",
|
|
6535
|
+
properties: {
|
|
6536
|
+
outputPath: { type: "string", description: "Custom output path" },
|
|
6537
|
+
includeTypes: { type: "boolean", default: true, description: "Generate TypeScript types" },
|
|
6538
|
+
includeHook: { type: "boolean", default: true, description: "Generate React Query hook" },
|
|
6539
|
+
dryRun: { type: "boolean", default: false, description: "Preview without writing" }
|
|
6540
|
+
}
|
|
6541
|
+
}
|
|
6542
|
+
},
|
|
6543
|
+
required: ["navRoute", "name"]
|
|
6544
|
+
}
|
|
6330
6545
|
};
|
|
6331
|
-
async function
|
|
6332
|
-
const
|
|
6333
|
-
|
|
6546
|
+
async function handleScaffoldApiClient(args, config) {
|
|
6547
|
+
const input = ScaffoldApiClientInputSchema.parse(args);
|
|
6548
|
+
logger.info("Scaffolding API client", { navRoute: input.navRoute, name: input.name });
|
|
6549
|
+
const result = await scaffoldApiClient(input, config);
|
|
6550
|
+
return formatResult4(result, input);
|
|
6551
|
+
}
|
|
6552
|
+
async function scaffoldApiClient(input, config) {
|
|
6553
|
+
const result = {
|
|
6554
|
+
success: true,
|
|
6555
|
+
files: [],
|
|
6556
|
+
instructions: []
|
|
6557
|
+
};
|
|
6558
|
+
const { navRoute, name, methods, options } = input;
|
|
6559
|
+
const dryRun = options?.dryRun ?? false;
|
|
6560
|
+
const includeTypes = options?.includeTypes ?? true;
|
|
6561
|
+
const includeHook = options?.includeHook ?? true;
|
|
6562
|
+
const nameLower = name.charAt(0).toLowerCase() + name.slice(1);
|
|
6563
|
+
const apiPath = navRouteToApiPath(navRoute);
|
|
6564
|
+
const projectRoot = input.path || config.smartstack.projectPath;
|
|
6565
|
+
const structure = await findSmartStackStructure(projectRoot);
|
|
6566
|
+
const webPath = structure.web || path15.join(projectRoot, "web");
|
|
6567
|
+
const servicesPath = options?.outputPath || path15.join(webPath, "src", "services", "api");
|
|
6568
|
+
const hooksPath = path15.join(webPath, "src", "hooks");
|
|
6569
|
+
const typesPath = path15.join(webPath, "src", "types");
|
|
6570
|
+
const apiClientContent = generateApiClient(name, nameLower, navRoute, apiPath, methods);
|
|
6571
|
+
const apiClientFile = path15.join(servicesPath, `${nameLower}.ts`);
|
|
6572
|
+
if (!dryRun) {
|
|
6573
|
+
await ensureDirectory(servicesPath);
|
|
6574
|
+
await writeText(apiClientFile, apiClientContent);
|
|
6575
|
+
}
|
|
6576
|
+
result.files.push({ path: apiClientFile, content: apiClientContent, type: "created" });
|
|
6577
|
+
if (includeTypes) {
|
|
6578
|
+
const typesContent = generateTypes(name);
|
|
6579
|
+
const typesFile = path15.join(typesPath, `${nameLower}.ts`);
|
|
6580
|
+
if (!dryRun) {
|
|
6581
|
+
await ensureDirectory(typesPath);
|
|
6582
|
+
await writeText(typesFile, typesContent);
|
|
6583
|
+
}
|
|
6584
|
+
result.files.push({ path: typesFile, content: typesContent, type: "created" });
|
|
6585
|
+
}
|
|
6586
|
+
if (includeHook) {
|
|
6587
|
+
const hookContent = generateHook(name, nameLower, methods);
|
|
6588
|
+
const hookFile = path15.join(hooksPath, `use${name}.ts`);
|
|
6589
|
+
if (!dryRun) {
|
|
6590
|
+
await ensureDirectory(hooksPath);
|
|
6591
|
+
await writeText(hookFile, hookContent);
|
|
6592
|
+
}
|
|
6593
|
+
result.files.push({ path: hookFile, content: hookContent, type: "created" });
|
|
6594
|
+
}
|
|
6595
|
+
result.instructions.push(`Import the API client: import { ${nameLower}Api } from './services/api/${nameLower}';`);
|
|
6596
|
+
if (includeHook) {
|
|
6597
|
+
result.instructions.push(`Import the hook: import { use${name}, use${name}List } from './hooks/use${name}';`);
|
|
6598
|
+
}
|
|
6599
|
+
result.instructions.push(`Ensure navRoutes.generated.ts includes route: "${navRoute}"`);
|
|
6600
|
+
return result;
|
|
6601
|
+
}
|
|
6602
|
+
function navRouteToApiPath(navRoute) {
|
|
6603
|
+
return `/api/${navRoute.replace(/\./g, "/")}`;
|
|
6604
|
+
}
|
|
6605
|
+
function generateApiClient(name, nameLower, navRoute, apiPath, methods) {
|
|
6606
|
+
const template = `/**
|
|
6607
|
+
* ${name} API Client
|
|
6608
|
+
*
|
|
6609
|
+
* Auto-generated by SmartStack MCP - DO NOT EDIT MANUALLY
|
|
6610
|
+
* NavRoute: ${navRoute}
|
|
6611
|
+
* API Path: ${apiPath}
|
|
6612
|
+
*/
|
|
6334
6613
|
|
|
6335
|
-
|
|
6614
|
+
import { getRoute } from '../routes/navRoutes.generated';
|
|
6615
|
+
import { apiClient } from '../lib/apiClient';
|
|
6616
|
+
import type {
|
|
6617
|
+
${name},
|
|
6618
|
+
${name}CreateRequest,
|
|
6619
|
+
${name}UpdateRequest,
|
|
6620
|
+
${name}ListResponse,
|
|
6621
|
+
PaginatedRequest,
|
|
6622
|
+
PaginatedResponse
|
|
6623
|
+
} from '../types/${nameLower}';
|
|
6624
|
+
|
|
6625
|
+
const ROUTE = getRoute('${navRoute}');
|
|
6626
|
+
|
|
6627
|
+
export const ${nameLower}Api = {
|
|
6628
|
+
${methods.includes("getAll") ? ` /**
|
|
6629
|
+
* Get all ${name}s with pagination
|
|
6630
|
+
*/
|
|
6631
|
+
async getAll(params?: PaginatedRequest): Promise<PaginatedResponse<${name}>> {
|
|
6632
|
+
const response = await apiClient.get<${name}ListResponse>(ROUTE.api, { params });
|
|
6633
|
+
return response.data;
|
|
6634
|
+
},
|
|
6635
|
+
` : ""}
|
|
6636
|
+
${methods.includes("getById") ? ` /**
|
|
6637
|
+
* Get ${name} by ID
|
|
6638
|
+
*/
|
|
6639
|
+
async getById(id: string): Promise<${name}> {
|
|
6640
|
+
const response = await apiClient.get<${name}>(\`\${ROUTE.api}/\${id}\`);
|
|
6641
|
+
return response.data;
|
|
6642
|
+
},
|
|
6643
|
+
` : ""}
|
|
6644
|
+
${methods.includes("create") ? ` /**
|
|
6645
|
+
* Create new ${name}
|
|
6646
|
+
*/
|
|
6647
|
+
async create(data: ${name}CreateRequest): Promise<${name}> {
|
|
6648
|
+
const response = await apiClient.post<${name}>(ROUTE.api, data);
|
|
6649
|
+
return response.data;
|
|
6650
|
+
},
|
|
6651
|
+
` : ""}
|
|
6652
|
+
${methods.includes("update") ? ` /**
|
|
6653
|
+
* Update existing ${name}
|
|
6654
|
+
*/
|
|
6655
|
+
async update(id: string, data: ${name}UpdateRequest): Promise<${name}> {
|
|
6656
|
+
const response = await apiClient.put<${name}>(\`\${ROUTE.api}/\${id}\`, data);
|
|
6657
|
+
return response.data;
|
|
6658
|
+
},
|
|
6659
|
+
` : ""}
|
|
6660
|
+
${methods.includes("delete") ? ` /**
|
|
6661
|
+
* Delete ${name}
|
|
6662
|
+
*/
|
|
6663
|
+
async delete(id: string): Promise<void> {
|
|
6664
|
+
await apiClient.delete(\`\${ROUTE.api}/\${id}\`);
|
|
6665
|
+
},
|
|
6666
|
+
` : ""}
|
|
6667
|
+
${methods.includes("search") ? ` /**
|
|
6668
|
+
* Search ${name}s
|
|
6669
|
+
*/
|
|
6670
|
+
async search(query: string, params?: PaginatedRequest): Promise<PaginatedResponse<${name}>> {
|
|
6671
|
+
const response = await apiClient.get<${name}ListResponse>(\`\${ROUTE.api}/search\`, {
|
|
6672
|
+
params: { q: query, ...params }
|
|
6673
|
+
});
|
|
6674
|
+
return response.data;
|
|
6675
|
+
},
|
|
6676
|
+
` : ""}
|
|
6677
|
+
${methods.includes("export") ? ` /**
|
|
6678
|
+
* Export ${name}s to file
|
|
6679
|
+
*/
|
|
6680
|
+
async export(format: 'csv' | 'xlsx' | 'pdf' = 'xlsx'): Promise<Blob> {
|
|
6681
|
+
const response = await apiClient.get(\`\${ROUTE.api}/export\`, {
|
|
6682
|
+
params: { format },
|
|
6683
|
+
responseType: 'blob'
|
|
6684
|
+
});
|
|
6685
|
+
return response.data;
|
|
6686
|
+
},
|
|
6687
|
+
` : ""}
|
|
6688
|
+
/**
|
|
6689
|
+
* Get the NavRoute for this API
|
|
6690
|
+
*/
|
|
6691
|
+
getRoute() {
|
|
6692
|
+
return ROUTE;
|
|
6693
|
+
},
|
|
6694
|
+
};
|
|
6336
6695
|
|
|
6337
|
-
|
|
6338
|
-
|
|
6696
|
+
export default ${nameLower}Api;
|
|
6697
|
+
`;
|
|
6698
|
+
return template;
|
|
6699
|
+
}
|
|
6700
|
+
function generateTypes(name) {
|
|
6701
|
+
return `/**
|
|
6702
|
+
* ${name} Types
|
|
6703
|
+
*
|
|
6704
|
+
* Auto-generated by SmartStack MCP - Customize as needed
|
|
6705
|
+
*/
|
|
6339
6706
|
|
|
6340
|
-
|
|
6707
|
+
export interface ${name} {
|
|
6708
|
+
id: string;
|
|
6709
|
+
code: string;
|
|
6710
|
+
name?: string;
|
|
6711
|
+
description?: string;
|
|
6712
|
+
isActive: boolean;
|
|
6713
|
+
createdAt: string;
|
|
6714
|
+
createdBy: string;
|
|
6715
|
+
updatedAt?: string;
|
|
6716
|
+
updatedBy?: string;
|
|
6717
|
+
}
|
|
6341
6718
|
|
|
6342
|
-
|
|
6719
|
+
export interface ${name}CreateRequest {
|
|
6720
|
+
code: string;
|
|
6721
|
+
name?: string;
|
|
6722
|
+
description?: string;
|
|
6723
|
+
}
|
|
6343
6724
|
|
|
6344
|
-
|
|
6725
|
+
export interface ${name}UpdateRequest {
|
|
6726
|
+
code?: string;
|
|
6727
|
+
name?: string;
|
|
6728
|
+
description?: string;
|
|
6729
|
+
isActive?: boolean;
|
|
6730
|
+
}
|
|
6345
6731
|
|
|
6346
|
-
|
|
6732
|
+
export interface ${name}ListResponse {
|
|
6733
|
+
items: ${name}[];
|
|
6734
|
+
totalCount: number;
|
|
6735
|
+
pageSize: number;
|
|
6736
|
+
currentPage: number;
|
|
6737
|
+
totalPages: number;
|
|
6738
|
+
}
|
|
6347
6739
|
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6740
|
+
export interface PaginatedRequest {
|
|
6741
|
+
page?: number;
|
|
6742
|
+
pageSize?: number;
|
|
6743
|
+
sortBy?: string;
|
|
6744
|
+
sortDirection?: 'asc' | 'desc';
|
|
6745
|
+
filter?: string;
|
|
6746
|
+
}
|
|
6352
6747
|
|
|
6353
|
-
|
|
6748
|
+
export interface PaginatedResponse<T> {
|
|
6749
|
+
items: T[];
|
|
6750
|
+
totalCount: number;
|
|
6751
|
+
pageSize: number;
|
|
6752
|
+
currentPage: number;
|
|
6753
|
+
totalPages: number;
|
|
6754
|
+
}
|
|
6755
|
+
`;
|
|
6756
|
+
}
|
|
6757
|
+
function generateHook(name, nameLower, methods) {
|
|
6758
|
+
return `/**
|
|
6759
|
+
* ${name} React Query Hooks
|
|
6760
|
+
*
|
|
6761
|
+
* Auto-generated by SmartStack MCP - DO NOT EDIT MANUALLY
|
|
6762
|
+
*/
|
|
6354
6763
|
|
|
6355
|
-
|
|
6764
|
+
import { useQuery, useMutation, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
|
|
6765
|
+
import { ${nameLower}Api } from '../services/api/${nameLower}';
|
|
6766
|
+
import type { ${name}, ${name}CreateRequest, ${name}UpdateRequest, PaginatedRequest, PaginatedResponse } from '../types/${nameLower}';
|
|
6356
6767
|
|
|
6357
|
-
|
|
6358
|
-
|
|
6768
|
+
const QUERY_KEY = '${nameLower}s';
|
|
6769
|
+
|
|
6770
|
+
${methods.includes("getAll") ? `/**
|
|
6771
|
+
* Hook to fetch paginated ${name} list
|
|
6772
|
+
*/
|
|
6773
|
+
export function use${name}List(
|
|
6774
|
+
params?: PaginatedRequest,
|
|
6775
|
+
options?: Omit<UseQueryOptions<PaginatedResponse<${name}>>, 'queryKey' | 'queryFn'>
|
|
6776
|
+
) {
|
|
6777
|
+
return useQuery({
|
|
6778
|
+
queryKey: [QUERY_KEY, 'list', params],
|
|
6779
|
+
queryFn: () => ${nameLower}Api.getAll(params),
|
|
6780
|
+
...options,
|
|
6781
|
+
});
|
|
6782
|
+
}
|
|
6783
|
+
` : ""}
|
|
6784
|
+
${methods.includes("getById") ? `/**
|
|
6785
|
+
* Hook to fetch single ${name} by ID
|
|
6786
|
+
*/
|
|
6787
|
+
export function use${name}(
|
|
6788
|
+
id: string | undefined,
|
|
6789
|
+
options?: Omit<UseQueryOptions<${name}>, 'queryKey' | 'queryFn'>
|
|
6790
|
+
) {
|
|
6791
|
+
return useQuery({
|
|
6792
|
+
queryKey: [QUERY_KEY, 'detail', id],
|
|
6793
|
+
queryFn: () => ${nameLower}Api.getById(id!),
|
|
6794
|
+
enabled: !!id,
|
|
6795
|
+
...options,
|
|
6796
|
+
});
|
|
6797
|
+
}
|
|
6798
|
+
` : ""}
|
|
6799
|
+
${methods.includes("create") ? `/**
|
|
6800
|
+
* Hook to create new ${name}
|
|
6801
|
+
*/
|
|
6802
|
+
export function use${name}Create(
|
|
6803
|
+
options?: UseMutationOptions<${name}, Error, ${name}CreateRequest>
|
|
6804
|
+
) {
|
|
6805
|
+
const queryClient = useQueryClient();
|
|
6806
|
+
|
|
6807
|
+
return useMutation({
|
|
6808
|
+
mutationFn: (data: ${name}CreateRequest) => ${nameLower}Api.create(data),
|
|
6809
|
+
onSuccess: () => {
|
|
6810
|
+
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
|
6811
|
+
},
|
|
6812
|
+
...options,
|
|
6813
|
+
});
|
|
6814
|
+
}
|
|
6815
|
+
` : ""}
|
|
6816
|
+
${methods.includes("update") ? `/**
|
|
6817
|
+
* Hook to update existing ${name}
|
|
6818
|
+
*/
|
|
6819
|
+
export function use${name}Update(
|
|
6820
|
+
options?: UseMutationOptions<${name}, Error, { id: string; data: ${name}UpdateRequest }>
|
|
6821
|
+
) {
|
|
6822
|
+
const queryClient = useQueryClient();
|
|
6823
|
+
|
|
6824
|
+
return useMutation({
|
|
6825
|
+
mutationFn: ({ id, data }) => ${nameLower}Api.update(id, data),
|
|
6826
|
+
onSuccess: (_, { id }) => {
|
|
6827
|
+
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
|
6828
|
+
queryClient.invalidateQueries({ queryKey: [QUERY_KEY, 'detail', id] });
|
|
6829
|
+
},
|
|
6830
|
+
...options,
|
|
6831
|
+
});
|
|
6832
|
+
}
|
|
6833
|
+
` : ""}
|
|
6834
|
+
${methods.includes("delete") ? `/**
|
|
6835
|
+
* Hook to delete ${name}
|
|
6836
|
+
*/
|
|
6837
|
+
export function use${name}Delete(
|
|
6838
|
+
options?: UseMutationOptions<void, Error, string>
|
|
6839
|
+
) {
|
|
6840
|
+
const queryClient = useQueryClient();
|
|
6841
|
+
|
|
6842
|
+
return useMutation({
|
|
6843
|
+
mutationFn: (id: string) => ${nameLower}Api.delete(id),
|
|
6844
|
+
onSuccess: () => {
|
|
6845
|
+
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
|
6846
|
+
},
|
|
6847
|
+
...options,
|
|
6848
|
+
});
|
|
6849
|
+
}
|
|
6850
|
+
` : ""}
|
|
6851
|
+
${methods.includes("search") ? `/**
|
|
6852
|
+
* Hook to search ${name}s
|
|
6853
|
+
*/
|
|
6854
|
+
export function use${name}Search(
|
|
6855
|
+
query: string,
|
|
6856
|
+
params?: PaginatedRequest,
|
|
6857
|
+
options?: Omit<UseQueryOptions<PaginatedResponse<${name}>>, 'queryKey' | 'queryFn'>
|
|
6858
|
+
) {
|
|
6859
|
+
return useQuery({
|
|
6860
|
+
queryKey: [QUERY_KEY, 'search', query, params],
|
|
6861
|
+
queryFn: () => ${nameLower}Api.search(query, params),
|
|
6862
|
+
enabled: query.length >= 2,
|
|
6863
|
+
...options,
|
|
6864
|
+
});
|
|
6865
|
+
}
|
|
6866
|
+
` : ""}
|
|
6867
|
+
`;
|
|
6868
|
+
}
|
|
6869
|
+
function formatResult4(result, input) {
|
|
6870
|
+
const lines = [];
|
|
6871
|
+
lines.push(`# Scaffold API Client: ${input.name}`);
|
|
6872
|
+
lines.push("");
|
|
6873
|
+
if (input.options?.dryRun) {
|
|
6874
|
+
lines.push("> **DRY RUN** - No files were written");
|
|
6875
|
+
lines.push("");
|
|
6876
|
+
}
|
|
6877
|
+
lines.push(`## NavRoute Integration`);
|
|
6878
|
+
lines.push("");
|
|
6879
|
+
lines.push(`- **NavRoute**: \`${input.navRoute}\``);
|
|
6880
|
+
lines.push(`- **API Path**: \`${navRouteToApiPath(input.navRoute)}\``);
|
|
6881
|
+
lines.push(`- **Methods**: ${input.methods.join(", ")}`);
|
|
6882
|
+
lines.push("");
|
|
6883
|
+
lines.push("## Generated Files");
|
|
6884
|
+
lines.push("");
|
|
6885
|
+
for (const file of result.files) {
|
|
6886
|
+
const relativePath = file.path.replace(/\\/g, "/").split("/src/").pop() || file.path;
|
|
6887
|
+
lines.push(`### ${relativePath}`);
|
|
6888
|
+
lines.push("");
|
|
6889
|
+
lines.push("```typescript");
|
|
6890
|
+
lines.push(file.content.substring(0, 1500) + (file.content.length > 1500 ? "\n// ... (truncated)" : ""));
|
|
6891
|
+
lines.push("```");
|
|
6892
|
+
lines.push("");
|
|
6893
|
+
}
|
|
6894
|
+
lines.push("## Next Steps");
|
|
6895
|
+
lines.push("");
|
|
6896
|
+
for (const instruction of result.instructions) {
|
|
6897
|
+
lines.push(`- ${instruction}`);
|
|
6898
|
+
}
|
|
6899
|
+
lines.push("");
|
|
6900
|
+
lines.push("## Required Setup");
|
|
6901
|
+
lines.push("");
|
|
6902
|
+
lines.push("1. Ensure `navRoutes.generated.ts` exists (run `scaffold_routes`)");
|
|
6903
|
+
lines.push("2. Configure `apiClient` with base URL and auth interceptors");
|
|
6904
|
+
lines.push("3. Install dependencies: `npm install @tanstack/react-query axios`");
|
|
6905
|
+
return lines.join("\n");
|
|
6906
|
+
}
|
|
6907
|
+
|
|
6908
|
+
// src/tools/scaffold-routes.ts
|
|
6909
|
+
import path16 from "path";
|
|
6910
|
+
import { glob as glob3 } from "glob";
|
|
6911
|
+
var scaffoldRoutesTool = {
|
|
6912
|
+
name: "scaffold_routes",
|
|
6913
|
+
description: `Generate React Router configuration from backend NavRoute attributes.
|
|
6914
|
+
|
|
6915
|
+
Creates:
|
|
6916
|
+
- navRoutes.generated.ts: Registry of all routes with API paths and permissions
|
|
6917
|
+
- routes.tsx: React Router configuration with nested routes
|
|
6918
|
+
- Layout components (optional)
|
|
6919
|
+
|
|
6920
|
+
Example:
|
|
6921
|
+
scaffold_routes source="controllers" scope="all"
|
|
6922
|
+
|
|
6923
|
+
Scans backend controllers for [NavRoute("context.application.module")] attributes
|
|
6924
|
+
and generates corresponding frontend routing infrastructure.`,
|
|
6925
|
+
inputSchema: {
|
|
6926
|
+
type: "object",
|
|
6927
|
+
properties: {
|
|
6928
|
+
path: {
|
|
6929
|
+
type: "string",
|
|
6930
|
+
description: "Path to SmartStack project root (defaults to configured project path)"
|
|
6931
|
+
},
|
|
6932
|
+
source: {
|
|
6933
|
+
type: "string",
|
|
6934
|
+
enum: ["controllers", "navigation", "manual"],
|
|
6935
|
+
default: "controllers",
|
|
6936
|
+
description: "Source for route discovery"
|
|
6937
|
+
},
|
|
6938
|
+
scope: {
|
|
6939
|
+
type: "string",
|
|
6940
|
+
enum: ["all", "platform", "business", "extensions"],
|
|
6941
|
+
default: "all",
|
|
6942
|
+
description: "Scope of routes to generate"
|
|
6943
|
+
},
|
|
6944
|
+
options: {
|
|
6945
|
+
type: "object",
|
|
6946
|
+
properties: {
|
|
6947
|
+
outputPath: { type: "string", description: "Custom output path" },
|
|
6948
|
+
includeLayouts: { type: "boolean", default: true },
|
|
6949
|
+
includeGuards: { type: "boolean", default: true },
|
|
6950
|
+
generateRegistry: { type: "boolean", default: true },
|
|
6951
|
+
dryRun: { type: "boolean", default: false }
|
|
6952
|
+
}
|
|
6953
|
+
}
|
|
6954
|
+
}
|
|
6955
|
+
}
|
|
6956
|
+
};
|
|
6957
|
+
async function handleScaffoldRoutes(args, config) {
|
|
6958
|
+
const input = ScaffoldRoutesInputSchema.parse(args);
|
|
6959
|
+
logger.info("Scaffolding routes", { source: input.source, scope: input.scope });
|
|
6960
|
+
const result = await scaffoldRoutes(input, config);
|
|
6961
|
+
return formatResult5(result, input);
|
|
6962
|
+
}
|
|
6963
|
+
async function scaffoldRoutes(input, config) {
|
|
6964
|
+
const result = {
|
|
6965
|
+
success: true,
|
|
6966
|
+
files: [],
|
|
6967
|
+
instructions: []
|
|
6968
|
+
};
|
|
6969
|
+
const { source, scope, options } = input;
|
|
6970
|
+
const dryRun = options?.dryRun ?? false;
|
|
6971
|
+
const includeLayouts = options?.includeLayouts ?? true;
|
|
6972
|
+
const includeGuards = options?.includeGuards ?? true;
|
|
6973
|
+
const generateRegistry = options?.generateRegistry ?? true;
|
|
6974
|
+
const projectRoot = input.path || config.smartstack.projectPath;
|
|
6975
|
+
const structure = await findSmartStackStructure(projectRoot);
|
|
6976
|
+
const webPath = structure.web || path16.join(projectRoot, "web");
|
|
6977
|
+
const routesPath = options?.outputPath || path16.join(webPath, "src", "routes");
|
|
6978
|
+
const navRoutes = await discoverNavRoutes(structure, scope);
|
|
6979
|
+
if (navRoutes.length === 0) {
|
|
6980
|
+
result.success = false;
|
|
6981
|
+
result.instructions.push("No NavRoute attributes found in controllers");
|
|
6982
|
+
return result;
|
|
6983
|
+
}
|
|
6984
|
+
if (generateRegistry) {
|
|
6985
|
+
const registryContent = generateNavRouteRegistry(navRoutes);
|
|
6986
|
+
const registryFile = path16.join(routesPath, "navRoutes.generated.ts");
|
|
6987
|
+
if (!dryRun) {
|
|
6988
|
+
await ensureDirectory(routesPath);
|
|
6989
|
+
await writeText(registryFile, registryContent);
|
|
6990
|
+
}
|
|
6991
|
+
result.files.push({ path: registryFile, content: registryContent, type: "created" });
|
|
6992
|
+
}
|
|
6993
|
+
const routerContent = generateRouterConfig(navRoutes, includeGuards);
|
|
6994
|
+
const routerFile = path16.join(routesPath, "index.tsx");
|
|
6995
|
+
if (!dryRun) {
|
|
6996
|
+
await ensureDirectory(routesPath);
|
|
6997
|
+
await writeText(routerFile, routerContent);
|
|
6998
|
+
}
|
|
6999
|
+
result.files.push({ path: routerFile, content: routerContent, type: "created" });
|
|
7000
|
+
if (includeLayouts) {
|
|
7001
|
+
const layoutsPath = path16.join(webPath, "src", "layouts");
|
|
7002
|
+
const contexts = [...new Set(navRoutes.map((r) => r.navRoute.split(".")[0]))];
|
|
7003
|
+
for (const context of contexts) {
|
|
7004
|
+
const layoutContent = generateLayout(context);
|
|
7005
|
+
const layoutFile = path16.join(layoutsPath, `${capitalize(context)}Layout.tsx`);
|
|
7006
|
+
if (!dryRun) {
|
|
7007
|
+
await ensureDirectory(layoutsPath);
|
|
7008
|
+
await writeText(layoutFile, layoutContent);
|
|
7009
|
+
}
|
|
7010
|
+
result.files.push({ path: layoutFile, content: layoutContent, type: "created" });
|
|
7011
|
+
}
|
|
7012
|
+
}
|
|
7013
|
+
if (includeGuards) {
|
|
7014
|
+
const guardsContent = generateRouteGuards();
|
|
7015
|
+
const guardsFile = path16.join(routesPath, "guards.tsx");
|
|
7016
|
+
if (!dryRun) {
|
|
7017
|
+
await writeText(guardsFile, guardsContent);
|
|
7018
|
+
}
|
|
7019
|
+
result.files.push({ path: guardsFile, content: guardsContent, type: "created" });
|
|
7020
|
+
}
|
|
7021
|
+
result.instructions.push(`Generated ${navRoutes.length} routes from ${source}`);
|
|
7022
|
+
result.instructions.push('Import routes: import { router } from "./routes";');
|
|
7023
|
+
result.instructions.push("Use with RouterProvider: <RouterProvider router={router} />");
|
|
7024
|
+
return result;
|
|
7025
|
+
}
|
|
7026
|
+
async function discoverNavRoutes(structure, scope) {
|
|
7027
|
+
const routes = [];
|
|
7028
|
+
const apiPath = structure.api;
|
|
7029
|
+
if (!apiPath) {
|
|
7030
|
+
logger.warn("No API project found");
|
|
7031
|
+
return routes;
|
|
7032
|
+
}
|
|
7033
|
+
const controllerFiles = await glob3("**/*Controller.cs", {
|
|
7034
|
+
cwd: apiPath,
|
|
7035
|
+
absolute: true,
|
|
7036
|
+
ignore: ["**/obj/**", "**/bin/**"]
|
|
7037
|
+
});
|
|
7038
|
+
for (const file of controllerFiles) {
|
|
7039
|
+
try {
|
|
7040
|
+
const content = await readText(file);
|
|
7041
|
+
const navRouteMatch = content.match(/\[NavRoute\s*\(\s*"([^"]+)"(?:\s*,\s*Suffix\s*=\s*"([^"]+)")?\s*\)\]/);
|
|
7042
|
+
if (navRouteMatch) {
|
|
7043
|
+
const navRoute = navRouteMatch[1];
|
|
7044
|
+
const suffix = navRouteMatch[2];
|
|
7045
|
+
const context = navRoute.split(".")[0];
|
|
7046
|
+
if (scope !== "all" && context !== scope) {
|
|
7047
|
+
continue;
|
|
7048
|
+
}
|
|
7049
|
+
const controllerMatch = path16.basename(file).match(/(.+)Controller\.cs$/);
|
|
7050
|
+
const controllerName = controllerMatch ? controllerMatch[1] : "Unknown";
|
|
7051
|
+
const methods = [];
|
|
7052
|
+
if (content.includes("[HttpGet]")) methods.push("GET");
|
|
7053
|
+
if (content.includes("[HttpPost]")) methods.push("POST");
|
|
7054
|
+
if (content.includes("[HttpPut]")) methods.push("PUT");
|
|
7055
|
+
if (content.includes("[HttpDelete]")) methods.push("DELETE");
|
|
7056
|
+
if (content.includes("[HttpPatch]")) methods.push("PATCH");
|
|
7057
|
+
const permissions = [];
|
|
7058
|
+
const authorizeMatches = content.matchAll(/\[Authorize\s*\(\s*[^)]*Policy\s*=\s*"([^"]+)"/g);
|
|
7059
|
+
for (const match of authorizeMatches) {
|
|
7060
|
+
permissions.push(match[1]);
|
|
7061
|
+
}
|
|
7062
|
+
const fullNavRoute = suffix ? `${navRoute}.${suffix}` : navRoute;
|
|
7063
|
+
routes.push({
|
|
7064
|
+
navRoute: fullNavRoute,
|
|
7065
|
+
apiPath: `/api/${navRoute.replace(/\./g, "/")}${suffix ? `/${suffix}` : ""}`,
|
|
7066
|
+
webPath: `/${navRoute.replace(/\./g, "/")}${suffix ? `/${suffix}` : ""}`,
|
|
7067
|
+
permissions,
|
|
7068
|
+
controller: controllerName,
|
|
7069
|
+
methods
|
|
7070
|
+
});
|
|
7071
|
+
}
|
|
7072
|
+
} catch {
|
|
7073
|
+
logger.debug(`Failed to parse controller: ${file}`);
|
|
7074
|
+
}
|
|
7075
|
+
}
|
|
7076
|
+
return routes.sort((a, b) => a.navRoute.localeCompare(b.navRoute));
|
|
7077
|
+
}
|
|
7078
|
+
function generateNavRouteRegistry(routes) {
|
|
7079
|
+
const lines = [
|
|
7080
|
+
"/**",
|
|
7081
|
+
" * NavRoute Registry",
|
|
7082
|
+
" *",
|
|
7083
|
+
" * Auto-generated by SmartStack MCP - DO NOT EDIT MANUALLY",
|
|
7084
|
+
" * Run `scaffold_routes` to regenerate",
|
|
7085
|
+
" */",
|
|
7086
|
+
"",
|
|
7087
|
+
"export interface NavRoute {",
|
|
7088
|
+
" navRoute: string;",
|
|
7089
|
+
" api: string;",
|
|
7090
|
+
" web: string;",
|
|
7091
|
+
" permissions: string[];",
|
|
7092
|
+
" controller?: string;",
|
|
7093
|
+
" methods: string[];",
|
|
7094
|
+
"}",
|
|
7095
|
+
"",
|
|
7096
|
+
"export const ROUTES: Record<string, NavRoute> = {"
|
|
7097
|
+
];
|
|
7098
|
+
for (const route of routes) {
|
|
7099
|
+
lines.push(` '${route.navRoute}': {`);
|
|
7100
|
+
lines.push(` navRoute: '${route.navRoute}',`);
|
|
7101
|
+
lines.push(` api: '${route.apiPath}',`);
|
|
7102
|
+
lines.push(` web: '${route.webPath}',`);
|
|
7103
|
+
lines.push(` permissions: [${route.permissions.map((p) => `'${p}'`).join(", ")}],`);
|
|
7104
|
+
if (route.controller) {
|
|
7105
|
+
lines.push(` controller: '${route.controller}',`);
|
|
7106
|
+
}
|
|
7107
|
+
lines.push(` methods: [${route.methods.map((m) => `'${m}'`).join(", ")}],`);
|
|
7108
|
+
lines.push(" },");
|
|
7109
|
+
}
|
|
7110
|
+
lines.push("};");
|
|
7111
|
+
lines.push("");
|
|
7112
|
+
lines.push("/**");
|
|
7113
|
+
lines.push(" * Get route configuration by NavRoute path");
|
|
7114
|
+
lines.push(" */");
|
|
7115
|
+
lines.push("export function getRoute(navRoute: string): NavRoute {");
|
|
7116
|
+
lines.push(" const route = ROUTES[navRoute];");
|
|
7117
|
+
lines.push(" if (!route) {");
|
|
7118
|
+
lines.push(" throw new Error(`Route not found: ${navRoute}`);");
|
|
7119
|
+
lines.push(" }");
|
|
7120
|
+
lines.push(" return route;");
|
|
7121
|
+
lines.push("}");
|
|
7122
|
+
lines.push("");
|
|
7123
|
+
lines.push("/**");
|
|
7124
|
+
lines.push(" * Check if user has permission for route");
|
|
7125
|
+
lines.push(" */");
|
|
7126
|
+
lines.push("export function hasRoutePermission(navRoute: string, userPermissions: string[]): boolean {");
|
|
7127
|
+
lines.push(" const route = ROUTES[navRoute];");
|
|
7128
|
+
lines.push(" if (!route || route.permissions.length === 0) return true;");
|
|
7129
|
+
lines.push(" return route.permissions.some(p => userPermissions.includes(p));");
|
|
7130
|
+
lines.push("}");
|
|
7131
|
+
lines.push("");
|
|
7132
|
+
lines.push("/**");
|
|
7133
|
+
lines.push(" * Get all routes for a context");
|
|
7134
|
+
lines.push(" */");
|
|
7135
|
+
lines.push("export function getRoutesByContext(context: string): NavRoute[] {");
|
|
7136
|
+
lines.push(" return Object.values(ROUTES).filter(r => r.navRoute.startsWith(`${context}.`));");
|
|
7137
|
+
lines.push("}");
|
|
7138
|
+
lines.push("");
|
|
7139
|
+
return lines.join("\n");
|
|
7140
|
+
}
|
|
7141
|
+
function generateRouterConfig(routes, includeGuards) {
|
|
7142
|
+
const routeTree = buildRouteTree(routes);
|
|
7143
|
+
const lines = [
|
|
7144
|
+
"/**",
|
|
7145
|
+
" * React Router Configuration",
|
|
7146
|
+
" *",
|
|
7147
|
+
" * Auto-generated by SmartStack MCP - DO NOT EDIT MANUALLY",
|
|
7148
|
+
" */",
|
|
7149
|
+
"",
|
|
7150
|
+
"import { createBrowserRouter, RouteObject } from 'react-router-dom';",
|
|
7151
|
+
"import { ROUTES } from './navRoutes.generated';"
|
|
7152
|
+
];
|
|
7153
|
+
if (includeGuards) {
|
|
7154
|
+
lines.push("import { ProtectedRoute, PermissionGuard } from './guards';");
|
|
7155
|
+
}
|
|
7156
|
+
const contexts = Object.keys(routeTree);
|
|
7157
|
+
for (const context of contexts) {
|
|
7158
|
+
lines.push(`import { ${capitalize(context)}Layout } from '../layouts/${capitalize(context)}Layout';`);
|
|
7159
|
+
}
|
|
7160
|
+
lines.push("");
|
|
7161
|
+
lines.push("// Page imports - customize these paths");
|
|
7162
|
+
for (const route of routes) {
|
|
7163
|
+
const pageName = route.navRoute.split(".").map(capitalize).join("");
|
|
7164
|
+
lines.push(`// import { ${pageName}Page } from '../pages/${pageName}Page';`);
|
|
7165
|
+
}
|
|
7166
|
+
lines.push("");
|
|
7167
|
+
lines.push("const routes: RouteObject[] = [");
|
|
7168
|
+
for (const [context, applications] of Object.entries(routeTree)) {
|
|
7169
|
+
lines.push(" {");
|
|
7170
|
+
lines.push(` path: '${context}',`);
|
|
7171
|
+
lines.push(` element: <${capitalize(context)}Layout />,`);
|
|
7172
|
+
lines.push(" children: [");
|
|
7173
|
+
for (const [app, modules] of Object.entries(applications)) {
|
|
7174
|
+
lines.push(" {");
|
|
7175
|
+
lines.push(` path: '${app}',`);
|
|
7176
|
+
lines.push(" children: [");
|
|
7177
|
+
for (const route of modules) {
|
|
7178
|
+
const modulePath = route.navRoute.split(".").slice(2).join("/");
|
|
7179
|
+
const pageName = route.navRoute.split(".").map(capitalize).join("");
|
|
7180
|
+
if (includeGuards && route.permissions.length > 0) {
|
|
7181
|
+
lines.push(" {");
|
|
7182
|
+
lines.push(` path: '${modulePath || ""}',`);
|
|
7183
|
+
lines.push(` element: (`);
|
|
7184
|
+
lines.push(` <PermissionGuard permissions={ROUTES['${route.navRoute}'].permissions}>`);
|
|
7185
|
+
lines.push(` {/* <${pageName}Page /> */}`);
|
|
7186
|
+
lines.push(` <div>TODO: ${pageName}Page</div>`);
|
|
7187
|
+
lines.push(` </PermissionGuard>`);
|
|
7188
|
+
lines.push(` ),`);
|
|
7189
|
+
lines.push(" },");
|
|
7190
|
+
} else {
|
|
7191
|
+
lines.push(" {");
|
|
7192
|
+
lines.push(` path: '${modulePath || ""}',`);
|
|
7193
|
+
lines.push(` element: <div>TODO: ${pageName}Page</div>,`);
|
|
7194
|
+
lines.push(" },");
|
|
7195
|
+
}
|
|
7196
|
+
}
|
|
7197
|
+
lines.push(" ],");
|
|
7198
|
+
lines.push(" },");
|
|
7199
|
+
}
|
|
7200
|
+
lines.push(" ],");
|
|
7201
|
+
lines.push(" },");
|
|
7202
|
+
}
|
|
7203
|
+
lines.push("];");
|
|
7204
|
+
lines.push("");
|
|
7205
|
+
lines.push("export const router = createBrowserRouter(routes);");
|
|
7206
|
+
lines.push("");
|
|
7207
|
+
lines.push("export default router;");
|
|
7208
|
+
lines.push("");
|
|
7209
|
+
return lines.join("\n");
|
|
7210
|
+
}
|
|
7211
|
+
function buildRouteTree(routes) {
|
|
7212
|
+
const tree = {};
|
|
7213
|
+
for (const route of routes) {
|
|
7214
|
+
const parts = route.navRoute.split(".");
|
|
7215
|
+
const context = parts[0];
|
|
7216
|
+
const app = parts[1] || "default";
|
|
7217
|
+
if (!tree[context]) {
|
|
7218
|
+
tree[context] = {};
|
|
7219
|
+
}
|
|
7220
|
+
if (!tree[context][app]) {
|
|
7221
|
+
tree[context][app] = [];
|
|
7222
|
+
}
|
|
7223
|
+
tree[context][app].push(route);
|
|
7224
|
+
}
|
|
7225
|
+
return tree;
|
|
7226
|
+
}
|
|
7227
|
+
function generateLayout(context) {
|
|
7228
|
+
const contextCapitalized = capitalize(context);
|
|
7229
|
+
return `/**
|
|
7230
|
+
* ${contextCapitalized} Layout
|
|
7231
|
+
*
|
|
7232
|
+
* Auto-generated by SmartStack MCP - Customize as needed
|
|
7233
|
+
*/
|
|
7234
|
+
|
|
7235
|
+
import React from 'react';
|
|
7236
|
+
import { Outlet, Link, useLocation } from 'react-router-dom';
|
|
7237
|
+
import { ROUTES, getRoutesByContext } from '../routes/navRoutes.generated';
|
|
7238
|
+
|
|
7239
|
+
export const ${contextCapitalized}Layout: React.FC = () => {
|
|
7240
|
+
const location = useLocation();
|
|
7241
|
+
const contextRoutes = getRoutesByContext('${context}');
|
|
7242
|
+
|
|
7243
|
+
return (
|
|
7244
|
+
<div className="flex h-screen bg-gray-100">
|
|
7245
|
+
{/* Sidebar */}
|
|
7246
|
+
<aside className="w-64 bg-white shadow-sm">
|
|
7247
|
+
<div className="p-4 border-b">
|
|
7248
|
+
<h1 className="text-xl font-semibold text-gray-900">${contextCapitalized}</h1>
|
|
7249
|
+
</div>
|
|
7250
|
+
<nav className="p-4">
|
|
7251
|
+
<ul className="space-y-2">
|
|
7252
|
+
{contextRoutes.map((route) => (
|
|
7253
|
+
<li key={route.navRoute}>
|
|
7254
|
+
<Link
|
|
7255
|
+
to={route.web}
|
|
7256
|
+
className={\`block px-3 py-2 rounded-md \${
|
|
7257
|
+
location.pathname === route.web
|
|
7258
|
+
? 'bg-blue-50 text-blue-700'
|
|
7259
|
+
: 'text-gray-700 hover:bg-gray-50'
|
|
7260
|
+
}\`}
|
|
7261
|
+
>
|
|
7262
|
+
{route.navRoute.split('.').pop()}
|
|
7263
|
+
</Link>
|
|
7264
|
+
</li>
|
|
7265
|
+
))}
|
|
7266
|
+
</ul>
|
|
7267
|
+
</nav>
|
|
7268
|
+
</aside>
|
|
7269
|
+
|
|
7270
|
+
{/* Main content */}
|
|
7271
|
+
<main className="flex-1 overflow-auto">
|
|
7272
|
+
<div className="p-6">
|
|
7273
|
+
<Outlet />
|
|
7274
|
+
</div>
|
|
7275
|
+
</main>
|
|
7276
|
+
</div>
|
|
7277
|
+
);
|
|
7278
|
+
};
|
|
7279
|
+
|
|
7280
|
+
export default ${contextCapitalized}Layout;
|
|
7281
|
+
`;
|
|
7282
|
+
}
|
|
7283
|
+
function generateRouteGuards() {
|
|
7284
|
+
return `/**
|
|
7285
|
+
* Route Guards
|
|
7286
|
+
*
|
|
7287
|
+
* Auto-generated by SmartStack MCP - Customize as needed
|
|
7288
|
+
*/
|
|
7289
|
+
|
|
7290
|
+
import React from 'react';
|
|
7291
|
+
import { Navigate, useLocation } from 'react-router-dom';
|
|
7292
|
+
|
|
7293
|
+
interface ProtectedRouteProps {
|
|
7294
|
+
children: React.ReactNode;
|
|
7295
|
+
redirectTo?: string;
|
|
7296
|
+
}
|
|
7297
|
+
|
|
7298
|
+
interface PermissionGuardProps {
|
|
7299
|
+
children: React.ReactNode;
|
|
7300
|
+
permissions: string[];
|
|
7301
|
+
fallback?: React.ReactNode;
|
|
7302
|
+
}
|
|
7303
|
+
|
|
7304
|
+
/**
|
|
7305
|
+
* Protect routes that require authentication
|
|
7306
|
+
*/
|
|
7307
|
+
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
|
7308
|
+
children,
|
|
7309
|
+
redirectTo = '/login'
|
|
7310
|
+
}) => {
|
|
7311
|
+
const location = useLocation();
|
|
7312
|
+
|
|
7313
|
+
// TODO: Replace with your auth hook
|
|
7314
|
+
const isAuthenticated = true; // useAuth().isAuthenticated;
|
|
7315
|
+
|
|
7316
|
+
if (!isAuthenticated) {
|
|
7317
|
+
return <Navigate to={redirectTo} state={{ from: location }} replace />;
|
|
7318
|
+
}
|
|
7319
|
+
|
|
7320
|
+
return <>{children}</>;
|
|
7321
|
+
};
|
|
7322
|
+
|
|
7323
|
+
/**
|
|
7324
|
+
* Guard routes based on user permissions
|
|
7325
|
+
*/
|
|
7326
|
+
export const PermissionGuard: React.FC<PermissionGuardProps> = ({
|
|
7327
|
+
children,
|
|
7328
|
+
permissions,
|
|
7329
|
+
fallback
|
|
7330
|
+
}) => {
|
|
7331
|
+
// TODO: Replace with your auth hook
|
|
7332
|
+
const userPermissions: string[] = []; // useAuth().permissions;
|
|
7333
|
+
|
|
7334
|
+
const hasPermission = permissions.length === 0 ||
|
|
7335
|
+
permissions.some(p => userPermissions.includes(p));
|
|
7336
|
+
|
|
7337
|
+
if (!hasPermission) {
|
|
7338
|
+
if (fallback) return <>{fallback}</>;
|
|
7339
|
+
return (
|
|
7340
|
+
<div className="flex items-center justify-center h-64">
|
|
7341
|
+
<div className="text-center">
|
|
7342
|
+
<h2 className="text-xl font-semibold text-gray-900">Access Denied</h2>
|
|
7343
|
+
<p className="mt-2 text-gray-600">
|
|
7344
|
+
You don't have permission to access this page.
|
|
7345
|
+
</p>
|
|
7346
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
7347
|
+
Required: {permissions.join(', ')}
|
|
7348
|
+
</p>
|
|
7349
|
+
</div>
|
|
7350
|
+
</div>
|
|
7351
|
+
);
|
|
7352
|
+
}
|
|
7353
|
+
|
|
7354
|
+
return <>{children}</>;
|
|
7355
|
+
};
|
|
7356
|
+
`;
|
|
7357
|
+
}
|
|
7358
|
+
function capitalize(str) {
|
|
7359
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
7360
|
+
}
|
|
7361
|
+
function formatResult5(result, input) {
|
|
7362
|
+
const lines = [];
|
|
7363
|
+
lines.push("# Scaffold Routes");
|
|
7364
|
+
lines.push("");
|
|
7365
|
+
if (input.options?.dryRun) {
|
|
7366
|
+
lines.push("> **DRY RUN** - No files were written");
|
|
7367
|
+
lines.push("");
|
|
7368
|
+
}
|
|
7369
|
+
lines.push("## Configuration");
|
|
7370
|
+
lines.push("");
|
|
7371
|
+
lines.push(`- **Source**: ${input.source}`);
|
|
7372
|
+
lines.push(`- **Scope**: ${input.scope}`);
|
|
7373
|
+
lines.push("");
|
|
7374
|
+
lines.push("## Generated Files");
|
|
7375
|
+
lines.push("");
|
|
7376
|
+
for (const file of result.files) {
|
|
7377
|
+
const relativePath = file.path.replace(/\\/g, "/").split("/src/").pop() || file.path;
|
|
7378
|
+
lines.push(`### ${relativePath}`);
|
|
7379
|
+
lines.push("");
|
|
7380
|
+
lines.push("```tsx");
|
|
7381
|
+
lines.push(file.content.substring(0, 2e3) + (file.content.length > 2e3 ? "\n// ... (truncated)" : ""));
|
|
7382
|
+
lines.push("```");
|
|
7383
|
+
lines.push("");
|
|
7384
|
+
}
|
|
7385
|
+
lines.push("## Instructions");
|
|
7386
|
+
lines.push("");
|
|
7387
|
+
for (const instruction of result.instructions) {
|
|
7388
|
+
lines.push(`- ${instruction}`);
|
|
7389
|
+
}
|
|
7390
|
+
return lines.join("\n");
|
|
7391
|
+
}
|
|
7392
|
+
|
|
7393
|
+
// src/tools/validate-frontend-routes.ts
|
|
7394
|
+
import path17 from "path";
|
|
7395
|
+
import { glob as glob4 } from "glob";
|
|
7396
|
+
var validateFrontendRoutesTool = {
|
|
7397
|
+
name: "validate_frontend_routes",
|
|
7398
|
+
description: `Validate frontend routes against backend NavRoute attributes.
|
|
7399
|
+
|
|
7400
|
+
Checks:
|
|
7401
|
+
- navRoutes.generated.ts exists and is up-to-date
|
|
7402
|
+
- API clients use correct NavRoute paths
|
|
7403
|
+
- React Router configuration matches backend routes
|
|
7404
|
+
- Permission configurations are synchronized
|
|
7405
|
+
|
|
7406
|
+
Example:
|
|
7407
|
+
validate_frontend_routes scope="all"
|
|
7408
|
+
|
|
7409
|
+
Reports issues and provides actionable recommendations for synchronization.`,
|
|
7410
|
+
inputSchema: {
|
|
7411
|
+
type: "object",
|
|
7412
|
+
properties: {
|
|
7413
|
+
path: {
|
|
7414
|
+
type: "string",
|
|
7415
|
+
description: "Path to SmartStack project root (defaults to configured project path)"
|
|
7416
|
+
},
|
|
7417
|
+
scope: {
|
|
7418
|
+
type: "string",
|
|
7419
|
+
enum: ["api-clients", "routes", "registry", "all"],
|
|
7420
|
+
default: "all",
|
|
7421
|
+
description: "Scope of validation"
|
|
7422
|
+
},
|
|
7423
|
+
options: {
|
|
7424
|
+
type: "object",
|
|
7425
|
+
properties: {
|
|
7426
|
+
fix: { type: "boolean", default: false, description: "Auto-fix minor issues" },
|
|
7427
|
+
strict: { type: "boolean", default: false, description: "Fail on warnings" }
|
|
7428
|
+
}
|
|
7429
|
+
}
|
|
7430
|
+
}
|
|
7431
|
+
}
|
|
7432
|
+
};
|
|
7433
|
+
async function handleValidateFrontendRoutes(args, config) {
|
|
7434
|
+
const input = ValidateFrontendRoutesInputSchema.parse(args);
|
|
7435
|
+
logger.info("Validating frontend routes", { scope: input.scope });
|
|
7436
|
+
const result = await validateFrontendRoutes(input, config);
|
|
7437
|
+
return formatResult6(result, input);
|
|
7438
|
+
}
|
|
7439
|
+
async function validateFrontendRoutes(input, config) {
|
|
7440
|
+
const result = {
|
|
7441
|
+
valid: true,
|
|
7442
|
+
registry: {
|
|
7443
|
+
exists: false,
|
|
7444
|
+
routeCount: 0,
|
|
7445
|
+
outdated: []
|
|
7446
|
+
},
|
|
7447
|
+
apiClients: {
|
|
7448
|
+
total: 0,
|
|
7449
|
+
valid: 0,
|
|
7450
|
+
issues: []
|
|
7451
|
+
},
|
|
7452
|
+
routes: {
|
|
7453
|
+
total: 0,
|
|
7454
|
+
orphaned: [],
|
|
7455
|
+
missing: []
|
|
7456
|
+
},
|
|
7457
|
+
recommendations: []
|
|
7458
|
+
};
|
|
7459
|
+
const { scope } = input;
|
|
7460
|
+
const projectRoot = input.path || config.smartstack.projectPath;
|
|
7461
|
+
const structure = await findSmartStackStructure(projectRoot);
|
|
7462
|
+
const webPath = structure.web || path17.join(projectRoot, "web");
|
|
7463
|
+
const backendRoutes = await discoverBackendNavRoutes(structure);
|
|
7464
|
+
if (scope === "all" || scope === "registry") {
|
|
7465
|
+
await validateRegistry(webPath, backendRoutes, result);
|
|
7466
|
+
}
|
|
7467
|
+
if (scope === "all" || scope === "api-clients") {
|
|
7468
|
+
await validateApiClients(webPath, backendRoutes, result);
|
|
7469
|
+
}
|
|
7470
|
+
if (scope === "all" || scope === "routes") {
|
|
7471
|
+
await validateRoutes(webPath, backendRoutes, result);
|
|
7472
|
+
}
|
|
7473
|
+
generateRecommendations2(result);
|
|
7474
|
+
result.valid = result.apiClients.issues.filter((i) => i.severity === "error").length === 0 && result.routes.missing.length === 0 && result.registry.exists;
|
|
7475
|
+
return result;
|
|
7476
|
+
}
|
|
7477
|
+
async function discoverBackendNavRoutes(structure) {
|
|
7478
|
+
const routes = [];
|
|
7479
|
+
const apiPath = structure.api;
|
|
7480
|
+
if (!apiPath) {
|
|
7481
|
+
return routes;
|
|
7482
|
+
}
|
|
7483
|
+
const controllerFiles = await glob4("**/*Controller.cs", {
|
|
7484
|
+
cwd: apiPath,
|
|
7485
|
+
absolute: true,
|
|
7486
|
+
ignore: ["**/obj/**", "**/bin/**"]
|
|
7487
|
+
});
|
|
7488
|
+
for (const file of controllerFiles) {
|
|
7489
|
+
try {
|
|
7490
|
+
const content = await readText(file);
|
|
7491
|
+
const navRouteMatch = content.match(/\[NavRoute\s*\(\s*"([^"]+)"(?:\s*,\s*Suffix\s*=\s*"([^"]+)")?\s*\)\]/);
|
|
7492
|
+
if (navRouteMatch) {
|
|
7493
|
+
const navRoute = navRouteMatch[1];
|
|
7494
|
+
const suffix = navRouteMatch[2];
|
|
7495
|
+
const fullNavRoute = suffix ? `${navRoute}.${suffix}` : navRoute;
|
|
7496
|
+
const controllerMatch = path17.basename(file).match(/(.+)Controller\.cs$/);
|
|
7497
|
+
const controllerName = controllerMatch ? controllerMatch[1] : "Unknown";
|
|
7498
|
+
const methods = [];
|
|
7499
|
+
if (content.includes("[HttpGet]")) methods.push("GET");
|
|
7500
|
+
if (content.includes("[HttpPost]")) methods.push("POST");
|
|
7501
|
+
if (content.includes("[HttpPut]")) methods.push("PUT");
|
|
7502
|
+
if (content.includes("[HttpDelete]")) methods.push("DELETE");
|
|
7503
|
+
const permissions = [];
|
|
7504
|
+
const authorizeMatches = content.matchAll(/\[Authorize\s*\(\s*[^)]*Policy\s*=\s*"([^"]+)"/g);
|
|
7505
|
+
for (const match of authorizeMatches) {
|
|
7506
|
+
permissions.push(match[1]);
|
|
7507
|
+
}
|
|
7508
|
+
routes.push({
|
|
7509
|
+
navRoute: fullNavRoute,
|
|
7510
|
+
apiPath: `/api/${navRoute.replace(/\./g, "/")}${suffix ? `/${suffix}` : ""}`,
|
|
7511
|
+
webPath: `/${navRoute.replace(/\./g, "/")}${suffix ? `/${suffix}` : ""}`,
|
|
7512
|
+
permissions,
|
|
7513
|
+
controller: controllerName,
|
|
7514
|
+
methods
|
|
7515
|
+
});
|
|
7516
|
+
}
|
|
7517
|
+
} catch {
|
|
7518
|
+
logger.debug(`Failed to parse controller: ${file}`);
|
|
7519
|
+
}
|
|
7520
|
+
}
|
|
7521
|
+
return routes;
|
|
7522
|
+
}
|
|
7523
|
+
async function validateRegistry(webPath, backendRoutes, result) {
|
|
7524
|
+
const registryPath = path17.join(webPath, "src", "routes", "navRoutes.generated.ts");
|
|
7525
|
+
if (!await fileExists(registryPath)) {
|
|
7526
|
+
result.registry.exists = false;
|
|
7527
|
+
result.recommendations.push("Run `scaffold_routes` to generate navRoutes.generated.ts");
|
|
7528
|
+
return;
|
|
7529
|
+
}
|
|
7530
|
+
result.registry.exists = true;
|
|
7531
|
+
try {
|
|
7532
|
+
const content = await readText(registryPath);
|
|
7533
|
+
const routeMatches = content.matchAll(/'([a-z.]+)':\s*\{/g);
|
|
7534
|
+
const registryRoutes = /* @__PURE__ */ new Set();
|
|
7535
|
+
for (const match of routeMatches) {
|
|
7536
|
+
registryRoutes.add(match[1]);
|
|
7537
|
+
}
|
|
7538
|
+
result.registry.routeCount = registryRoutes.size;
|
|
7539
|
+
for (const backendRoute of backendRoutes) {
|
|
7540
|
+
if (!registryRoutes.has(backendRoute.navRoute)) {
|
|
7541
|
+
result.registry.outdated.push(backendRoute.navRoute);
|
|
7542
|
+
}
|
|
7543
|
+
}
|
|
7544
|
+
for (const registryRoute of registryRoutes) {
|
|
7545
|
+
if (!backendRoutes.find((r) => r.navRoute === registryRoute)) {
|
|
7546
|
+
result.registry.outdated.push(`${registryRoute} (removed from backend)`);
|
|
7547
|
+
}
|
|
7548
|
+
}
|
|
7549
|
+
} catch {
|
|
7550
|
+
result.registry.exists = false;
|
|
7551
|
+
}
|
|
7552
|
+
}
|
|
7553
|
+
async function validateApiClients(webPath, backendRoutes, result) {
|
|
7554
|
+
const servicesPath = path17.join(webPath, "src", "services", "api");
|
|
7555
|
+
const clientFiles = await glob4("**/*.ts", {
|
|
7556
|
+
cwd: servicesPath,
|
|
7557
|
+
absolute: true,
|
|
7558
|
+
ignore: ["**/index.ts"]
|
|
7559
|
+
});
|
|
7560
|
+
result.apiClients.total = clientFiles.length;
|
|
7561
|
+
for (const file of clientFiles) {
|
|
7562
|
+
try {
|
|
7563
|
+
const content = await readText(file);
|
|
7564
|
+
const relativePath = path17.relative(webPath, file);
|
|
7565
|
+
const usesRegistry = content.includes("getRoute('") || content.includes('getRoute("');
|
|
7566
|
+
if (!usesRegistry) {
|
|
7567
|
+
const hardcodedMatch = content.match(/apiClient\.(get|post|put|delete)\s*[<(]\s*['"`]([^'"`]+)['"`]/);
|
|
7568
|
+
if (hardcodedMatch) {
|
|
7569
|
+
result.apiClients.issues.push({
|
|
7570
|
+
type: "invalid-path",
|
|
7571
|
+
severity: "warning",
|
|
7572
|
+
file: relativePath,
|
|
7573
|
+
message: `Hardcoded API path: ${hardcodedMatch[2]}`,
|
|
7574
|
+
suggestion: "Use getRoute() from navRoutes.generated.ts instead"
|
|
7575
|
+
});
|
|
7576
|
+
}
|
|
7577
|
+
} else {
|
|
7578
|
+
const navRouteMatch = content.match(/getRoute\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/);
|
|
7579
|
+
if (navRouteMatch) {
|
|
7580
|
+
const navRoute = navRouteMatch[1];
|
|
7581
|
+
const backendRoute = backendRoutes.find((r) => r.navRoute === navRoute);
|
|
7582
|
+
if (!backendRoute) {
|
|
7583
|
+
result.apiClients.issues.push({
|
|
7584
|
+
type: "missing-route",
|
|
7585
|
+
severity: "error",
|
|
7586
|
+
file: relativePath,
|
|
7587
|
+
navRoute,
|
|
7588
|
+
message: `NavRoute "${navRoute}" not found in backend controllers`,
|
|
7589
|
+
suggestion: "Verify the NavRoute path or update the backend controller"
|
|
7590
|
+
});
|
|
7591
|
+
} else {
|
|
7592
|
+
result.apiClients.valid++;
|
|
7593
|
+
}
|
|
7594
|
+
}
|
|
7595
|
+
}
|
|
7596
|
+
} catch {
|
|
7597
|
+
logger.debug(`Failed to parse API client: ${file}`);
|
|
7598
|
+
}
|
|
7599
|
+
}
|
|
7600
|
+
}
|
|
7601
|
+
async function validateRoutes(webPath, backendRoutes, result) {
|
|
7602
|
+
const routesPath = path17.join(webPath, "src", "routes", "index.tsx");
|
|
7603
|
+
if (!await fileExists(routesPath)) {
|
|
7604
|
+
result.routes.total = 0;
|
|
7605
|
+
result.routes.missing = backendRoutes.map((r) => r.navRoute);
|
|
7606
|
+
return;
|
|
7607
|
+
}
|
|
7608
|
+
try {
|
|
7609
|
+
const content = await readText(routesPath);
|
|
7610
|
+
const pathMatches = content.matchAll(/path:\s*['"`]([^'"`]+)['"`]/g);
|
|
7611
|
+
const frontendPaths = /* @__PURE__ */ new Set();
|
|
7612
|
+
for (const match of pathMatches) {
|
|
7613
|
+
frontendPaths.add(match[1]);
|
|
7614
|
+
}
|
|
7615
|
+
result.routes.total = frontendPaths.size;
|
|
7616
|
+
for (const backendRoute of backendRoutes) {
|
|
7617
|
+
const webPath2 = backendRoute.webPath.replace(/^\//, "");
|
|
7618
|
+
const parts = webPath2.split("/");
|
|
7619
|
+
let found = false;
|
|
7620
|
+
for (const pathPart of parts) {
|
|
7621
|
+
if (frontendPaths.has(pathPart)) {
|
|
7622
|
+
found = true;
|
|
7623
|
+
break;
|
|
7624
|
+
}
|
|
7625
|
+
}
|
|
7626
|
+
if (!found && parts.length > 0) {
|
|
7627
|
+
result.routes.missing.push(backendRoute.navRoute);
|
|
7628
|
+
}
|
|
7629
|
+
}
|
|
7630
|
+
for (const frontendPath of frontendPaths) {
|
|
7631
|
+
if (frontendPath === "*" || frontendPath === "" || frontendPath.startsWith(":")) {
|
|
7632
|
+
continue;
|
|
7633
|
+
}
|
|
7634
|
+
const matchingBackend = backendRoutes.find(
|
|
7635
|
+
(r) => r.webPath.includes(frontendPath) || r.navRoute.includes(frontendPath)
|
|
7636
|
+
);
|
|
7637
|
+
if (!matchingBackend) {
|
|
7638
|
+
result.routes.orphaned.push(frontendPath);
|
|
7639
|
+
}
|
|
7640
|
+
}
|
|
7641
|
+
} catch {
|
|
7642
|
+
result.routes.total = 0;
|
|
7643
|
+
result.routes.missing = backendRoutes.map((r) => r.navRoute);
|
|
7644
|
+
}
|
|
7645
|
+
}
|
|
7646
|
+
function generateRecommendations2(result) {
|
|
7647
|
+
if (!result.registry.exists) {
|
|
7648
|
+
result.recommendations.push('Run `scaffold_routes source="controllers"` to generate route registry');
|
|
7649
|
+
} else if (result.registry.outdated.length > 0) {
|
|
7650
|
+
result.recommendations.push(`Registry is outdated: ${result.registry.outdated.length} routes need sync. Run \`scaffold_routes\``);
|
|
7651
|
+
}
|
|
7652
|
+
if (result.apiClients.issues.length > 0) {
|
|
7653
|
+
const hardcoded = result.apiClients.issues.filter((i) => i.type === "invalid-path").length;
|
|
7654
|
+
const missing = result.apiClients.issues.filter((i) => i.type === "missing-route").length;
|
|
7655
|
+
if (hardcoded > 0) {
|
|
7656
|
+
result.recommendations.push(`${hardcoded} API clients use hardcoded paths. Migrate to getRoute()`);
|
|
7657
|
+
}
|
|
7658
|
+
if (missing > 0) {
|
|
7659
|
+
result.recommendations.push(`${missing} API clients reference non-existent NavRoutes`);
|
|
7660
|
+
}
|
|
7661
|
+
}
|
|
7662
|
+
if (result.routes.missing.length > 0) {
|
|
7663
|
+
result.recommendations.push(`${result.routes.missing.length} backend routes have no frontend counterpart`);
|
|
7664
|
+
}
|
|
7665
|
+
if (result.routes.orphaned.length > 0) {
|
|
7666
|
+
result.recommendations.push(`${result.routes.orphaned.length} frontend routes have no backend NavRoute`);
|
|
7667
|
+
}
|
|
7668
|
+
if (result.valid && result.recommendations.length === 0) {
|
|
7669
|
+
result.recommendations.push("All routes are synchronized between frontend and backend");
|
|
7670
|
+
}
|
|
7671
|
+
}
|
|
7672
|
+
function formatResult6(result, _input) {
|
|
7673
|
+
const lines = [];
|
|
7674
|
+
const statusIcon = result.valid ? "\u2705" : "\u274C";
|
|
7675
|
+
lines.push(`# Frontend Route Validation ${statusIcon}`);
|
|
7676
|
+
lines.push("");
|
|
7677
|
+
lines.push("## Summary");
|
|
7678
|
+
lines.push("");
|
|
7679
|
+
lines.push(`| Metric | Value |`);
|
|
7680
|
+
lines.push(`|--------|-------|`);
|
|
7681
|
+
lines.push(`| Valid | ${result.valid ? "Yes" : "No"} |`);
|
|
7682
|
+
lines.push(`| Registry Exists | ${result.registry.exists ? "Yes" : "No"} |`);
|
|
7683
|
+
lines.push(`| Registry Routes | ${result.registry.routeCount} |`);
|
|
7684
|
+
lines.push(`| API Clients | ${result.apiClients.valid}/${result.apiClients.total} valid |`);
|
|
7685
|
+
lines.push(`| Frontend Routes | ${result.routes.total} |`);
|
|
7686
|
+
lines.push(`| Missing Routes | ${result.routes.missing.length} |`);
|
|
7687
|
+
lines.push(`| Orphaned Routes | ${result.routes.orphaned.length} |`);
|
|
7688
|
+
lines.push("");
|
|
7689
|
+
if (result.registry.outdated.length > 0) {
|
|
7690
|
+
lines.push("## Outdated Registry Entries");
|
|
7691
|
+
lines.push("");
|
|
7692
|
+
for (const route of result.registry.outdated) {
|
|
7693
|
+
lines.push(`- \`${route}\``);
|
|
7694
|
+
}
|
|
7695
|
+
lines.push("");
|
|
7696
|
+
}
|
|
7697
|
+
if (result.apiClients.issues.length > 0) {
|
|
7698
|
+
lines.push("## API Client Issues");
|
|
7699
|
+
lines.push("");
|
|
7700
|
+
for (const issue of result.apiClients.issues) {
|
|
7701
|
+
const icon = issue.severity === "error" ? "\u274C" : "\u26A0\uFE0F";
|
|
7702
|
+
lines.push(`### ${icon} ${issue.file}`);
|
|
7703
|
+
lines.push("");
|
|
7704
|
+
lines.push(`- **Type**: ${issue.type}`);
|
|
7705
|
+
lines.push(`- **Message**: ${issue.message}`);
|
|
7706
|
+
if (issue.navRoute) {
|
|
7707
|
+
lines.push(`- **NavRoute**: \`${issue.navRoute}\``);
|
|
7708
|
+
}
|
|
7709
|
+
lines.push(`- **Suggestion**: ${issue.suggestion}`);
|
|
7710
|
+
lines.push("");
|
|
7711
|
+
}
|
|
7712
|
+
}
|
|
7713
|
+
if (result.routes.missing.length > 0) {
|
|
7714
|
+
lines.push("## Missing Frontend Routes");
|
|
7715
|
+
lines.push("");
|
|
7716
|
+
lines.push("These backend NavRoutes have no corresponding frontend route:");
|
|
7717
|
+
lines.push("");
|
|
7718
|
+
for (const route of result.routes.missing) {
|
|
7719
|
+
lines.push(`- \`${route}\``);
|
|
7720
|
+
}
|
|
7721
|
+
lines.push("");
|
|
7722
|
+
}
|
|
7723
|
+
if (result.routes.orphaned.length > 0) {
|
|
7724
|
+
lines.push("## Orphaned Frontend Routes");
|
|
7725
|
+
lines.push("");
|
|
7726
|
+
lines.push("These frontend routes have no corresponding backend NavRoute:");
|
|
7727
|
+
lines.push("");
|
|
7728
|
+
for (const route of result.routes.orphaned) {
|
|
7729
|
+
lines.push(`- \`${route}\``);
|
|
7730
|
+
}
|
|
7731
|
+
lines.push("");
|
|
7732
|
+
}
|
|
7733
|
+
lines.push("## Recommendations");
|
|
7734
|
+
lines.push("");
|
|
7735
|
+
for (const rec of result.recommendations) {
|
|
7736
|
+
lines.push(`- ${rec}`);
|
|
7737
|
+
}
|
|
7738
|
+
lines.push("");
|
|
7739
|
+
lines.push("## Commands");
|
|
7740
|
+
lines.push("");
|
|
7741
|
+
lines.push("```bash");
|
|
7742
|
+
lines.push("# Regenerate route registry");
|
|
7743
|
+
lines.push('scaffold_routes source="controllers"');
|
|
7744
|
+
lines.push("");
|
|
7745
|
+
lines.push("# Generate API client for a specific NavRoute");
|
|
7746
|
+
lines.push('scaffold_api_client navRoute="platform.administration.users" name="User"');
|
|
7747
|
+
lines.push("```");
|
|
7748
|
+
return lines.join("\n");
|
|
7749
|
+
}
|
|
7750
|
+
|
|
7751
|
+
// src/resources/conventions.ts
|
|
7752
|
+
var conventionsResourceTemplate = {
|
|
7753
|
+
uri: "smartstack://conventions",
|
|
7754
|
+
name: "AtlasHub Conventions",
|
|
7755
|
+
description: "Documentation of AtlasHub/SmartStack naming conventions, patterns, and best practices",
|
|
7756
|
+
mimeType: "text/markdown"
|
|
7757
|
+
};
|
|
7758
|
+
async function getConventionsResource(config) {
|
|
7759
|
+
const { schemas, tablePrefixes, codePrefixes, migrationFormat, namespaces, servicePattern } = config.conventions;
|
|
7760
|
+
return `# AtlasHub SmartStack Conventions
|
|
7761
|
+
|
|
7762
|
+
## Overview
|
|
7763
|
+
|
|
7764
|
+
This document describes the mandatory conventions for extending the SmartStack/AtlasHub platform.
|
|
7765
|
+
Following these conventions ensures compatibility and prevents conflicts.
|
|
7766
|
+
|
|
7767
|
+
---
|
|
7768
|
+
|
|
7769
|
+
## 1. Database Conventions
|
|
7770
|
+
|
|
7771
|
+
### SQL Schemas
|
|
7772
|
+
|
|
7773
|
+
SmartStack uses SQL Server schemas to separate platform tables from client extensions:
|
|
7774
|
+
|
|
7775
|
+
| Schema | Usage | Description |
|
|
7776
|
+
|--------|-------|-------------|
|
|
7777
|
+
| \`${schemas.platform}\` | SmartStack platform | All native SmartStack tables |
|
|
7778
|
+
| \`${schemas.extensions}\` | Client extensions | Custom tables added by clients |
|
|
7779
|
+
|
|
7780
|
+
### Domain Table Prefixes
|
|
7781
|
+
|
|
7782
|
+
Tables are organized by domain using prefixes:
|
|
7783
|
+
|
|
7784
|
+
| Prefix | Domain | Example Tables |
|
|
7785
|
+
|--------|--------|----------------|
|
|
6359
7786
|
| \`auth_\` | Authorization | auth_Users, auth_Roles, auth_Permissions |
|
|
6360
7787
|
| \`nav_\` | Navigation | nav_Contexts, nav_Applications, nav_Modules |
|
|
6361
7788
|
| \`usr_\` | User profiles | usr_Profiles, usr_Preferences |
|
|
@@ -7307,7 +8734,7 @@ Run specific or all checks:
|
|
|
7307
8734
|
}
|
|
7308
8735
|
|
|
7309
8736
|
// src/resources/project-info.ts
|
|
7310
|
-
import
|
|
8737
|
+
import path18 from "path";
|
|
7311
8738
|
var projectInfoResourceTemplate = {
|
|
7312
8739
|
uri: "smartstack://project",
|
|
7313
8740
|
name: "SmartStack Project Info",
|
|
@@ -7344,16 +8771,16 @@ async function getProjectInfoResource(config) {
|
|
|
7344
8771
|
lines.push("```");
|
|
7345
8772
|
lines.push(`${projectInfo.name}/`);
|
|
7346
8773
|
if (structure.domain) {
|
|
7347
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
8774
|
+
lines.push(`\u251C\u2500\u2500 ${path18.basename(structure.domain)}/ # Domain layer (entities)`);
|
|
7348
8775
|
}
|
|
7349
8776
|
if (structure.application) {
|
|
7350
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
8777
|
+
lines.push(`\u251C\u2500\u2500 ${path18.basename(structure.application)}/ # Application layer (services)`);
|
|
7351
8778
|
}
|
|
7352
8779
|
if (structure.infrastructure) {
|
|
7353
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
8780
|
+
lines.push(`\u251C\u2500\u2500 ${path18.basename(structure.infrastructure)}/ # Infrastructure (EF Core)`);
|
|
7354
8781
|
}
|
|
7355
8782
|
if (structure.api) {
|
|
7356
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
8783
|
+
lines.push(`\u251C\u2500\u2500 ${path18.basename(structure.api)}/ # API layer (controllers)`);
|
|
7357
8784
|
}
|
|
7358
8785
|
if (structure.web) {
|
|
7359
8786
|
lines.push(`\u2514\u2500\u2500 web/smartstack-web/ # React frontend`);
|
|
@@ -7366,8 +8793,8 @@ async function getProjectInfoResource(config) {
|
|
|
7366
8793
|
lines.push("| Project | Path |");
|
|
7367
8794
|
lines.push("|---------|------|");
|
|
7368
8795
|
for (const csproj of projectInfo.csprojFiles) {
|
|
7369
|
-
const name =
|
|
7370
|
-
const relativePath =
|
|
8796
|
+
const name = path18.basename(csproj, ".csproj");
|
|
8797
|
+
const relativePath = path18.relative(projectPath, csproj);
|
|
7371
8798
|
lines.push(`| ${name} | \`${relativePath}\` |`);
|
|
7372
8799
|
}
|
|
7373
8800
|
lines.push("");
|
|
@@ -7377,10 +8804,10 @@ async function getProjectInfoResource(config) {
|
|
|
7377
8804
|
cwd: structure.migrations,
|
|
7378
8805
|
ignore: ["*.Designer.cs"]
|
|
7379
8806
|
});
|
|
7380
|
-
const migrations = migrationFiles.map((f) =>
|
|
8807
|
+
const migrations = migrationFiles.map((f) => path18.basename(f)).filter((f) => !f.includes("ModelSnapshot") && !f.includes(".Designer.")).sort();
|
|
7381
8808
|
lines.push("## EF Core Migrations");
|
|
7382
8809
|
lines.push("");
|
|
7383
|
-
lines.push(`**Location**: \`${
|
|
8810
|
+
lines.push(`**Location**: \`${path18.relative(projectPath, structure.migrations)}\``);
|
|
7384
8811
|
lines.push(`**Total Migrations**: ${migrations.length}`);
|
|
7385
8812
|
lines.push("");
|
|
7386
8813
|
if (migrations.length > 0) {
|
|
@@ -7415,11 +8842,11 @@ async function getProjectInfoResource(config) {
|
|
|
7415
8842
|
lines.push("dotnet build");
|
|
7416
8843
|
lines.push("");
|
|
7417
8844
|
lines.push("# Run API");
|
|
7418
|
-
lines.push(`cd ${structure.api ?
|
|
8845
|
+
lines.push(`cd ${structure.api ? path18.relative(projectPath, structure.api) : "src/Api"}`);
|
|
7419
8846
|
lines.push("dotnet run");
|
|
7420
8847
|
lines.push("");
|
|
7421
8848
|
lines.push("# Run frontend");
|
|
7422
|
-
lines.push(`cd ${structure.web ?
|
|
8849
|
+
lines.push(`cd ${structure.web ? path18.relative(projectPath, structure.web) : "web"}`);
|
|
7423
8850
|
lines.push("npm run dev");
|
|
7424
8851
|
lines.push("");
|
|
7425
8852
|
lines.push("# Create migration");
|
|
@@ -7442,7 +8869,7 @@ async function getProjectInfoResource(config) {
|
|
|
7442
8869
|
}
|
|
7443
8870
|
|
|
7444
8871
|
// src/resources/api-endpoints.ts
|
|
7445
|
-
import
|
|
8872
|
+
import path19 from "path";
|
|
7446
8873
|
var apiEndpointsResourceTemplate = {
|
|
7447
8874
|
uri: "smartstack://api/",
|
|
7448
8875
|
name: "SmartStack API Endpoints",
|
|
@@ -7467,7 +8894,7 @@ async function getApiEndpointsResource(config, endpointFilter) {
|
|
|
7467
8894
|
}
|
|
7468
8895
|
async function parseController(filePath, _rootPath) {
|
|
7469
8896
|
const content = await readText(filePath);
|
|
7470
|
-
const fileName =
|
|
8897
|
+
const fileName = path19.basename(filePath, ".cs");
|
|
7471
8898
|
const controllerName = fileName.replace("Controller", "");
|
|
7472
8899
|
const endpoints = [];
|
|
7473
8900
|
const routeMatch = content.match(/\[Route\s*\(\s*"([^"]+)"\s*\)\]/);
|
|
@@ -7614,7 +9041,7 @@ function getMethodEmoji(method) {
|
|
|
7614
9041
|
}
|
|
7615
9042
|
|
|
7616
9043
|
// src/resources/db-schema.ts
|
|
7617
|
-
import
|
|
9044
|
+
import path20 from "path";
|
|
7618
9045
|
var dbSchemaResourceTemplate = {
|
|
7619
9046
|
uri: "smartstack://schema/",
|
|
7620
9047
|
name: "SmartStack Database Schema",
|
|
@@ -7704,7 +9131,7 @@ async function parseEntity(filePath, rootPath, _config) {
|
|
|
7704
9131
|
tableName,
|
|
7705
9132
|
properties,
|
|
7706
9133
|
relationships,
|
|
7707
|
-
file:
|
|
9134
|
+
file: path20.relative(rootPath, filePath)
|
|
7708
9135
|
};
|
|
7709
9136
|
}
|
|
7710
9137
|
async function enrichFromConfigurations(entities, infrastructurePath, _config) {
|
|
@@ -7850,7 +9277,7 @@ function formatSchema(entities, filter, _config) {
|
|
|
7850
9277
|
}
|
|
7851
9278
|
|
|
7852
9279
|
// src/resources/entities.ts
|
|
7853
|
-
import
|
|
9280
|
+
import path21 from "path";
|
|
7854
9281
|
var entitiesResourceTemplate = {
|
|
7855
9282
|
uri: "smartstack://entities/",
|
|
7856
9283
|
name: "SmartStack Entities",
|
|
@@ -7910,7 +9337,7 @@ async function parseEntitySummary(filePath, rootPath, config) {
|
|
|
7910
9337
|
hasSoftDelete,
|
|
7911
9338
|
hasRowVersion,
|
|
7912
9339
|
file: filePath,
|
|
7913
|
-
relativePath:
|
|
9340
|
+
relativePath: path21.relative(rootPath, filePath)
|
|
7914
9341
|
};
|
|
7915
9342
|
}
|
|
7916
9343
|
function inferTableInfo(entityName, config) {
|
|
@@ -8102,7 +9529,11 @@ async function createServer() {
|
|
|
8102
9529
|
scaffoldTestsTool,
|
|
8103
9530
|
analyzeTestCoverageTool,
|
|
8104
9531
|
validateTestConventionsTool,
|
|
8105
|
-
suggestTestScenariosTool
|
|
9532
|
+
suggestTestScenariosTool,
|
|
9533
|
+
// Frontend Route Tools
|
|
9534
|
+
scaffoldApiClientTool,
|
|
9535
|
+
scaffoldRoutesTool,
|
|
9536
|
+
validateFrontendRoutesTool
|
|
8106
9537
|
]
|
|
8107
9538
|
};
|
|
8108
9539
|
});
|
|
@@ -8141,6 +9572,16 @@ async function createServer() {
|
|
|
8141
9572
|
case "suggest_test_scenarios":
|
|
8142
9573
|
result = await handleSuggestTestScenarios(args, config);
|
|
8143
9574
|
break;
|
|
9575
|
+
// Frontend Route Tools
|
|
9576
|
+
case "scaffold_api_client":
|
|
9577
|
+
result = await handleScaffoldApiClient(args ?? {}, config);
|
|
9578
|
+
break;
|
|
9579
|
+
case "scaffold_routes":
|
|
9580
|
+
result = await handleScaffoldRoutes(args ?? {}, config);
|
|
9581
|
+
break;
|
|
9582
|
+
case "validate_frontend_routes":
|
|
9583
|
+
result = await handleValidateFrontendRoutes(args ?? {}, config);
|
|
9584
|
+
break;
|
|
8144
9585
|
default:
|
|
8145
9586
|
throw new Error(`Unknown tool: ${name}`);
|
|
8146
9587
|
}
|