@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 CHANGED
@@ -75,17 +75,17 @@ if (envLevel && ["debug", "info", "warn", "error"].includes(envLevel)) {
75
75
  }
76
76
 
77
77
  // src/config.ts
78
- import path2 from "path";
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, path18, cause) {
85
+ constructor(message, operation, path22, cause) {
86
86
  super(message);
87
87
  this.operation = operation;
88
- this.path = path18;
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
- domain: "SmartStack.Domain",
222
- application: "SmartStack.Application",
223
- infrastructure: "SmartStack.Infrastructure",
224
- api: "SmartStack.Api"
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 = path2.join(process.cwd(), "config", "default-config.json");
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 path4 from "path";
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 path3 from "path";
502
- var execAsync = promisify(exec);
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 execAsync(`git ${command}`, options);
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 = path3.join(cwd || process.cwd(), ".git");
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/utils/dotnet.ts
564
- import { exec as exec2 } from "child_process";
565
- import { promisify as promisify2 } from "util";
566
- var execAsync2 = promisify2(exec2);
567
- async function findCsprojFiles(cwd) {
568
- return findFiles("**/*.csproj", { cwd: cwd || process.cwd() });
569
- }
570
- async function hasEfCore(csprojPath) {
571
- try {
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
- async function findDbContextName(projectPath) {
579
- try {
580
- const csFiles = await findFiles("**/*.cs", { cwd: projectPath });
581
- for (const file of csFiles) {
582
- const content = await readText(file);
583
- const match = content.match(/class\s+(\w+)\s*:\s*(?:\w+,\s*)*DbContext/);
584
- if (match) {
585
- return match[1];
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
- async function getTargetFramework(csprojPath) {
594
- try {
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: path4.basename(projectPath),
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(path4.dirname(csproj)) || void 0;
794
+ info.dbContextName = await findDbContextName(path5.dirname(csproj)) || void 0;
630
795
  break;
631
796
  }
632
797
  }
633
798
  }
634
- const packageJsonPath = path4.join(projectPath, "web", "smartstack-web", "package.json");
635
- if (await fileExists(packageJsonPath)) {
636
- try {
637
- const packageJson = JSON.parse(await readText(packageJsonPath));
638
- info.hasReact = !!packageJson.dependencies?.react;
639
- info.version = packageJson.version || info.version;
640
- } catch {
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.includes("SmartStack.Api")) || info.csprojFiles[0];
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 = path4.basename(csproj, ".csproj").toLowerCase();
658
- const projectDir = path4.dirname(csproj);
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 = path4.join(structure.infrastructure, "Persistence", "Migrations");
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 webPath = path4.join(projectPath, "web", "smartstack-web");
676
- if (await directoryExists(webPath)) {
677
- structure.web = webPath;
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 path5 from "path";
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: path5.relative(structure.root, 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: path5.relative(structure.root, 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: path5.relative(structure.root, 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: path5.relative(structure.root, 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: path5.relative(structure.root, 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 = path5.basename(file);
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: path5.relative(structure.root, 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) => path5.basename(f)).filter((f) => migrationPattern.test(f) && !f.includes("Designer")).sort();
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 = path5.basename(file, ".cs");
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: path5.relative(structure.root, 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: path5.relative(structure.root, 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 = path5.basename(file, ".cs");
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: path5.relative(structure.root, 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: path5.relative(structure.root, 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: path5.relative(structure.root, 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: path5.relative(structure.root, 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: path5.relative(structure.root, 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: path5.relative(structure.root, 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: path5.relative(structure.root, 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 = path5.basename(file, ".cs");
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: path5.relative(structure.root, 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: path5.relative(structure.root, 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: path5.relative(structure.root, 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 path6 from "path";
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 = path6.basename(file);
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: path6.relative(rootPath, 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: path6.relative(rootPath, 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 = path6.relative(projectPath, structure.migrations).replace(/\\/g, "/");
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 = path6.relative(projectPath, snapshotFiles[0]).replace(/\\/g, "/");
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) => path6.relative(structure.root, 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 path7 from "path";
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 = path7.join(basePath, "Services");
1866
- const interfacePath = path7.join(servicesPath, `I${name}Service.cs`);
1867
- const implementationPath = path7.join(servicesPath, `${name}Service.cs`);
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 || path7.join(projectRoot, "Domain");
2112
- const infraPath = structure.infrastructure || path7.join(projectRoot, "Infrastructure");
2113
- const entityFilePath = path7.join(domainPath, `${name}.cs`);
2114
- const configFilePath = path7.join(infraPath, "Persistence", "Configurations", `${name}Configuration.cs`);
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(path7.join(infraPath, "Persistence", "Configurations"));
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 || path7.join(projectRoot, "Api");
2234
- const controllersPath = path7.join(apiPath, "Controllers");
2235
- const controllerFilePath = path7.join(controllersPath, `${name}Controller.cs`);
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 || path7.join(projectRoot, "web", "smartstack-web");
2416
- const componentsPath = options?.outputPath || path7.join(webPath, "src", "components");
2417
- const hooksPath = path7.join(webPath, "src", "hooks");
2418
- const componentFilePath = path7.join(componentsPath, `${name}.tsx`);
2419
- const hookFilePath = path7.join(hooksPath, `use${name}.ts`);
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 ? path7.join(path7.dirname(structure.application), `${path7.basename(structure.application)}.Tests`, "Services") : path7.join(config.smartstack.projectPath, "Application.Tests", "Services");
2545
- const testFilePath = path7.join(testsPath, `${name}ServiceTests.cs`);
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 = path7.join(basePath, "DTOs", name);
2678
- const responseFilePath = path7.join(dtosPath, `${name}ResponseDto.cs`);
2679
- const createFilePath = path7.join(dtosPath, `Create${name}Dto.cs`);
2680
- const updateFilePath = path7.join(dtosPath, `Update${name}Dto.cs`);
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 = path7.join(basePath, "Validators");
2775
- const createValidatorFilePath = path7.join(validatorsPath, `Create${name}DtoValidator.cs`);
2776
- const updateValidatorFilePath = path7.join(validatorsPath, `Update${name}DtoValidator.cs`);
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 || path7.join(config.smartstack.projectPath, "Infrastructure");
2910
- const interfaceFilePath = path7.join(appPath, "Repositories", `I${name}Repository.cs`);
2911
- const implementationFilePath = path7.join(infraPath, "Repositories", `${name}Repository.cs`);
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(path7.join(appPath, "Repositories"));
2914
- await ensureDirectory(path7.join(infraPath, "Repositories"));
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"} ${path7.basename(file.path)}`);
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 path8 from "path";
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 = path8.basename(file, ".cs");
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 path9 from "path";
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 || path9.join(config.smartstack.projectPath, "Infrastructure");
3438
- const migrationsPath = path9.join(infraPath, "Migrations");
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 = path9.basename(file);
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 path10 from "path";
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 = path10.join(structure.root, "Tests", "Unit", "Domain", `${name}Tests.cs`);
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(path10.dirname(testPath));
5087
+ await ensureDirectory(path11.dirname(testPath));
4920
5088
  await writeText(testPath, content);
4921
5089
  }
4922
5090
  result.files.push({
4923
- path: path10.relative(structure.root, testPath),
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 = path10.join(structure.root, "Tests", "Security", `${name}SecurityTests.cs`);
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(path10.dirname(securityPath));
5105
+ await ensureDirectory(path11.dirname(securityPath));
4938
5106
  await writeText(securityPath, securityContent);
4939
5107
  }
4940
5108
  result.files.push({
4941
- path: path10.relative(structure.root, securityPath),
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 = path10.join(structure.root, "Tests", "Unit", "Services", `${name}ServiceTests.cs`);
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(path10.dirname(testPath));
5133
+ await ensureDirectory(path11.dirname(testPath));
4966
5134
  await writeText(testPath, content);
4967
5135
  }
4968
5136
  result.files.push({
4969
- path: path10.relative(structure.root, testPath),
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 = path10.join(structure.root, "Tests", "Integration", "Controllers", `${name}ControllerTests.cs`);
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(path10.dirname(testPath));
5163
+ await ensureDirectory(path11.dirname(testPath));
4996
5164
  await writeText(testPath, content);
4997
5165
  }
4998
5166
  result.files.push({
4999
- path: path10.relative(structure.root, testPath),
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 = path10.join(structure.root, "Tests", "Security", `${name}SecurityTests.cs`);
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(path10.dirname(securityPath));
5177
+ await ensureDirectory(path11.dirname(securityPath));
5010
5178
  await writeText(securityPath, securityContent);
5011
5179
  }
5012
5180
  result.files.push({
5013
- path: path10.relative(structure.root, securityPath),
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 = path10.join(structure.root, "Tests", "Unit", "Validators", `${name}ValidatorTests.cs`);
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(path10.dirname(testPath));
5202
+ await ensureDirectory(path11.dirname(testPath));
5035
5203
  await writeText(testPath, content);
5036
5204
  }
5037
5205
  result.files.push({
5038
- path: path10.relative(structure.root, testPath),
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 = path10.join(structure.root, "Tests", "Integration", "Repositories", `${name}RepositoryTests.cs`);
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(path10.dirname(testPath));
5229
+ await ensureDirectory(path11.dirname(testPath));
5062
5230
  await writeText(testPath, content);
5063
5231
  }
5064
5232
  result.files.push({
5065
- path: path10.relative(structure.root, testPath),
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 path11 from "path";
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 = path11.join(structure.root, "Tests", "Unit", "Domain");
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 = path11.basename(f, ".cs");
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 = path11.join(structure.root, "Tests", "Unit", "Services");
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 = path11.basename(f, ".cs");
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 = path11.basename(f, ".cs");
5420
+ const basename = path12.basename(f, ".cs");
5253
5421
  return basename.replace(/Controller$/, "");
5254
5422
  }).filter((n) => n !== "Base");
5255
- const testPath = path11.join(structure.root, "Tests", "Integration", "Controllers");
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 = path11.basename(f, ".cs");
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) => path11.basename(f, ".cs")).filter((name) => {
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 = path11.basename(f, ".cs");
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 path12 from "path";
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 = path12.join(structure.root, "Tests");
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 = path12.join(testsPath, testFile);
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(path12.sep);
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(path12.join("Unit", "Domain")));
5561
- const hasIntegrationControllers = testFiles.some((f) => f.includes(path12.join("Integration", "Controllers")));
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 path13 from "path";
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 path13.isAbsolute(name) ? name : path13.join(structure.root, name);
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 path13.join(searchPath, altFiles[0]);
6095
+ return path14.join(searchPath, altFiles[0]);
5928
6096
  }
5929
6097
  return null;
5930
6098
  }
5931
- return path13.join(searchPath, files[0]);
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/resources/conventions.ts
6325
- var conventionsResourceTemplate = {
6326
- uri: "smartstack://conventions",
6327
- name: "AtlasHub Conventions",
6328
- description: "Documentation of AtlasHub/SmartStack naming conventions, patterns, and best practices",
6329
- mimeType: "text/markdown"
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 getConventionsResource(config) {
6332
- const { schemas, tablePrefixes, codePrefixes, migrationFormat, namespaces, servicePattern } = config.conventions;
6333
- return `# AtlasHub SmartStack Conventions
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
- ## Overview
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
- This document describes the mandatory conventions for extending the SmartStack/AtlasHub platform.
6338
- Following these conventions ensures compatibility and prevents conflicts.
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
- ## 1. Database Conventions
6719
+ export interface ${name}CreateRequest {
6720
+ code: string;
6721
+ name?: string;
6722
+ description?: string;
6723
+ }
6343
6724
 
6344
- ### SQL Schemas
6725
+ export interface ${name}UpdateRequest {
6726
+ code?: string;
6727
+ name?: string;
6728
+ description?: string;
6729
+ isActive?: boolean;
6730
+ }
6345
6731
 
6346
- SmartStack uses SQL Server schemas to separate platform tables from client extensions:
6732
+ export interface ${name}ListResponse {
6733
+ items: ${name}[];
6734
+ totalCount: number;
6735
+ pageSize: number;
6736
+ currentPage: number;
6737
+ totalPages: number;
6738
+ }
6347
6739
 
6348
- | Schema | Usage | Description |
6349
- |--------|-------|-------------|
6350
- | \`${schemas.platform}\` | SmartStack platform | All native SmartStack tables |
6351
- | \`${schemas.extensions}\` | Client extensions | Custom tables added by clients |
6740
+ export interface PaginatedRequest {
6741
+ page?: number;
6742
+ pageSize?: number;
6743
+ sortBy?: string;
6744
+ sortDirection?: 'asc' | 'desc';
6745
+ filter?: string;
6746
+ }
6352
6747
 
6353
- ### Domain Table Prefixes
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
- Tables are organized by domain using prefixes:
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
- | Prefix | Domain | Example Tables |
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 path14 from "path";
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 ${path14.basename(structure.domain)}/ # Domain layer (entities)`);
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 ${path14.basename(structure.application)}/ # Application layer (services)`);
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 ${path14.basename(structure.infrastructure)}/ # Infrastructure (EF Core)`);
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 ${path14.basename(structure.api)}/ # API layer (controllers)`);
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 = path14.basename(csproj, ".csproj");
7370
- const relativePath = path14.relative(projectPath, csproj);
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) => path14.basename(f)).filter((f) => !f.includes("ModelSnapshot") && !f.includes(".Designer.")).sort();
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**: \`${path14.relative(projectPath, structure.migrations)}\``);
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 ? path14.relative(projectPath, structure.api) : "SmartStack.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 ? path14.relative(projectPath, structure.web) : "web/smartstack-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 path15 from "path";
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 = path15.basename(filePath, ".cs");
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 path16 from "path";
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: path16.relative(rootPath, filePath)
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 path17 from "path";
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: path17.relative(rootPath, filePath)
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
  }