@atlashub/smartstack-mcp 1.6.0 → 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 +378 -227
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -75,17 +75,17 @@ if (envLevel && ["debug", "info", "warn", "error"].includes(envLevel)) {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
// src/config.ts
|
|
78
|
-
import
|
|
78
|
+
import path3 from "path";
|
|
79
79
|
|
|
80
80
|
// src/utils/fs.ts
|
|
81
81
|
import { stat, mkdir, readFile, writeFile, cp, rm } from "fs/promises";
|
|
82
82
|
import path from "path";
|
|
83
83
|
import { glob } from "glob";
|
|
84
84
|
var FileSystemError = class extends Error {
|
|
85
|
-
constructor(message, operation,
|
|
85
|
+
constructor(message, operation, path22, cause) {
|
|
86
86
|
super(message);
|
|
87
87
|
this.operation = operation;
|
|
88
|
-
this.path =
|
|
88
|
+
this.path = path22;
|
|
89
89
|
this.cause = cause;
|
|
90
90
|
this.name = "FileSystemError";
|
|
91
91
|
}
|
|
@@ -184,6 +184,125 @@ async function findFiles(pattern, options = {}) {
|
|
|
184
184
|
return files;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
// src/utils/dotnet.ts
|
|
188
|
+
import { exec } from "child_process";
|
|
189
|
+
import { promisify } from "util";
|
|
190
|
+
import path2 from "path";
|
|
191
|
+
var execAsync = promisify(exec);
|
|
192
|
+
async function findCsprojFiles(cwd) {
|
|
193
|
+
return findFiles("**/*.csproj", { cwd: cwd || process.cwd() });
|
|
194
|
+
}
|
|
195
|
+
async function hasEfCore(csprojPath) {
|
|
196
|
+
try {
|
|
197
|
+
const content = await readText(csprojPath);
|
|
198
|
+
return content.includes("Microsoft.EntityFrameworkCore");
|
|
199
|
+
} catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function findDbContextName(projectPath) {
|
|
204
|
+
try {
|
|
205
|
+
const csFiles = await findFiles("**/*.cs", { cwd: projectPath });
|
|
206
|
+
for (const file of csFiles) {
|
|
207
|
+
const content = await readText(file);
|
|
208
|
+
const match = content.match(/class\s+(\w+)\s*:\s*(?:\w+,\s*)*DbContext/);
|
|
209
|
+
if (match) {
|
|
210
|
+
return match[1];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function getTargetFramework(csprojPath) {
|
|
219
|
+
try {
|
|
220
|
+
const content = await readText(csprojPath);
|
|
221
|
+
const match = content.match(/<TargetFramework>([^<]+)<\/TargetFramework>/);
|
|
222
|
+
return match ? match[1] : null;
|
|
223
|
+
} catch {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async function detectNamespaces(csprojFiles) {
|
|
228
|
+
if (csprojFiles.length === 0) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
const projectNames = csprojFiles.map((f) => path2.basename(f, ".csproj"));
|
|
232
|
+
const layerSuffixes = ["Domain", "Application", "Infrastructure", "Api", "Web", "Core", "Tests"];
|
|
233
|
+
const baseNamespaces = [];
|
|
234
|
+
for (const name of projectNames) {
|
|
235
|
+
const parts = name.split(".");
|
|
236
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
237
|
+
const part = parts[i];
|
|
238
|
+
if (layerSuffixes.some((s) => s.toLowerCase() === part.toLowerCase())) {
|
|
239
|
+
const baseParts = parts.slice(0, i);
|
|
240
|
+
if (baseParts.length > 0) {
|
|
241
|
+
baseNamespaces.push(baseParts.join("."));
|
|
242
|
+
}
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (baseNamespaces.length === 0) {
|
|
248
|
+
const commonPrefix = findCommonPrefix(projectNames);
|
|
249
|
+
if (commonPrefix && commonPrefix.length > 0) {
|
|
250
|
+
const base = commonPrefix.replace(/\.$/, "");
|
|
251
|
+
if (base) {
|
|
252
|
+
baseNamespaces.push(base);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (baseNamespaces.length === 0) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
const baseNamespace = getMostCommon(baseNamespaces) || baseNamespaces[0];
|
|
260
|
+
return {
|
|
261
|
+
baseNamespace,
|
|
262
|
+
domain: `${baseNamespace}.Domain`,
|
|
263
|
+
application: `${baseNamespace}.Application`,
|
|
264
|
+
infrastructure: `${baseNamespace}.Infrastructure`,
|
|
265
|
+
api: `${baseNamespace}.Api`
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function findCommonPrefix(strings) {
|
|
269
|
+
if (strings.length === 0) return "";
|
|
270
|
+
if (strings.length === 1) {
|
|
271
|
+
const parts = strings[0].split(".");
|
|
272
|
+
if (parts.length > 1) {
|
|
273
|
+
return parts.slice(0, -1).join(".") + ".";
|
|
274
|
+
}
|
|
275
|
+
return strings[0] + ".";
|
|
276
|
+
}
|
|
277
|
+
let prefix = strings[0];
|
|
278
|
+
for (let i = 1; i < strings.length; i++) {
|
|
279
|
+
while (strings[i].indexOf(prefix) !== 0 && prefix.length > 0) {
|
|
280
|
+
prefix = prefix.slice(0, -1);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const lastDot = prefix.lastIndexOf(".");
|
|
284
|
+
if (lastDot > 0 && !prefix.endsWith(".")) {
|
|
285
|
+
prefix = prefix.slice(0, lastDot + 1);
|
|
286
|
+
}
|
|
287
|
+
return prefix;
|
|
288
|
+
}
|
|
289
|
+
function getMostCommon(arr) {
|
|
290
|
+
if (arr.length === 0) return null;
|
|
291
|
+
const counts = /* @__PURE__ */ new Map();
|
|
292
|
+
for (const item of arr) {
|
|
293
|
+
counts.set(item, (counts.get(item) || 0) + 1);
|
|
294
|
+
}
|
|
295
|
+
let maxCount = 0;
|
|
296
|
+
let mostCommon = arr[0];
|
|
297
|
+
for (const [item, count] of counts) {
|
|
298
|
+
if (count > maxCount) {
|
|
299
|
+
maxCount = count;
|
|
300
|
+
mostCommon = item;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return mostCommon;
|
|
304
|
+
}
|
|
305
|
+
|
|
187
306
|
// src/config.ts
|
|
188
307
|
var defaultConfig = {
|
|
189
308
|
version: "1.0.0",
|
|
@@ -218,10 +337,12 @@ var defaultConfig = {
|
|
|
218
337
|
},
|
|
219
338
|
migrationFormat: "{context}_v{version}_{sequence}_{Description}",
|
|
220
339
|
namespaces: {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
340
|
+
// Empty = auto-detect from .csproj files
|
|
341
|
+
// Can be overridden in config file for custom namespaces
|
|
342
|
+
domain: "",
|
|
343
|
+
application: "",
|
|
344
|
+
infrastructure: "",
|
|
345
|
+
api: ""
|
|
225
346
|
},
|
|
226
347
|
servicePattern: {
|
|
227
348
|
interface: "I{Name}Service",
|
|
@@ -268,7 +389,7 @@ async function getConfig() {
|
|
|
268
389
|
if (cachedConfig) {
|
|
269
390
|
return cachedConfig;
|
|
270
391
|
}
|
|
271
|
-
const configPath =
|
|
392
|
+
const configPath = path3.join(process.cwd(), "config", "default-config.json");
|
|
272
393
|
if (await fileExists(configPath)) {
|
|
273
394
|
try {
|
|
274
395
|
const fileConfig = await readJson(configPath);
|
|
@@ -285,6 +406,32 @@ async function getConfig() {
|
|
|
285
406
|
cachedConfig.smartstack.projectPath = resolveProjectPath(
|
|
286
407
|
cachedConfig.smartstack.projectPath
|
|
287
408
|
);
|
|
409
|
+
const namespacesEmpty = !cachedConfig.conventions.namespaces.domain || !cachedConfig.conventions.namespaces.application || !cachedConfig.conventions.namespaces.infrastructure || !cachedConfig.conventions.namespaces.api;
|
|
410
|
+
if (namespacesEmpty && cachedConfig.smartstack.projectPath) {
|
|
411
|
+
try {
|
|
412
|
+
const csprojFiles = await findCsprojFiles(cachedConfig.smartstack.projectPath);
|
|
413
|
+
const detected = await detectNamespaces(csprojFiles);
|
|
414
|
+
if (detected) {
|
|
415
|
+
if (!cachedConfig.conventions.namespaces.domain) {
|
|
416
|
+
cachedConfig.conventions.namespaces.domain = detected.domain;
|
|
417
|
+
}
|
|
418
|
+
if (!cachedConfig.conventions.namespaces.application) {
|
|
419
|
+
cachedConfig.conventions.namespaces.application = detected.application;
|
|
420
|
+
}
|
|
421
|
+
if (!cachedConfig.conventions.namespaces.infrastructure) {
|
|
422
|
+
cachedConfig.conventions.namespaces.infrastructure = detected.infrastructure;
|
|
423
|
+
}
|
|
424
|
+
if (!cachedConfig.conventions.namespaces.api) {
|
|
425
|
+
cachedConfig.conventions.namespaces.api = detected.api;
|
|
426
|
+
}
|
|
427
|
+
logger.info("Namespaces auto-detected from project", { baseNamespace: detected.baseNamespace });
|
|
428
|
+
} else {
|
|
429
|
+
logger.warn("Could not auto-detect namespaces from .csproj files. Configure manually in config file.");
|
|
430
|
+
}
|
|
431
|
+
} catch (error) {
|
|
432
|
+
logger.warn("Failed to auto-detect namespaces", { error });
|
|
433
|
+
}
|
|
434
|
+
}
|
|
288
435
|
if (process.env.SMARTSTACK_API_URL) {
|
|
289
436
|
cachedConfig.smartstack.apiUrl = process.env.SMARTSTACK_API_URL;
|
|
290
437
|
}
|
|
@@ -492,6 +639,7 @@ var SuggestTestScenariosInputSchema = z.object({
|
|
|
492
639
|
depth: z.enum(["basic", "comprehensive", "security-focused"]).default("comprehensive").describe("Depth of analysis")
|
|
493
640
|
});
|
|
494
641
|
var ScaffoldApiClientInputSchema = z.object({
|
|
642
|
+
path: z.string().optional().describe("Path to SmartStack project root (defaults to configured project path)"),
|
|
495
643
|
navRoute: z.string().min(1).describe('NavRoute path (e.g., "platform.administration.users")'),
|
|
496
644
|
name: z.string().min(1).describe('Entity name in PascalCase (e.g., "User", "Order")'),
|
|
497
645
|
methods: z.array(z.enum(["getAll", "getById", "create", "update", "delete", "search", "export"])).default(["getAll", "getById", "create", "update", "delete"]).describe("API methods to generate"),
|
|
@@ -503,6 +651,7 @@ var ScaffoldApiClientInputSchema = z.object({
|
|
|
503
651
|
}).optional()
|
|
504
652
|
});
|
|
505
653
|
var ScaffoldRoutesInputSchema = z.object({
|
|
654
|
+
path: z.string().optional().describe("Path to SmartStack project root (defaults to configured project path)"),
|
|
506
655
|
source: z.enum(["controllers", "navigation", "manual"]).default("controllers").describe("Source for route discovery: controllers (scan NavRoute attributes), navigation (from DB), manual (from config)"),
|
|
507
656
|
scope: z.enum(["all", "platform", "business", "extensions"]).default("all").describe("Scope of routes to generate"),
|
|
508
657
|
options: z.object({
|
|
@@ -514,6 +663,7 @@ var ScaffoldRoutesInputSchema = z.object({
|
|
|
514
663
|
}).optional()
|
|
515
664
|
});
|
|
516
665
|
var ValidateFrontendRoutesInputSchema = z.object({
|
|
666
|
+
path: z.string().optional().describe("Path to SmartStack project root (defaults to configured project path)"),
|
|
517
667
|
scope: z.enum(["api-clients", "routes", "registry", "all"]).default("all").describe("Scope of validation"),
|
|
518
668
|
options: z.object({
|
|
519
669
|
fix: z.boolean().default(false).describe("Auto-fix minor issues"),
|
|
@@ -522,13 +672,13 @@ var ValidateFrontendRoutesInputSchema = z.object({
|
|
|
522
672
|
});
|
|
523
673
|
|
|
524
674
|
// src/lib/detector.ts
|
|
525
|
-
import
|
|
675
|
+
import path5 from "path";
|
|
526
676
|
|
|
527
677
|
// src/utils/git.ts
|
|
528
|
-
import { exec } from "child_process";
|
|
529
|
-
import { promisify } from "util";
|
|
530
|
-
import
|
|
531
|
-
var
|
|
678
|
+
import { exec as exec2 } from "child_process";
|
|
679
|
+
import { promisify as promisify2 } from "util";
|
|
680
|
+
import path4 from "path";
|
|
681
|
+
var execAsync2 = promisify2(exec2);
|
|
532
682
|
var GitError = class extends Error {
|
|
533
683
|
constructor(message, command, cwd, cause) {
|
|
534
684
|
super(message);
|
|
@@ -541,7 +691,7 @@ var GitError = class extends Error {
|
|
|
541
691
|
async function git(command, cwd) {
|
|
542
692
|
const options = cwd ? { cwd, maxBuffer: 10 * 1024 * 1024 } : { maxBuffer: 10 * 1024 * 1024 };
|
|
543
693
|
try {
|
|
544
|
-
const { stdout } = await
|
|
694
|
+
const { stdout } = await execAsync2(`git ${command}`, options);
|
|
545
695
|
return stdout.trim();
|
|
546
696
|
} catch (error) {
|
|
547
697
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -555,7 +705,7 @@ async function git(command, cwd) {
|
|
|
555
705
|
}
|
|
556
706
|
}
|
|
557
707
|
async function isGitRepo(cwd) {
|
|
558
|
-
const gitDir =
|
|
708
|
+
const gitDir = path4.join(cwd || process.cwd(), ".git");
|
|
559
709
|
return directoryExists(gitDir);
|
|
560
710
|
}
|
|
561
711
|
async function getCurrentBranch(cwd) {
|
|
@@ -589,51 +739,37 @@ async function getDiff(fromBranch, toBranch, filePath, cwd) {
|
|
|
589
739
|
}
|
|
590
740
|
}
|
|
591
741
|
|
|
592
|
-
// src/
|
|
593
|
-
import {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
const content = await readText(csprojPath);
|
|
602
|
-
return content.includes("Microsoft.EntityFrameworkCore");
|
|
603
|
-
} catch {
|
|
604
|
-
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
|
+
}
|
|
605
751
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
}
|
|
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;
|
|
616
761
|
}
|
|
617
|
-
return null;
|
|
618
|
-
} catch {
|
|
619
|
-
return null;
|
|
620
762
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
const content = await readText(csprojPath);
|
|
625
|
-
const match = content.match(/<TargetFramework>([^<]+)<\/TargetFramework>/);
|
|
626
|
-
return match ? match[1] : null;
|
|
627
|
-
} catch {
|
|
628
|
-
return null;
|
|
763
|
+
const srcWeb = path5.join(projectPath, "src", "web");
|
|
764
|
+
if (await fileExists(path5.join(srcWeb, "package.json"))) {
|
|
765
|
+
return srcWeb;
|
|
629
766
|
}
|
|
767
|
+
return null;
|
|
630
768
|
}
|
|
631
|
-
|
|
632
|
-
// src/lib/detector.ts
|
|
633
769
|
async function detectProject(projectPath) {
|
|
634
770
|
logger.debug("Detecting project info", { path: projectPath });
|
|
635
771
|
const info = {
|
|
636
|
-
name:
|
|
772
|
+
name: path5.basename(projectPath),
|
|
637
773
|
version: "0.0.0",
|
|
638
774
|
isGitRepo: false,
|
|
639
775
|
hasDotNet: false,
|
|
@@ -655,22 +791,25 @@ async function detectProject(projectPath) {
|
|
|
655
791
|
for (const csproj of info.csprojFiles) {
|
|
656
792
|
if (await hasEfCore(csproj)) {
|
|
657
793
|
info.hasEfCore = true;
|
|
658
|
-
info.dbContextName = await findDbContextName(
|
|
794
|
+
info.dbContextName = await findDbContextName(path5.dirname(csproj)) || void 0;
|
|
659
795
|
break;
|
|
660
796
|
}
|
|
661
797
|
}
|
|
662
798
|
}
|
|
663
|
-
const
|
|
664
|
-
if (
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
+
}
|
|
670
809
|
}
|
|
671
810
|
}
|
|
672
811
|
if (info.csprojFiles.length > 0) {
|
|
673
|
-
const mainCsproj = info.csprojFiles.find((f) => f.
|
|
812
|
+
const mainCsproj = info.csprojFiles.find((f) => /\.Api\.csproj$/i.test(f)) || info.csprojFiles.find((f) => /Api\.csproj$/i.test(f)) || info.csprojFiles[0];
|
|
674
813
|
const targetFramework = await getTargetFramework(mainCsproj);
|
|
675
814
|
if (targetFramework) {
|
|
676
815
|
logger.debug("Target framework detected", { framework: targetFramework });
|
|
@@ -683,8 +822,8 @@ async function findSmartStackStructure(projectPath) {
|
|
|
683
822
|
const structure = { root: projectPath };
|
|
684
823
|
const csprojFiles = await findCsprojFiles(projectPath);
|
|
685
824
|
for (const csproj of csprojFiles) {
|
|
686
|
-
const projectName =
|
|
687
|
-
const projectDir =
|
|
825
|
+
const projectName = path5.basename(csproj, ".csproj").toLowerCase();
|
|
826
|
+
const projectDir = path5.dirname(csproj);
|
|
688
827
|
if (projectName.includes("domain")) {
|
|
689
828
|
structure.domain = projectDir;
|
|
690
829
|
} else if (projectName.includes("application")) {
|
|
@@ -696,14 +835,14 @@ async function findSmartStackStructure(projectPath) {
|
|
|
696
835
|
}
|
|
697
836
|
}
|
|
698
837
|
if (structure.infrastructure) {
|
|
699
|
-
const migrationsPath =
|
|
838
|
+
const migrationsPath = path5.join(structure.infrastructure, "Persistence", "Migrations");
|
|
700
839
|
if (await directoryExists(migrationsPath)) {
|
|
701
840
|
structure.migrations = migrationsPath;
|
|
702
841
|
}
|
|
703
842
|
}
|
|
704
|
-
const
|
|
705
|
-
if (
|
|
706
|
-
structure.web =
|
|
843
|
+
const webFolder = await findWebProjectFolder(projectPath);
|
|
844
|
+
if (webFolder) {
|
|
845
|
+
structure.web = webFolder;
|
|
707
846
|
}
|
|
708
847
|
return structure;
|
|
709
848
|
}
|
|
@@ -724,7 +863,7 @@ async function findControllerFiles(apiPath) {
|
|
|
724
863
|
}
|
|
725
864
|
|
|
726
865
|
// src/tools/validate-conventions.ts
|
|
727
|
-
import
|
|
866
|
+
import path6 from "path";
|
|
728
867
|
var validateConventionsTool = {
|
|
729
868
|
name: "validate_conventions",
|
|
730
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)",
|
|
@@ -813,7 +952,7 @@ async function validateTablePrefixes(structure, config, result) {
|
|
|
813
952
|
type: "error",
|
|
814
953
|
category: "tables",
|
|
815
954
|
message: `Table "${tableName}" uses invalid schema "${schemaName}"`,
|
|
816
|
-
file:
|
|
955
|
+
file: path6.relative(structure.root, file),
|
|
817
956
|
suggestion: `Use schema "${config.conventions.schemas.platform}" for SmartStack tables or "${config.conventions.schemas.extensions}" for client extensions`
|
|
818
957
|
});
|
|
819
958
|
}
|
|
@@ -823,7 +962,7 @@ async function validateTablePrefixes(structure, config, result) {
|
|
|
823
962
|
type: "warning",
|
|
824
963
|
category: "tables",
|
|
825
964
|
message: `Table "${tableName}" does not use a standard domain prefix`,
|
|
826
|
-
file:
|
|
965
|
+
file: path6.relative(structure.root, file),
|
|
827
966
|
suggestion: `Consider using a domain prefix: ${validPrefixes.slice(0, 5).join(", ")}, etc.`
|
|
828
967
|
});
|
|
829
968
|
}
|
|
@@ -838,7 +977,7 @@ async function validateTablePrefixes(structure, config, result) {
|
|
|
838
977
|
type: "error",
|
|
839
978
|
category: "tables",
|
|
840
979
|
message: `Table "${tableName}" uses unknown schema constant "${schemaConstant}"`,
|
|
841
|
-
file:
|
|
980
|
+
file: path6.relative(structure.root, file),
|
|
842
981
|
suggestion: `Use SchemaConstants.Core for SmartStack tables or SchemaConstants.Extensions for client extensions`
|
|
843
982
|
});
|
|
844
983
|
}
|
|
@@ -848,7 +987,7 @@ async function validateTablePrefixes(structure, config, result) {
|
|
|
848
987
|
type: "warning",
|
|
849
988
|
category: "tables",
|
|
850
989
|
message: `Table "${tableName}" does not use a standard domain prefix`,
|
|
851
|
-
file:
|
|
990
|
+
file: path6.relative(structure.root, file),
|
|
852
991
|
suggestion: `Consider using a domain prefix: ${validPrefixes.slice(0, 5).join(", ")}, etc.`
|
|
853
992
|
});
|
|
854
993
|
}
|
|
@@ -861,7 +1000,7 @@ async function validateTablePrefixes(structure, config, result) {
|
|
|
861
1000
|
type: "error",
|
|
862
1001
|
category: "tables",
|
|
863
1002
|
message: `Table "${tableName}" is missing schema specification`,
|
|
864
|
-
file:
|
|
1003
|
+
file: path6.relative(structure.root, file),
|
|
865
1004
|
suggestion: `Add schema: .ToTable("${tableName}", SchemaConstants.Core)`
|
|
866
1005
|
});
|
|
867
1006
|
}
|
|
@@ -881,7 +1020,7 @@ async function validateMigrationNaming(structure, _config, result) {
|
|
|
881
1020
|
const migrationPattern = /^(\w+)_v(\d+\.\d+\.\d+)_(\d{3})_(.+)\.cs$/;
|
|
882
1021
|
const designerPattern = /\.Designer\.cs$/;
|
|
883
1022
|
for (const file of migrationFiles) {
|
|
884
|
-
const fileName =
|
|
1023
|
+
const fileName = path6.basename(file);
|
|
885
1024
|
if (designerPattern.test(fileName) || fileName.includes("ModelSnapshot")) {
|
|
886
1025
|
continue;
|
|
887
1026
|
}
|
|
@@ -890,12 +1029,12 @@ async function validateMigrationNaming(structure, _config, result) {
|
|
|
890
1029
|
type: "error",
|
|
891
1030
|
category: "migrations",
|
|
892
1031
|
message: `Migration "${fileName}" does not follow naming convention`,
|
|
893
|
-
file:
|
|
1032
|
+
file: path6.relative(structure.root, file),
|
|
894
1033
|
suggestion: `Expected format: {context}_v{version}_{sequence}_{Description}.cs (e.g., core_v1.0.0_001_CreateAuthUsers.cs)`
|
|
895
1034
|
});
|
|
896
1035
|
}
|
|
897
1036
|
}
|
|
898
|
-
const orderedMigrations = migrationFiles.map((f) =>
|
|
1037
|
+
const orderedMigrations = migrationFiles.map((f) => path6.basename(f)).filter((f) => migrationPattern.test(f) && !f.includes("Designer")).sort();
|
|
899
1038
|
for (let i = 1; i < orderedMigrations.length; i++) {
|
|
900
1039
|
const prev = orderedMigrations[i - 1];
|
|
901
1040
|
const curr = orderedMigrations[i];
|
|
@@ -928,7 +1067,7 @@ async function validateServiceInterfaces(structure, _config, result) {
|
|
|
928
1067
|
});
|
|
929
1068
|
for (const file of serviceFiles) {
|
|
930
1069
|
const content = await readText(file);
|
|
931
|
-
const fileName =
|
|
1070
|
+
const fileName = path6.basename(file, ".cs");
|
|
932
1071
|
if (fileName.startsWith("I")) continue;
|
|
933
1072
|
const expectedInterface = `I${fileName}`;
|
|
934
1073
|
const interfacePattern = new RegExp(`:\\s*${expectedInterface}\\b`);
|
|
@@ -939,7 +1078,7 @@ async function validateServiceInterfaces(structure, _config, result) {
|
|
|
939
1078
|
type: "warning",
|
|
940
1079
|
category: "services",
|
|
941
1080
|
message: `Service "${fileName}" should implement "${expectedInterface}"`,
|
|
942
|
-
file:
|
|
1081
|
+
file: path6.relative(structure.root, file),
|
|
943
1082
|
suggestion: `Create interface ${expectedInterface} and implement it`
|
|
944
1083
|
});
|
|
945
1084
|
}
|
|
@@ -966,7 +1105,7 @@ async function validateNamespaces(structure, config, result) {
|
|
|
966
1105
|
type: "error",
|
|
967
1106
|
category: "namespaces",
|
|
968
1107
|
message: `${layer.name} file has incorrect namespace "${namespace}"`,
|
|
969
|
-
file:
|
|
1108
|
+
file: path6.relative(structure.root, file),
|
|
970
1109
|
suggestion: `Should start with "${layer.expected}"`
|
|
971
1110
|
});
|
|
972
1111
|
}
|
|
@@ -986,7 +1125,7 @@ async function validateEntities(structure, _config, result) {
|
|
|
986
1125
|
const entityFiles = await findFiles("**/*.cs", { cwd: structure.domain });
|
|
987
1126
|
for (const file of entityFiles) {
|
|
988
1127
|
const content = await readText(file);
|
|
989
|
-
const fileName =
|
|
1128
|
+
const fileName = path6.basename(file, ".cs");
|
|
990
1129
|
if (fileName.endsWith("Dto") || fileName.endsWith("Command") || fileName.endsWith("Query") || fileName.endsWith("Handler") || fileName.endsWith("Validator") || fileName.endsWith("Exception") || fileName.startsWith("I")) {
|
|
991
1130
|
continue;
|
|
992
1131
|
}
|
|
@@ -1005,7 +1144,7 @@ async function validateEntities(structure, _config, result) {
|
|
|
1005
1144
|
type: "warning",
|
|
1006
1145
|
category: "entities",
|
|
1007
1146
|
message: `Entity "${entityName}" inherits BaseEntity but doesn't implement ITenantEntity`,
|
|
1008
|
-
file:
|
|
1147
|
+
file: path6.relative(structure.root, file),
|
|
1009
1148
|
suggestion: "Add ITenantEntity interface for multi-tenant support, or use SystemEntity for platform-level entities"
|
|
1010
1149
|
});
|
|
1011
1150
|
}
|
|
@@ -1014,7 +1153,7 @@ async function validateEntities(structure, _config, result) {
|
|
|
1014
1153
|
type: "warning",
|
|
1015
1154
|
category: "entities",
|
|
1016
1155
|
message: `Entity "${entityName}" is missing private constructor for EF Core`,
|
|
1017
|
-
file:
|
|
1156
|
+
file: path6.relative(structure.root, file),
|
|
1018
1157
|
suggestion: `Add: private ${entityName}() { }`
|
|
1019
1158
|
});
|
|
1020
1159
|
}
|
|
@@ -1023,7 +1162,7 @@ async function validateEntities(structure, _config, result) {
|
|
|
1023
1162
|
type: "warning",
|
|
1024
1163
|
category: "entities",
|
|
1025
1164
|
message: `Entity "${entityName}" is missing factory method`,
|
|
1026
|
-
file:
|
|
1165
|
+
file: path6.relative(structure.root, file),
|
|
1027
1166
|
suggestion: `Add factory method: public static ${entityName} Create(...)`
|
|
1028
1167
|
});
|
|
1029
1168
|
}
|
|
@@ -1062,7 +1201,7 @@ async function validateTenantAwareness(structure, _config, result) {
|
|
|
1062
1201
|
type: "error",
|
|
1063
1202
|
category: "tenants",
|
|
1064
1203
|
message: `Entity "${entityName}" implements ITenantEntity but is missing TenantId property`,
|
|
1065
|
-
file:
|
|
1204
|
+
file: path6.relative(structure.root, file),
|
|
1066
1205
|
suggestion: "Add: public Guid TenantId { get; private set; }"
|
|
1067
1206
|
});
|
|
1068
1207
|
}
|
|
@@ -1072,7 +1211,7 @@ async function validateTenantAwareness(structure, _config, result) {
|
|
|
1072
1211
|
type: "error",
|
|
1073
1212
|
category: "tenants",
|
|
1074
1213
|
message: `Entity "${entityName}" implements ITenantEntity but Create() doesn't require tenantId`,
|
|
1075
|
-
file:
|
|
1214
|
+
file: path6.relative(structure.root, file),
|
|
1076
1215
|
suggestion: "Add tenantId as first parameter: Create(Guid tenantId, ...)"
|
|
1077
1216
|
});
|
|
1078
1217
|
}
|
|
@@ -1084,7 +1223,7 @@ async function validateTenantAwareness(structure, _config, result) {
|
|
|
1084
1223
|
type: "error",
|
|
1085
1224
|
category: "tenants",
|
|
1086
1225
|
message: `System entity "${entityName}" should not have TenantId`,
|
|
1087
|
-
file:
|
|
1226
|
+
file: path6.relative(structure.root, file),
|
|
1088
1227
|
suggestion: "Remove TenantId from system entities"
|
|
1089
1228
|
});
|
|
1090
1229
|
}
|
|
@@ -1095,7 +1234,7 @@ async function validateTenantAwareness(structure, _config, result) {
|
|
|
1095
1234
|
type: "warning",
|
|
1096
1235
|
category: "tenants",
|
|
1097
1236
|
message: `Entity "${entityName}" has TenantId but doesn't implement ITenantEntity`,
|
|
1098
|
-
file:
|
|
1237
|
+
file: path6.relative(structure.root, file),
|
|
1099
1238
|
suggestion: "Add ITenantEntity interface for explicit tenant-awareness"
|
|
1100
1239
|
});
|
|
1101
1240
|
}
|
|
@@ -1150,7 +1289,7 @@ async function validateControllerRoutes(structure, _config, result) {
|
|
|
1150
1289
|
let systemControllerCount = 0;
|
|
1151
1290
|
for (const file of controllerFiles) {
|
|
1152
1291
|
const content = await readText(file);
|
|
1153
|
-
const fileName =
|
|
1292
|
+
const fileName = path6.basename(file, ".cs");
|
|
1154
1293
|
if (systemControllers.includes(fileName)) {
|
|
1155
1294
|
systemControllerCount++;
|
|
1156
1295
|
continue;
|
|
@@ -1168,7 +1307,7 @@ async function validateControllerRoutes(structure, _config, result) {
|
|
|
1168
1307
|
type: "warning",
|
|
1169
1308
|
category: "controllers",
|
|
1170
1309
|
message: `Controller "${fileName}" has NavRoute with insufficient depth: "${routePath}"`,
|
|
1171
|
-
file:
|
|
1310
|
+
file: path6.relative(structure.root, file),
|
|
1172
1311
|
suggestion: 'NavRoute should have at least 2 levels: "context.application" (e.g., "platform.administration")'
|
|
1173
1312
|
});
|
|
1174
1313
|
}
|
|
@@ -1178,7 +1317,7 @@ async function validateControllerRoutes(structure, _config, result) {
|
|
|
1178
1317
|
type: "error",
|
|
1179
1318
|
category: "controllers",
|
|
1180
1319
|
message: `Controller "${fileName}" has NavRoute with uppercase characters: "${routePath}"`,
|
|
1181
|
-
file:
|
|
1320
|
+
file: path6.relative(structure.root, file),
|
|
1182
1321
|
suggestion: 'NavRoute paths must be lowercase (e.g., "platform.administration.users")'
|
|
1183
1322
|
});
|
|
1184
1323
|
}
|
|
@@ -1189,7 +1328,7 @@ async function validateControllerRoutes(structure, _config, result) {
|
|
|
1189
1328
|
type: "warning",
|
|
1190
1329
|
category: "controllers",
|
|
1191
1330
|
message: `Controller "${fileName}" uses hardcoded Route instead of NavRoute`,
|
|
1192
|
-
file:
|
|
1331
|
+
file: path6.relative(structure.root, file),
|
|
1193
1332
|
suggestion: 'Use [NavRoute("context.application.module")] for navigation-based routing'
|
|
1194
1333
|
});
|
|
1195
1334
|
}
|
|
@@ -1257,7 +1396,7 @@ function formatResult(result) {
|
|
|
1257
1396
|
}
|
|
1258
1397
|
|
|
1259
1398
|
// src/tools/check-migrations.ts
|
|
1260
|
-
import
|
|
1399
|
+
import path7 from "path";
|
|
1261
1400
|
var checkMigrationsTool = {
|
|
1262
1401
|
name: "check_migrations",
|
|
1263
1402
|
description: "Analyze EF Core migrations for conflicts, ordering issues, and ModelSnapshot discrepancies between branches",
|
|
@@ -1316,7 +1455,7 @@ async function parseMigrations(migrationsPath, rootPath) {
|
|
|
1316
1455
|
const migrations = [];
|
|
1317
1456
|
const pattern = /^(\w+)_v(\d+\.\d+\.\d+)_(\d{3})_(.+)\.cs$/;
|
|
1318
1457
|
for (const file of files) {
|
|
1319
|
-
const fileName =
|
|
1458
|
+
const fileName = path7.basename(file);
|
|
1320
1459
|
if (fileName.includes(".Designer.") || fileName.includes("ModelSnapshot")) {
|
|
1321
1460
|
continue;
|
|
1322
1461
|
}
|
|
@@ -1331,7 +1470,7 @@ async function parseMigrations(migrationsPath, rootPath) {
|
|
|
1331
1470
|
sequence: match[3],
|
|
1332
1471
|
// Sequence number (001, 002, etc.)
|
|
1333
1472
|
description: match[4],
|
|
1334
|
-
file:
|
|
1473
|
+
file: path7.relative(rootPath, file),
|
|
1335
1474
|
applied: true
|
|
1336
1475
|
// We'd need DB connection to check this
|
|
1337
1476
|
});
|
|
@@ -1342,7 +1481,7 @@ async function parseMigrations(migrationsPath, rootPath) {
|
|
|
1342
1481
|
version: "0.0.0",
|
|
1343
1482
|
sequence: "000",
|
|
1344
1483
|
description: fileName.replace(".cs", ""),
|
|
1345
|
-
file:
|
|
1484
|
+
file: path7.relative(rootPath, file),
|
|
1346
1485
|
applied: true
|
|
1347
1486
|
});
|
|
1348
1487
|
}
|
|
@@ -1432,10 +1571,10 @@ function checkChronologicalOrder(result) {
|
|
|
1432
1571
|
}
|
|
1433
1572
|
async function checkBranchConflicts(result, structure, currentBranch, compareBranch, projectPath) {
|
|
1434
1573
|
if (!structure.migrations) return;
|
|
1435
|
-
const migrationsRelPath =
|
|
1574
|
+
const migrationsRelPath = path7.relative(projectPath, structure.migrations).replace(/\\/g, "/");
|
|
1436
1575
|
const snapshotFiles = await findFiles("*ModelSnapshot.cs", { cwd: structure.migrations });
|
|
1437
1576
|
if (snapshotFiles.length > 0) {
|
|
1438
|
-
const snapshotRelPath =
|
|
1577
|
+
const snapshotRelPath = path7.relative(projectPath, snapshotFiles[0]).replace(/\\/g, "/");
|
|
1439
1578
|
const currentSnapshot = await readText(snapshotFiles[0]);
|
|
1440
1579
|
const compareSnapshot = await getFileFromBranch(compareBranch, snapshotRelPath, projectPath);
|
|
1441
1580
|
if (compareSnapshot && currentSnapshot !== compareSnapshot) {
|
|
@@ -1475,7 +1614,7 @@ async function checkModelSnapshot(result, structure) {
|
|
|
1475
1614
|
result.conflicts.push({
|
|
1476
1615
|
type: "snapshot",
|
|
1477
1616
|
description: "Multiple ModelSnapshot files found",
|
|
1478
|
-
files: snapshotFiles.map((f) =>
|
|
1617
|
+
files: snapshotFiles.map((f) => path7.relative(structure.root, f)),
|
|
1479
1618
|
resolution: "Remove duplicate snapshots, keep only one per DbContext"
|
|
1480
1619
|
});
|
|
1481
1620
|
}
|
|
@@ -1562,7 +1701,7 @@ function formatResult2(result, currentBranch, compareBranch) {
|
|
|
1562
1701
|
|
|
1563
1702
|
// src/tools/scaffold-extension.ts
|
|
1564
1703
|
import Handlebars from "handlebars";
|
|
1565
|
-
import
|
|
1704
|
+
import path8 from "path";
|
|
1566
1705
|
var scaffoldExtensionTool = {
|
|
1567
1706
|
name: "scaffold_extension",
|
|
1568
1707
|
description: "Generate code to extend SmartStack: feature (full-stack), entity, service, controller, component, dto, validator, repository, or test",
|
|
@@ -1891,9 +2030,9 @@ services.AddScoped<I{{name}}Service, {{name}}Service>();
|
|
|
1891
2030
|
const diContent = Handlebars.compile(diTemplate)(context);
|
|
1892
2031
|
const projectRoot = config.smartstack.projectPath;
|
|
1893
2032
|
const basePath = structure.application || projectRoot;
|
|
1894
|
-
const servicesPath =
|
|
1895
|
-
const interfacePath =
|
|
1896
|
-
const implementationPath =
|
|
2033
|
+
const servicesPath = path8.join(basePath, "Services");
|
|
2034
|
+
const interfacePath = path8.join(servicesPath, `I${name}Service.cs`);
|
|
2035
|
+
const implementationPath = path8.join(servicesPath, `${name}Service.cs`);
|
|
1897
2036
|
validatePathSecurity(interfacePath, projectRoot);
|
|
1898
2037
|
validatePathSecurity(implementationPath, projectRoot);
|
|
1899
2038
|
if (!dryRun) {
|
|
@@ -2137,15 +2276,15 @@ public class {{name}}Configuration : IEntityTypeConfiguration<{{name}}>
|
|
|
2137
2276
|
const entityContent = Handlebars.compile(entityTemplate)(context);
|
|
2138
2277
|
const configContent = Handlebars.compile(configTemplate)(context);
|
|
2139
2278
|
const projectRoot = config.smartstack.projectPath;
|
|
2140
|
-
const domainPath = structure.domain ||
|
|
2141
|
-
const infraPath = structure.infrastructure ||
|
|
2142
|
-
const entityFilePath =
|
|
2143
|
-
const configFilePath =
|
|
2279
|
+
const domainPath = structure.domain || path8.join(projectRoot, "Domain");
|
|
2280
|
+
const infraPath = structure.infrastructure || path8.join(projectRoot, "Infrastructure");
|
|
2281
|
+
const entityFilePath = path8.join(domainPath, `${name}.cs`);
|
|
2282
|
+
const configFilePath = path8.join(infraPath, "Persistence", "Configurations", `${name}Configuration.cs`);
|
|
2144
2283
|
validatePathSecurity(entityFilePath, projectRoot);
|
|
2145
2284
|
validatePathSecurity(configFilePath, projectRoot);
|
|
2146
2285
|
if (!dryRun) {
|
|
2147
2286
|
await ensureDirectory(domainPath);
|
|
2148
|
-
await ensureDirectory(
|
|
2287
|
+
await ensureDirectory(path8.join(infraPath, "Persistence", "Configurations"));
|
|
2149
2288
|
await writeText(entityFilePath, entityContent);
|
|
2150
2289
|
await writeText(configFilePath, configContent);
|
|
2151
2290
|
}
|
|
@@ -2259,9 +2398,9 @@ public record Update{{name}}Request();
|
|
|
2259
2398
|
};
|
|
2260
2399
|
const controllerContent = Handlebars.compile(controllerTemplate)(context);
|
|
2261
2400
|
const projectRoot = config.smartstack.projectPath;
|
|
2262
|
-
const apiPath = structure.api ||
|
|
2263
|
-
const controllersPath =
|
|
2264
|
-
const controllerFilePath =
|
|
2401
|
+
const apiPath = structure.api || path8.join(projectRoot, "Api");
|
|
2402
|
+
const controllersPath = path8.join(apiPath, "Controllers");
|
|
2403
|
+
const controllerFilePath = path8.join(controllersPath, `${name}Controller.cs`);
|
|
2265
2404
|
validatePathSecurity(controllerFilePath, projectRoot);
|
|
2266
2405
|
if (!dryRun) {
|
|
2267
2406
|
await ensureDirectory(controllersPath);
|
|
@@ -2441,11 +2580,11 @@ export function use{{name}}(options: Use{{name}}Options = {}) {
|
|
|
2441
2580
|
const componentContent = Handlebars.compile(componentTemplate)(context);
|
|
2442
2581
|
const hookContent = Handlebars.compile(hookTemplate)(context);
|
|
2443
2582
|
const projectRoot = config.smartstack.projectPath;
|
|
2444
|
-
const webPath = structure.web ||
|
|
2445
|
-
const componentsPath = options?.outputPath ||
|
|
2446
|
-
const hooksPath =
|
|
2447
|
-
const componentFilePath =
|
|
2448
|
-
const hookFilePath =
|
|
2583
|
+
const webPath = structure.web || path8.join(projectRoot, "web");
|
|
2584
|
+
const componentsPath = options?.outputPath || path8.join(webPath, "src", "components");
|
|
2585
|
+
const hooksPath = path8.join(webPath, "src", "hooks");
|
|
2586
|
+
const componentFilePath = path8.join(componentsPath, `${name}.tsx`);
|
|
2587
|
+
const hookFilePath = path8.join(hooksPath, `use${name}.ts`);
|
|
2449
2588
|
validatePathSecurity(componentFilePath, projectRoot);
|
|
2450
2589
|
validatePathSecurity(hookFilePath, projectRoot);
|
|
2451
2590
|
if (!dryRun) {
|
|
@@ -2570,8 +2709,8 @@ public class {{name}}ServiceTests
|
|
|
2570
2709
|
isSystemEntity
|
|
2571
2710
|
};
|
|
2572
2711
|
const testContent = Handlebars.compile(serviceTestTemplate2)(context);
|
|
2573
|
-
const testsPath = structure.application ?
|
|
2574
|
-
const testFilePath =
|
|
2712
|
+
const testsPath = structure.application ? path8.join(path8.dirname(structure.application), `${path8.basename(structure.application)}.Tests`, "Services") : path8.join(config.smartstack.projectPath, "Application.Tests", "Services");
|
|
2713
|
+
const testFilePath = path8.join(testsPath, `${name}ServiceTests.cs`);
|
|
2575
2714
|
if (!dryRun) {
|
|
2576
2715
|
await ensureDirectory(testsPath);
|
|
2577
2716
|
await writeText(testFilePath, testContent);
|
|
@@ -2703,10 +2842,10 @@ public record Update{{name}}Dto
|
|
|
2703
2842
|
const createContent = Handlebars.compile(createDtoTemplate)(context);
|
|
2704
2843
|
const updateContent = Handlebars.compile(updateDtoTemplate)(context);
|
|
2705
2844
|
const basePath = structure.application || config.smartstack.projectPath;
|
|
2706
|
-
const dtosPath =
|
|
2707
|
-
const responseFilePath =
|
|
2708
|
-
const createFilePath =
|
|
2709
|
-
const updateFilePath =
|
|
2845
|
+
const dtosPath = path8.join(basePath, "DTOs", name);
|
|
2846
|
+
const responseFilePath = path8.join(dtosPath, `${name}ResponseDto.cs`);
|
|
2847
|
+
const createFilePath = path8.join(dtosPath, `Create${name}Dto.cs`);
|
|
2848
|
+
const updateFilePath = path8.join(dtosPath, `Update${name}Dto.cs`);
|
|
2710
2849
|
if (!dryRun) {
|
|
2711
2850
|
await ensureDirectory(dtosPath);
|
|
2712
2851
|
await writeText(responseFilePath, responseContent);
|
|
@@ -2800,9 +2939,9 @@ public class Update{{name}}DtoValidator : AbstractValidator<Update{{name}}Dto>
|
|
|
2800
2939
|
const createValidatorContent = Handlebars.compile(createValidatorTemplate)(context);
|
|
2801
2940
|
const updateValidatorContent = Handlebars.compile(updateValidatorTemplate)(context);
|
|
2802
2941
|
const basePath = structure.application || config.smartstack.projectPath;
|
|
2803
|
-
const validatorsPath =
|
|
2804
|
-
const createValidatorFilePath =
|
|
2805
|
-
const updateValidatorFilePath =
|
|
2942
|
+
const validatorsPath = path8.join(basePath, "Validators");
|
|
2943
|
+
const createValidatorFilePath = path8.join(validatorsPath, `Create${name}DtoValidator.cs`);
|
|
2944
|
+
const updateValidatorFilePath = path8.join(validatorsPath, `Update${name}DtoValidator.cs`);
|
|
2806
2945
|
if (!dryRun) {
|
|
2807
2946
|
await ensureDirectory(validatorsPath);
|
|
2808
2947
|
await writeText(createValidatorFilePath, createValidatorContent);
|
|
@@ -2935,12 +3074,12 @@ public class {{name}}Repository : I{{name}}Repository
|
|
|
2935
3074
|
const interfaceContent = Handlebars.compile(interfaceTemplate)(context);
|
|
2936
3075
|
const implementationContent = Handlebars.compile(implementationTemplate)(context);
|
|
2937
3076
|
const appPath = structure.application || config.smartstack.projectPath;
|
|
2938
|
-
const infraPath = structure.infrastructure ||
|
|
2939
|
-
const interfaceFilePath =
|
|
2940
|
-
const implementationFilePath =
|
|
3077
|
+
const infraPath = structure.infrastructure || path8.join(config.smartstack.projectPath, "Infrastructure");
|
|
3078
|
+
const interfaceFilePath = path8.join(appPath, "Repositories", `I${name}Repository.cs`);
|
|
3079
|
+
const implementationFilePath = path8.join(infraPath, "Repositories", `${name}Repository.cs`);
|
|
2941
3080
|
if (!dryRun) {
|
|
2942
|
-
await ensureDirectory(
|
|
2943
|
-
await ensureDirectory(
|
|
3081
|
+
await ensureDirectory(path8.join(appPath, "Repositories"));
|
|
3082
|
+
await ensureDirectory(path8.join(infraPath, "Repositories"));
|
|
2944
3083
|
await writeText(interfaceFilePath, interfaceContent);
|
|
2945
3084
|
await writeText(implementationFilePath, implementationContent);
|
|
2946
3085
|
}
|
|
@@ -2965,7 +3104,7 @@ function formatResult3(result, type, name, dryRun = false) {
|
|
|
2965
3104
|
lines.push(dryRun ? "## \u{1F4C4} Files to Generate" : "## \u2705 Files Generated");
|
|
2966
3105
|
lines.push("");
|
|
2967
3106
|
for (const file of result.files) {
|
|
2968
|
-
lines.push(`### ${file.type === "created" ? "\u{1F4C4}" : "\u270F\uFE0F"} ${
|
|
3107
|
+
lines.push(`### ${file.type === "created" ? "\u{1F4C4}" : "\u270F\uFE0F"} ${path8.basename(file.path)}`);
|
|
2969
3108
|
lines.push(`**Path**: \`${file.path}\``);
|
|
2970
3109
|
lines.push("");
|
|
2971
3110
|
lines.push("```" + (file.path.endsWith(".cs") ? "csharp" : "typescript"));
|
|
@@ -3010,7 +3149,7 @@ function formatResult3(result, type, name, dryRun = false) {
|
|
|
3010
3149
|
|
|
3011
3150
|
// src/tools/api-docs.ts
|
|
3012
3151
|
import axios from "axios";
|
|
3013
|
-
import
|
|
3152
|
+
import path9 from "path";
|
|
3014
3153
|
var apiDocsTool = {
|
|
3015
3154
|
name: "api_docs",
|
|
3016
3155
|
description: "Get API documentation for SmartStack endpoints. Can fetch from Swagger/OpenAPI or parse controller files directly.",
|
|
@@ -3115,7 +3254,7 @@ async function parseControllers(structure) {
|
|
|
3115
3254
|
const endpoints = [];
|
|
3116
3255
|
for (const file of controllerFiles) {
|
|
3117
3256
|
const content = await readText(file);
|
|
3118
|
-
const fileName =
|
|
3257
|
+
const fileName = path9.basename(file, ".cs");
|
|
3119
3258
|
const controllerName = fileName.replace("Controller", "");
|
|
3120
3259
|
const routeMatch = content.match(/\[Route\s*\(\s*"([^"]+)"\s*\)\]/);
|
|
3121
3260
|
const baseRoute = routeMatch ? routeMatch[1].replace("[controller]", controllerName.toLowerCase()) : `/api/${controllerName.toLowerCase()}`;
|
|
@@ -3375,7 +3514,7 @@ function formatAsOpenApi(endpoints) {
|
|
|
3375
3514
|
|
|
3376
3515
|
// src/tools/suggest-migration.ts
|
|
3377
3516
|
import { z as z2 } from "zod";
|
|
3378
|
-
import
|
|
3517
|
+
import path10 from "path";
|
|
3379
3518
|
var suggestMigrationTool = {
|
|
3380
3519
|
name: "suggest_migration",
|
|
3381
3520
|
description: "Suggest a migration name following SmartStack conventions ({context}_v{version}_{sequence}_{Description})",
|
|
@@ -3463,13 +3602,13 @@ async function handleSuggestMigration(args, config) {
|
|
|
3463
3602
|
}
|
|
3464
3603
|
async function findExistingMigrations(structure, config, context) {
|
|
3465
3604
|
const migrations = [];
|
|
3466
|
-
const infraPath = structure.infrastructure ||
|
|
3467
|
-
const migrationsPath =
|
|
3605
|
+
const infraPath = structure.infrastructure || path10.join(config.smartstack.projectPath, "Infrastructure");
|
|
3606
|
+
const migrationsPath = path10.join(infraPath, "Migrations");
|
|
3468
3607
|
try {
|
|
3469
3608
|
const migrationFiles = await findFiles("*.cs", { cwd: migrationsPath });
|
|
3470
3609
|
const migrationPattern = /^(\w+)_v(\d+\.\d+\.\d+)_(\d+)_(\w+)\.cs$/;
|
|
3471
3610
|
for (const file of migrationFiles) {
|
|
3472
|
-
const fileName =
|
|
3611
|
+
const fileName = path10.basename(file);
|
|
3473
3612
|
if (fileName.includes(".Designer.") || fileName.includes("ModelSnapshot")) {
|
|
3474
3613
|
continue;
|
|
3475
3614
|
}
|
|
@@ -3512,7 +3651,7 @@ function compareVersions2(a, b) {
|
|
|
3512
3651
|
|
|
3513
3652
|
// src/tools/scaffold-tests.ts
|
|
3514
3653
|
import Handlebars2 from "handlebars";
|
|
3515
|
-
import
|
|
3654
|
+
import path11 from "path";
|
|
3516
3655
|
var scaffoldTestsTool = {
|
|
3517
3656
|
name: "scaffold_tests",
|
|
3518
3657
|
description: "Generate unit, integration, and security tests for SmartStack entities, services, controllers, validators, and repositories. Ensures non-regression and maximum security coverage.",
|
|
@@ -4942,14 +5081,14 @@ async function scaffoldEntityTests(name, options, testTypes, structure, config,
|
|
|
4942
5081
|
};
|
|
4943
5082
|
if (testTypes.includes("unit")) {
|
|
4944
5083
|
const content = Handlebars2.compile(entityTestTemplate)(context);
|
|
4945
|
-
const testPath =
|
|
5084
|
+
const testPath = path11.join(structure.root, "Tests", "Unit", "Domain", `${name}Tests.cs`);
|
|
4946
5085
|
validatePathSecurity(testPath, structure.root);
|
|
4947
5086
|
if (!dryRun) {
|
|
4948
|
-
await ensureDirectory(
|
|
5087
|
+
await ensureDirectory(path11.dirname(testPath));
|
|
4949
5088
|
await writeText(testPath, content);
|
|
4950
5089
|
}
|
|
4951
5090
|
result.files.push({
|
|
4952
|
-
path:
|
|
5091
|
+
path: path11.relative(structure.root, testPath),
|
|
4953
5092
|
content,
|
|
4954
5093
|
type: "created"
|
|
4955
5094
|
});
|
|
@@ -4960,14 +5099,14 @@ async function scaffoldEntityTests(name, options, testTypes, structure, config,
|
|
|
4960
5099
|
nameLower: name.charAt(0).toLowerCase() + name.slice(1),
|
|
4961
5100
|
apiNamespace: config.conventions.namespaces.api
|
|
4962
5101
|
});
|
|
4963
|
-
const securityPath =
|
|
5102
|
+
const securityPath = path11.join(structure.root, "Tests", "Security", `${name}SecurityTests.cs`);
|
|
4964
5103
|
validatePathSecurity(securityPath, structure.root);
|
|
4965
5104
|
if (!dryRun) {
|
|
4966
|
-
await ensureDirectory(
|
|
5105
|
+
await ensureDirectory(path11.dirname(securityPath));
|
|
4967
5106
|
await writeText(securityPath, securityContent);
|
|
4968
5107
|
}
|
|
4969
5108
|
result.files.push({
|
|
4970
|
-
path:
|
|
5109
|
+
path: path11.relative(structure.root, securityPath),
|
|
4971
5110
|
content: securityContent,
|
|
4972
5111
|
type: "created"
|
|
4973
5112
|
});
|
|
@@ -4988,14 +5127,14 @@ async function scaffoldServiceTests(name, options, testTypes, structure, config,
|
|
|
4988
5127
|
};
|
|
4989
5128
|
if (testTypes.includes("unit")) {
|
|
4990
5129
|
const content = Handlebars2.compile(serviceTestTemplate)(context);
|
|
4991
|
-
const testPath =
|
|
5130
|
+
const testPath = path11.join(structure.root, "Tests", "Unit", "Services", `${name}ServiceTests.cs`);
|
|
4992
5131
|
validatePathSecurity(testPath, structure.root);
|
|
4993
5132
|
if (!dryRun) {
|
|
4994
|
-
await ensureDirectory(
|
|
5133
|
+
await ensureDirectory(path11.dirname(testPath));
|
|
4995
5134
|
await writeText(testPath, content);
|
|
4996
5135
|
}
|
|
4997
5136
|
result.files.push({
|
|
4998
|
-
path:
|
|
5137
|
+
path: path11.relative(structure.root, testPath),
|
|
4999
5138
|
content,
|
|
5000
5139
|
type: "created"
|
|
5001
5140
|
});
|
|
@@ -5018,28 +5157,28 @@ async function scaffoldControllerTests(name, options, testTypes, structure, conf
|
|
|
5018
5157
|
};
|
|
5019
5158
|
if (testTypes.includes("integration")) {
|
|
5020
5159
|
const content = Handlebars2.compile(controllerTestTemplate)(context);
|
|
5021
|
-
const testPath =
|
|
5160
|
+
const testPath = path11.join(structure.root, "Tests", "Integration", "Controllers", `${name}ControllerTests.cs`);
|
|
5022
5161
|
validatePathSecurity(testPath, structure.root);
|
|
5023
5162
|
if (!dryRun) {
|
|
5024
|
-
await ensureDirectory(
|
|
5163
|
+
await ensureDirectory(path11.dirname(testPath));
|
|
5025
5164
|
await writeText(testPath, content);
|
|
5026
5165
|
}
|
|
5027
5166
|
result.files.push({
|
|
5028
|
-
path:
|
|
5167
|
+
path: path11.relative(structure.root, testPath),
|
|
5029
5168
|
content,
|
|
5030
5169
|
type: "created"
|
|
5031
5170
|
});
|
|
5032
5171
|
}
|
|
5033
5172
|
if (testTypes.includes("security")) {
|
|
5034
5173
|
const securityContent = Handlebars2.compile(securityTestTemplate)(context);
|
|
5035
|
-
const securityPath =
|
|
5174
|
+
const securityPath = path11.join(structure.root, "Tests", "Security", `${name}SecurityTests.cs`);
|
|
5036
5175
|
validatePathSecurity(securityPath, structure.root);
|
|
5037
5176
|
if (!dryRun) {
|
|
5038
|
-
await ensureDirectory(
|
|
5177
|
+
await ensureDirectory(path11.dirname(securityPath));
|
|
5039
5178
|
await writeText(securityPath, securityContent);
|
|
5040
5179
|
}
|
|
5041
5180
|
result.files.push({
|
|
5042
|
-
path:
|
|
5181
|
+
path: path11.relative(structure.root, securityPath),
|
|
5043
5182
|
content: securityContent,
|
|
5044
5183
|
type: "created"
|
|
5045
5184
|
});
|
|
@@ -5057,14 +5196,14 @@ async function scaffoldValidatorTests(name, options, testTypes, structure, confi
|
|
|
5057
5196
|
};
|
|
5058
5197
|
if (testTypes.includes("unit")) {
|
|
5059
5198
|
const content = Handlebars2.compile(validatorTestTemplate)(context);
|
|
5060
|
-
const testPath =
|
|
5199
|
+
const testPath = path11.join(structure.root, "Tests", "Unit", "Validators", `${name}ValidatorTests.cs`);
|
|
5061
5200
|
validatePathSecurity(testPath, structure.root);
|
|
5062
5201
|
if (!dryRun) {
|
|
5063
|
-
await ensureDirectory(
|
|
5202
|
+
await ensureDirectory(path11.dirname(testPath));
|
|
5064
5203
|
await writeText(testPath, content);
|
|
5065
5204
|
}
|
|
5066
5205
|
result.files.push({
|
|
5067
|
-
path:
|
|
5206
|
+
path: path11.relative(structure.root, testPath),
|
|
5068
5207
|
content,
|
|
5069
5208
|
type: "created"
|
|
5070
5209
|
});
|
|
@@ -5084,14 +5223,14 @@ async function scaffoldRepositoryTests(name, options, testTypes, structure, conf
|
|
|
5084
5223
|
};
|
|
5085
5224
|
if (testTypes.includes("integration")) {
|
|
5086
5225
|
const content = Handlebars2.compile(repositoryTestTemplate)(context);
|
|
5087
|
-
const testPath =
|
|
5226
|
+
const testPath = path11.join(structure.root, "Tests", "Integration", "Repositories", `${name}RepositoryTests.cs`);
|
|
5088
5227
|
validatePathSecurity(testPath, structure.root);
|
|
5089
5228
|
if (!dryRun) {
|
|
5090
|
-
await ensureDirectory(
|
|
5229
|
+
await ensureDirectory(path11.dirname(testPath));
|
|
5091
5230
|
await writeText(testPath, content);
|
|
5092
5231
|
}
|
|
5093
5232
|
result.files.push({
|
|
5094
|
-
path:
|
|
5233
|
+
path: path11.relative(structure.root, testPath),
|
|
5095
5234
|
content,
|
|
5096
5235
|
type: "created"
|
|
5097
5236
|
});
|
|
@@ -5141,7 +5280,7 @@ function formatTestResult(result, _target, name, dryRun) {
|
|
|
5141
5280
|
}
|
|
5142
5281
|
|
|
5143
5282
|
// src/tools/analyze-test-coverage.ts
|
|
5144
|
-
import
|
|
5283
|
+
import path12 from "path";
|
|
5145
5284
|
var analyzeTestCoverageTool = {
|
|
5146
5285
|
name: "analyze_test_coverage",
|
|
5147
5286
|
description: "Analyze test coverage for a SmartStack project. Identifies entities, services, and controllers without tests, calculates coverage ratios, and provides recommendations.",
|
|
@@ -5214,7 +5353,7 @@ async function analyzeEntityCoverage(structure, result) {
|
|
|
5214
5353
|
}
|
|
5215
5354
|
const entityFiles = await findFiles("**/*.cs", { cwd: structure.domain });
|
|
5216
5355
|
const entityNames = extractComponentNames(entityFiles, ["Entity", "Aggregate"]);
|
|
5217
|
-
const testPath =
|
|
5356
|
+
const testPath = path12.join(structure.root, "Tests", "Unit", "Domain");
|
|
5218
5357
|
let testFiles = [];
|
|
5219
5358
|
try {
|
|
5220
5359
|
testFiles = await findFiles("**/*Tests.cs", { cwd: testPath });
|
|
@@ -5243,17 +5382,17 @@ async function analyzeServiceCoverage(structure, result) {
|
|
|
5243
5382
|
}
|
|
5244
5383
|
const serviceFiles = await findFiles("**/I*Service.cs", { cwd: structure.application });
|
|
5245
5384
|
const serviceNames = serviceFiles.map((f) => {
|
|
5246
|
-
const basename =
|
|
5385
|
+
const basename = path12.basename(f, ".cs");
|
|
5247
5386
|
return basename.startsWith("I") ? basename.slice(1) : basename;
|
|
5248
5387
|
}).filter((n) => n.endsWith("Service")).map((n) => n.replace(/Service$/, ""));
|
|
5249
|
-
const testPath =
|
|
5388
|
+
const testPath = path12.join(structure.root, "Tests", "Unit", "Services");
|
|
5250
5389
|
let testFiles = [];
|
|
5251
5390
|
try {
|
|
5252
5391
|
testFiles = await findFiles("**/*ServiceTests.cs", { cwd: testPath });
|
|
5253
5392
|
} catch {
|
|
5254
5393
|
}
|
|
5255
5394
|
const testedServices = testFiles.map((f) => {
|
|
5256
|
-
const basename =
|
|
5395
|
+
const basename = path12.basename(f, ".cs");
|
|
5257
5396
|
return basename.replace(/ServiceTests$/, "");
|
|
5258
5397
|
});
|
|
5259
5398
|
result.services.total = serviceNames.length;
|
|
@@ -5278,17 +5417,17 @@ async function analyzeControllerCoverage(structure, result) {
|
|
|
5278
5417
|
}
|
|
5279
5418
|
const controllerFiles = await findFiles("**/*Controller.cs", { cwd: structure.api });
|
|
5280
5419
|
const controllerNames = controllerFiles.map((f) => {
|
|
5281
|
-
const basename =
|
|
5420
|
+
const basename = path12.basename(f, ".cs");
|
|
5282
5421
|
return basename.replace(/Controller$/, "");
|
|
5283
5422
|
}).filter((n) => n !== "Base");
|
|
5284
|
-
const testPath =
|
|
5423
|
+
const testPath = path12.join(structure.root, "Tests", "Integration", "Controllers");
|
|
5285
5424
|
let testFiles = [];
|
|
5286
5425
|
try {
|
|
5287
5426
|
testFiles = await findFiles("**/*ControllerTests.cs", { cwd: testPath });
|
|
5288
5427
|
} catch {
|
|
5289
5428
|
}
|
|
5290
5429
|
const testedControllers = testFiles.map((f) => {
|
|
5291
|
-
const basename =
|
|
5430
|
+
const basename = path12.basename(f, ".cs");
|
|
5292
5431
|
return basename.replace(/ControllerTests$/, "");
|
|
5293
5432
|
});
|
|
5294
5433
|
result.controllers.total = controllerNames.length;
|
|
@@ -5307,7 +5446,7 @@ async function analyzeControllerCoverage(structure, result) {
|
|
|
5307
5446
|
}
|
|
5308
5447
|
}
|
|
5309
5448
|
function extractComponentNames(files, excludeSuffixes = []) {
|
|
5310
|
-
return files.map((f) =>
|
|
5449
|
+
return files.map((f) => path12.basename(f, ".cs")).filter((name) => {
|
|
5311
5450
|
const excludePatterns = [
|
|
5312
5451
|
"Configuration",
|
|
5313
5452
|
"Extensions",
|
|
@@ -5325,7 +5464,7 @@ function extractComponentNames(files, excludeSuffixes = []) {
|
|
|
5325
5464
|
}
|
|
5326
5465
|
function extractTestedNames(testFiles) {
|
|
5327
5466
|
return testFiles.map((f) => {
|
|
5328
|
-
const basename =
|
|
5467
|
+
const basename = path12.basename(f, ".cs");
|
|
5329
5468
|
return basename.replace(/Tests$/, "");
|
|
5330
5469
|
});
|
|
5331
5470
|
}
|
|
@@ -5463,7 +5602,7 @@ function formatCoverageCell(category) {
|
|
|
5463
5602
|
}
|
|
5464
5603
|
|
|
5465
5604
|
// src/tools/validate-test-conventions.ts
|
|
5466
|
-
import
|
|
5605
|
+
import path13 from "path";
|
|
5467
5606
|
var validateTestConventionsTool = {
|
|
5468
5607
|
name: "validate_test_conventions",
|
|
5469
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).",
|
|
@@ -5526,7 +5665,7 @@ async function handleValidateTestConventions(args, config) {
|
|
|
5526
5665
|
suggestions: [],
|
|
5527
5666
|
autoFixedCount: 0
|
|
5528
5667
|
};
|
|
5529
|
-
const testsPath =
|
|
5668
|
+
const testsPath = path13.join(structure.root, "Tests");
|
|
5530
5669
|
try {
|
|
5531
5670
|
let testFiles = [];
|
|
5532
5671
|
try {
|
|
@@ -5543,7 +5682,7 @@ async function handleValidateTestConventions(args, config) {
|
|
|
5543
5682
|
await validateStructure(testsPath, testFiles, result);
|
|
5544
5683
|
}
|
|
5545
5684
|
for (const testFile of testFiles) {
|
|
5546
|
-
const fullPath =
|
|
5685
|
+
const fullPath = path13.join(testsPath, testFile);
|
|
5547
5686
|
const content = await readText(fullPath);
|
|
5548
5687
|
if (checks.includes("naming")) {
|
|
5549
5688
|
validateNaming(testFile, content, result, input.autoFix);
|
|
@@ -5569,7 +5708,7 @@ async function validateStructure(testsPath, testFiles, result) {
|
|
|
5569
5708
|
const expectedDirs = ["Unit", "Integration"];
|
|
5570
5709
|
const foundDirs = /* @__PURE__ */ new Set();
|
|
5571
5710
|
for (const file of testFiles) {
|
|
5572
|
-
const parts = file.split(
|
|
5711
|
+
const parts = file.split(path13.sep);
|
|
5573
5712
|
if (parts.length > 1) {
|
|
5574
5713
|
foundDirs.add(parts[0]);
|
|
5575
5714
|
}
|
|
@@ -5586,8 +5725,8 @@ async function validateStructure(testsPath, testFiles, result) {
|
|
|
5586
5725
|
});
|
|
5587
5726
|
}
|
|
5588
5727
|
}
|
|
5589
|
-
const hasUnitDomain = testFiles.some((f) => f.includes(
|
|
5590
|
-
const hasIntegrationControllers = testFiles.some((f) => f.includes(
|
|
5728
|
+
const hasUnitDomain = testFiles.some((f) => f.includes(path13.join("Unit", "Domain")));
|
|
5729
|
+
const hasIntegrationControllers = testFiles.some((f) => f.includes(path13.join("Integration", "Controllers")));
|
|
5591
5730
|
if (!hasUnitDomain && testFiles.some((f) => f.includes("Unit"))) {
|
|
5592
5731
|
result.suggestions.push("Consider organizing unit tests into subdirectories: Unit/Domain, Unit/Services, Unit/Validators");
|
|
5593
5732
|
}
|
|
@@ -5847,7 +5986,7 @@ function formatValidationResult(result) {
|
|
|
5847
5986
|
}
|
|
5848
5987
|
|
|
5849
5988
|
// src/tools/suggest-test-scenarios.ts
|
|
5850
|
-
import
|
|
5989
|
+
import path14 from "path";
|
|
5851
5990
|
var suggestTestScenariosTool = {
|
|
5852
5991
|
name: "suggest_test_scenarios",
|
|
5853
5992
|
description: "Analyze source code and suggest test scenarios based on detected methods, parameters, and patterns. Generates comprehensive test case recommendations for SmartStack components.",
|
|
@@ -5944,7 +6083,7 @@ async function findSourceFile(target, name, structure, _config) {
|
|
|
5944
6083
|
pattern = `**/${name}Controller.cs`;
|
|
5945
6084
|
break;
|
|
5946
6085
|
case "file":
|
|
5947
|
-
return
|
|
6086
|
+
return path14.isAbsolute(name) ? name : path14.join(structure.root, name);
|
|
5948
6087
|
default:
|
|
5949
6088
|
return null;
|
|
5950
6089
|
}
|
|
@@ -5953,11 +6092,11 @@ async function findSourceFile(target, name, structure, _config) {
|
|
|
5953
6092
|
const altPattern = `**/*${name}*.cs`;
|
|
5954
6093
|
const altFiles = await findFiles(altPattern, { cwd: searchPath });
|
|
5955
6094
|
if (altFiles.length > 0) {
|
|
5956
|
-
return
|
|
6095
|
+
return path14.join(searchPath, altFiles[0]);
|
|
5957
6096
|
}
|
|
5958
6097
|
return null;
|
|
5959
6098
|
}
|
|
5960
|
-
return
|
|
6099
|
+
return path14.join(searchPath, files[0]);
|
|
5961
6100
|
}
|
|
5962
6101
|
function parseSourceCode(content) {
|
|
5963
6102
|
const methods = [];
|
|
@@ -6351,7 +6490,7 @@ function getTypeEmoji(type) {
|
|
|
6351
6490
|
}
|
|
6352
6491
|
|
|
6353
6492
|
// src/tools/scaffold-api-client.ts
|
|
6354
|
-
import
|
|
6493
|
+
import path15 from "path";
|
|
6355
6494
|
var scaffoldApiClientTool = {
|
|
6356
6495
|
name: "scaffold_api_client",
|
|
6357
6496
|
description: `Generate TypeScript API client with NavRoute integration.
|
|
@@ -6370,6 +6509,10 @@ ensuring frontend routes stay synchronized with backend NavRoute attributes.`,
|
|
|
6370
6509
|
inputSchema: {
|
|
6371
6510
|
type: "object",
|
|
6372
6511
|
properties: {
|
|
6512
|
+
path: {
|
|
6513
|
+
type: "string",
|
|
6514
|
+
description: "Path to SmartStack project root (defaults to configured project path)"
|
|
6515
|
+
},
|
|
6373
6516
|
navRoute: {
|
|
6374
6517
|
type: "string",
|
|
6375
6518
|
description: 'NavRoute path (e.g., "platform.administration.users")'
|
|
@@ -6418,14 +6561,14 @@ async function scaffoldApiClient(input, config) {
|
|
|
6418
6561
|
const includeHook = options?.includeHook ?? true;
|
|
6419
6562
|
const nameLower = name.charAt(0).toLowerCase() + name.slice(1);
|
|
6420
6563
|
const apiPath = navRouteToApiPath(navRoute);
|
|
6421
|
-
const projectRoot = config.smartstack.projectPath;
|
|
6564
|
+
const projectRoot = input.path || config.smartstack.projectPath;
|
|
6422
6565
|
const structure = await findSmartStackStructure(projectRoot);
|
|
6423
|
-
const webPath = structure.web ||
|
|
6424
|
-
const servicesPath = options?.outputPath ||
|
|
6425
|
-
const hooksPath =
|
|
6426
|
-
const typesPath =
|
|
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");
|
|
6427
6570
|
const apiClientContent = generateApiClient(name, nameLower, navRoute, apiPath, methods);
|
|
6428
|
-
const apiClientFile =
|
|
6571
|
+
const apiClientFile = path15.join(servicesPath, `${nameLower}.ts`);
|
|
6429
6572
|
if (!dryRun) {
|
|
6430
6573
|
await ensureDirectory(servicesPath);
|
|
6431
6574
|
await writeText(apiClientFile, apiClientContent);
|
|
@@ -6433,7 +6576,7 @@ async function scaffoldApiClient(input, config) {
|
|
|
6433
6576
|
result.files.push({ path: apiClientFile, content: apiClientContent, type: "created" });
|
|
6434
6577
|
if (includeTypes) {
|
|
6435
6578
|
const typesContent = generateTypes(name);
|
|
6436
|
-
const typesFile =
|
|
6579
|
+
const typesFile = path15.join(typesPath, `${nameLower}.ts`);
|
|
6437
6580
|
if (!dryRun) {
|
|
6438
6581
|
await ensureDirectory(typesPath);
|
|
6439
6582
|
await writeText(typesFile, typesContent);
|
|
@@ -6442,7 +6585,7 @@ async function scaffoldApiClient(input, config) {
|
|
|
6442
6585
|
}
|
|
6443
6586
|
if (includeHook) {
|
|
6444
6587
|
const hookContent = generateHook(name, nameLower, methods);
|
|
6445
|
-
const hookFile =
|
|
6588
|
+
const hookFile = path15.join(hooksPath, `use${name}.ts`);
|
|
6446
6589
|
if (!dryRun) {
|
|
6447
6590
|
await ensureDirectory(hooksPath);
|
|
6448
6591
|
await writeText(hookFile, hookContent);
|
|
@@ -6763,8 +6906,8 @@ function formatResult4(result, input) {
|
|
|
6763
6906
|
}
|
|
6764
6907
|
|
|
6765
6908
|
// src/tools/scaffold-routes.ts
|
|
6766
|
-
import
|
|
6767
|
-
import { glob as
|
|
6909
|
+
import path16 from "path";
|
|
6910
|
+
import { glob as glob3 } from "glob";
|
|
6768
6911
|
var scaffoldRoutesTool = {
|
|
6769
6912
|
name: "scaffold_routes",
|
|
6770
6913
|
description: `Generate React Router configuration from backend NavRoute attributes.
|
|
@@ -6782,6 +6925,10 @@ and generates corresponding frontend routing infrastructure.`,
|
|
|
6782
6925
|
inputSchema: {
|
|
6783
6926
|
type: "object",
|
|
6784
6927
|
properties: {
|
|
6928
|
+
path: {
|
|
6929
|
+
type: "string",
|
|
6930
|
+
description: "Path to SmartStack project root (defaults to configured project path)"
|
|
6931
|
+
},
|
|
6785
6932
|
source: {
|
|
6786
6933
|
type: "string",
|
|
6787
6934
|
enum: ["controllers", "navigation", "manual"],
|
|
@@ -6824,10 +6971,10 @@ async function scaffoldRoutes(input, config) {
|
|
|
6824
6971
|
const includeLayouts = options?.includeLayouts ?? true;
|
|
6825
6972
|
const includeGuards = options?.includeGuards ?? true;
|
|
6826
6973
|
const generateRegistry = options?.generateRegistry ?? true;
|
|
6827
|
-
const projectRoot = config.smartstack.projectPath;
|
|
6974
|
+
const projectRoot = input.path || config.smartstack.projectPath;
|
|
6828
6975
|
const structure = await findSmartStackStructure(projectRoot);
|
|
6829
|
-
const webPath = structure.web ||
|
|
6830
|
-
const routesPath = options?.outputPath ||
|
|
6976
|
+
const webPath = structure.web || path16.join(projectRoot, "web");
|
|
6977
|
+
const routesPath = options?.outputPath || path16.join(webPath, "src", "routes");
|
|
6831
6978
|
const navRoutes = await discoverNavRoutes(structure, scope);
|
|
6832
6979
|
if (navRoutes.length === 0) {
|
|
6833
6980
|
result.success = false;
|
|
@@ -6836,7 +6983,7 @@ async function scaffoldRoutes(input, config) {
|
|
|
6836
6983
|
}
|
|
6837
6984
|
if (generateRegistry) {
|
|
6838
6985
|
const registryContent = generateNavRouteRegistry(navRoutes);
|
|
6839
|
-
const registryFile =
|
|
6986
|
+
const registryFile = path16.join(routesPath, "navRoutes.generated.ts");
|
|
6840
6987
|
if (!dryRun) {
|
|
6841
6988
|
await ensureDirectory(routesPath);
|
|
6842
6989
|
await writeText(registryFile, registryContent);
|
|
@@ -6844,18 +6991,18 @@ async function scaffoldRoutes(input, config) {
|
|
|
6844
6991
|
result.files.push({ path: registryFile, content: registryContent, type: "created" });
|
|
6845
6992
|
}
|
|
6846
6993
|
const routerContent = generateRouterConfig(navRoutes, includeGuards);
|
|
6847
|
-
const routerFile =
|
|
6994
|
+
const routerFile = path16.join(routesPath, "index.tsx");
|
|
6848
6995
|
if (!dryRun) {
|
|
6849
6996
|
await ensureDirectory(routesPath);
|
|
6850
6997
|
await writeText(routerFile, routerContent);
|
|
6851
6998
|
}
|
|
6852
6999
|
result.files.push({ path: routerFile, content: routerContent, type: "created" });
|
|
6853
7000
|
if (includeLayouts) {
|
|
6854
|
-
const layoutsPath =
|
|
7001
|
+
const layoutsPath = path16.join(webPath, "src", "layouts");
|
|
6855
7002
|
const contexts = [...new Set(navRoutes.map((r) => r.navRoute.split(".")[0]))];
|
|
6856
7003
|
for (const context of contexts) {
|
|
6857
7004
|
const layoutContent = generateLayout(context);
|
|
6858
|
-
const layoutFile =
|
|
7005
|
+
const layoutFile = path16.join(layoutsPath, `${capitalize(context)}Layout.tsx`);
|
|
6859
7006
|
if (!dryRun) {
|
|
6860
7007
|
await ensureDirectory(layoutsPath);
|
|
6861
7008
|
await writeText(layoutFile, layoutContent);
|
|
@@ -6865,7 +7012,7 @@ async function scaffoldRoutes(input, config) {
|
|
|
6865
7012
|
}
|
|
6866
7013
|
if (includeGuards) {
|
|
6867
7014
|
const guardsContent = generateRouteGuards();
|
|
6868
|
-
const guardsFile =
|
|
7015
|
+
const guardsFile = path16.join(routesPath, "guards.tsx");
|
|
6869
7016
|
if (!dryRun) {
|
|
6870
7017
|
await writeText(guardsFile, guardsContent);
|
|
6871
7018
|
}
|
|
@@ -6883,7 +7030,7 @@ async function discoverNavRoutes(structure, scope) {
|
|
|
6883
7030
|
logger.warn("No API project found");
|
|
6884
7031
|
return routes;
|
|
6885
7032
|
}
|
|
6886
|
-
const controllerFiles = await
|
|
7033
|
+
const controllerFiles = await glob3("**/*Controller.cs", {
|
|
6887
7034
|
cwd: apiPath,
|
|
6888
7035
|
absolute: true,
|
|
6889
7036
|
ignore: ["**/obj/**", "**/bin/**"]
|
|
@@ -6899,7 +7046,7 @@ async function discoverNavRoutes(structure, scope) {
|
|
|
6899
7046
|
if (scope !== "all" && context !== scope) {
|
|
6900
7047
|
continue;
|
|
6901
7048
|
}
|
|
6902
|
-
const controllerMatch =
|
|
7049
|
+
const controllerMatch = path16.basename(file).match(/(.+)Controller\.cs$/);
|
|
6903
7050
|
const controllerName = controllerMatch ? controllerMatch[1] : "Unknown";
|
|
6904
7051
|
const methods = [];
|
|
6905
7052
|
if (content.includes("[HttpGet]")) methods.push("GET");
|
|
@@ -7244,8 +7391,8 @@ function formatResult5(result, input) {
|
|
|
7244
7391
|
}
|
|
7245
7392
|
|
|
7246
7393
|
// src/tools/validate-frontend-routes.ts
|
|
7247
|
-
import
|
|
7248
|
-
import { glob as
|
|
7394
|
+
import path17 from "path";
|
|
7395
|
+
import { glob as glob4 } from "glob";
|
|
7249
7396
|
var validateFrontendRoutesTool = {
|
|
7250
7397
|
name: "validate_frontend_routes",
|
|
7251
7398
|
description: `Validate frontend routes against backend NavRoute attributes.
|
|
@@ -7263,6 +7410,10 @@ Reports issues and provides actionable recommendations for synchronization.`,
|
|
|
7263
7410
|
inputSchema: {
|
|
7264
7411
|
type: "object",
|
|
7265
7412
|
properties: {
|
|
7413
|
+
path: {
|
|
7414
|
+
type: "string",
|
|
7415
|
+
description: "Path to SmartStack project root (defaults to configured project path)"
|
|
7416
|
+
},
|
|
7266
7417
|
scope: {
|
|
7267
7418
|
type: "string",
|
|
7268
7419
|
enum: ["api-clients", "routes", "registry", "all"],
|
|
@@ -7306,9 +7457,9 @@ async function validateFrontendRoutes(input, config) {
|
|
|
7306
7457
|
recommendations: []
|
|
7307
7458
|
};
|
|
7308
7459
|
const { scope } = input;
|
|
7309
|
-
const projectRoot = config.smartstack.projectPath;
|
|
7460
|
+
const projectRoot = input.path || config.smartstack.projectPath;
|
|
7310
7461
|
const structure = await findSmartStackStructure(projectRoot);
|
|
7311
|
-
const webPath = structure.web ||
|
|
7462
|
+
const webPath = structure.web || path17.join(projectRoot, "web");
|
|
7312
7463
|
const backendRoutes = await discoverBackendNavRoutes(structure);
|
|
7313
7464
|
if (scope === "all" || scope === "registry") {
|
|
7314
7465
|
await validateRegistry(webPath, backendRoutes, result);
|
|
@@ -7329,7 +7480,7 @@ async function discoverBackendNavRoutes(structure) {
|
|
|
7329
7480
|
if (!apiPath) {
|
|
7330
7481
|
return routes;
|
|
7331
7482
|
}
|
|
7332
|
-
const controllerFiles = await
|
|
7483
|
+
const controllerFiles = await glob4("**/*Controller.cs", {
|
|
7333
7484
|
cwd: apiPath,
|
|
7334
7485
|
absolute: true,
|
|
7335
7486
|
ignore: ["**/obj/**", "**/bin/**"]
|
|
@@ -7342,7 +7493,7 @@ async function discoverBackendNavRoutes(structure) {
|
|
|
7342
7493
|
const navRoute = navRouteMatch[1];
|
|
7343
7494
|
const suffix = navRouteMatch[2];
|
|
7344
7495
|
const fullNavRoute = suffix ? `${navRoute}.${suffix}` : navRoute;
|
|
7345
|
-
const controllerMatch =
|
|
7496
|
+
const controllerMatch = path17.basename(file).match(/(.+)Controller\.cs$/);
|
|
7346
7497
|
const controllerName = controllerMatch ? controllerMatch[1] : "Unknown";
|
|
7347
7498
|
const methods = [];
|
|
7348
7499
|
if (content.includes("[HttpGet]")) methods.push("GET");
|
|
@@ -7370,7 +7521,7 @@ async function discoverBackendNavRoutes(structure) {
|
|
|
7370
7521
|
return routes;
|
|
7371
7522
|
}
|
|
7372
7523
|
async function validateRegistry(webPath, backendRoutes, result) {
|
|
7373
|
-
const registryPath =
|
|
7524
|
+
const registryPath = path17.join(webPath, "src", "routes", "navRoutes.generated.ts");
|
|
7374
7525
|
if (!await fileExists(registryPath)) {
|
|
7375
7526
|
result.registry.exists = false;
|
|
7376
7527
|
result.recommendations.push("Run `scaffold_routes` to generate navRoutes.generated.ts");
|
|
@@ -7400,8 +7551,8 @@ async function validateRegistry(webPath, backendRoutes, result) {
|
|
|
7400
7551
|
}
|
|
7401
7552
|
}
|
|
7402
7553
|
async function validateApiClients(webPath, backendRoutes, result) {
|
|
7403
|
-
const servicesPath =
|
|
7404
|
-
const clientFiles = await
|
|
7554
|
+
const servicesPath = path17.join(webPath, "src", "services", "api");
|
|
7555
|
+
const clientFiles = await glob4("**/*.ts", {
|
|
7405
7556
|
cwd: servicesPath,
|
|
7406
7557
|
absolute: true,
|
|
7407
7558
|
ignore: ["**/index.ts"]
|
|
@@ -7410,7 +7561,7 @@ async function validateApiClients(webPath, backendRoutes, result) {
|
|
|
7410
7561
|
for (const file of clientFiles) {
|
|
7411
7562
|
try {
|
|
7412
7563
|
const content = await readText(file);
|
|
7413
|
-
const relativePath =
|
|
7564
|
+
const relativePath = path17.relative(webPath, file);
|
|
7414
7565
|
const usesRegistry = content.includes("getRoute('") || content.includes('getRoute("');
|
|
7415
7566
|
if (!usesRegistry) {
|
|
7416
7567
|
const hardcodedMatch = content.match(/apiClient\.(get|post|put|delete)\s*[<(]\s*['"`]([^'"`]+)['"`]/);
|
|
@@ -7448,7 +7599,7 @@ async function validateApiClients(webPath, backendRoutes, result) {
|
|
|
7448
7599
|
}
|
|
7449
7600
|
}
|
|
7450
7601
|
async function validateRoutes(webPath, backendRoutes, result) {
|
|
7451
|
-
const routesPath =
|
|
7602
|
+
const routesPath = path17.join(webPath, "src", "routes", "index.tsx");
|
|
7452
7603
|
if (!await fileExists(routesPath)) {
|
|
7453
7604
|
result.routes.total = 0;
|
|
7454
7605
|
result.routes.missing = backendRoutes.map((r) => r.navRoute);
|
|
@@ -8583,7 +8734,7 @@ Run specific or all checks:
|
|
|
8583
8734
|
}
|
|
8584
8735
|
|
|
8585
8736
|
// src/resources/project-info.ts
|
|
8586
|
-
import
|
|
8737
|
+
import path18 from "path";
|
|
8587
8738
|
var projectInfoResourceTemplate = {
|
|
8588
8739
|
uri: "smartstack://project",
|
|
8589
8740
|
name: "SmartStack Project Info",
|
|
@@ -8620,16 +8771,16 @@ async function getProjectInfoResource(config) {
|
|
|
8620
8771
|
lines.push("```");
|
|
8621
8772
|
lines.push(`${projectInfo.name}/`);
|
|
8622
8773
|
if (structure.domain) {
|
|
8623
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
8774
|
+
lines.push(`\u251C\u2500\u2500 ${path18.basename(structure.domain)}/ # Domain layer (entities)`);
|
|
8624
8775
|
}
|
|
8625
8776
|
if (structure.application) {
|
|
8626
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
8777
|
+
lines.push(`\u251C\u2500\u2500 ${path18.basename(structure.application)}/ # Application layer (services)`);
|
|
8627
8778
|
}
|
|
8628
8779
|
if (structure.infrastructure) {
|
|
8629
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
8780
|
+
lines.push(`\u251C\u2500\u2500 ${path18.basename(structure.infrastructure)}/ # Infrastructure (EF Core)`);
|
|
8630
8781
|
}
|
|
8631
8782
|
if (structure.api) {
|
|
8632
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
8783
|
+
lines.push(`\u251C\u2500\u2500 ${path18.basename(structure.api)}/ # API layer (controllers)`);
|
|
8633
8784
|
}
|
|
8634
8785
|
if (structure.web) {
|
|
8635
8786
|
lines.push(`\u2514\u2500\u2500 web/smartstack-web/ # React frontend`);
|
|
@@ -8642,8 +8793,8 @@ async function getProjectInfoResource(config) {
|
|
|
8642
8793
|
lines.push("| Project | Path |");
|
|
8643
8794
|
lines.push("|---------|------|");
|
|
8644
8795
|
for (const csproj of projectInfo.csprojFiles) {
|
|
8645
|
-
const name =
|
|
8646
|
-
const relativePath =
|
|
8796
|
+
const name = path18.basename(csproj, ".csproj");
|
|
8797
|
+
const relativePath = path18.relative(projectPath, csproj);
|
|
8647
8798
|
lines.push(`| ${name} | \`${relativePath}\` |`);
|
|
8648
8799
|
}
|
|
8649
8800
|
lines.push("");
|
|
@@ -8653,10 +8804,10 @@ async function getProjectInfoResource(config) {
|
|
|
8653
8804
|
cwd: structure.migrations,
|
|
8654
8805
|
ignore: ["*.Designer.cs"]
|
|
8655
8806
|
});
|
|
8656
|
-
const migrations = migrationFiles.map((f) =>
|
|
8807
|
+
const migrations = migrationFiles.map((f) => path18.basename(f)).filter((f) => !f.includes("ModelSnapshot") && !f.includes(".Designer.")).sort();
|
|
8657
8808
|
lines.push("## EF Core Migrations");
|
|
8658
8809
|
lines.push("");
|
|
8659
|
-
lines.push(`**Location**: \`${
|
|
8810
|
+
lines.push(`**Location**: \`${path18.relative(projectPath, structure.migrations)}\``);
|
|
8660
8811
|
lines.push(`**Total Migrations**: ${migrations.length}`);
|
|
8661
8812
|
lines.push("");
|
|
8662
8813
|
if (migrations.length > 0) {
|
|
@@ -8691,11 +8842,11 @@ async function getProjectInfoResource(config) {
|
|
|
8691
8842
|
lines.push("dotnet build");
|
|
8692
8843
|
lines.push("");
|
|
8693
8844
|
lines.push("# Run API");
|
|
8694
|
-
lines.push(`cd ${structure.api ?
|
|
8845
|
+
lines.push(`cd ${structure.api ? path18.relative(projectPath, structure.api) : "src/Api"}`);
|
|
8695
8846
|
lines.push("dotnet run");
|
|
8696
8847
|
lines.push("");
|
|
8697
8848
|
lines.push("# Run frontend");
|
|
8698
|
-
lines.push(`cd ${structure.web ?
|
|
8849
|
+
lines.push(`cd ${structure.web ? path18.relative(projectPath, structure.web) : "web"}`);
|
|
8699
8850
|
lines.push("npm run dev");
|
|
8700
8851
|
lines.push("");
|
|
8701
8852
|
lines.push("# Create migration");
|
|
@@ -8718,7 +8869,7 @@ async function getProjectInfoResource(config) {
|
|
|
8718
8869
|
}
|
|
8719
8870
|
|
|
8720
8871
|
// src/resources/api-endpoints.ts
|
|
8721
|
-
import
|
|
8872
|
+
import path19 from "path";
|
|
8722
8873
|
var apiEndpointsResourceTemplate = {
|
|
8723
8874
|
uri: "smartstack://api/",
|
|
8724
8875
|
name: "SmartStack API Endpoints",
|
|
@@ -8743,7 +8894,7 @@ async function getApiEndpointsResource(config, endpointFilter) {
|
|
|
8743
8894
|
}
|
|
8744
8895
|
async function parseController(filePath, _rootPath) {
|
|
8745
8896
|
const content = await readText(filePath);
|
|
8746
|
-
const fileName =
|
|
8897
|
+
const fileName = path19.basename(filePath, ".cs");
|
|
8747
8898
|
const controllerName = fileName.replace("Controller", "");
|
|
8748
8899
|
const endpoints = [];
|
|
8749
8900
|
const routeMatch = content.match(/\[Route\s*\(\s*"([^"]+)"\s*\)\]/);
|
|
@@ -8890,7 +9041,7 @@ function getMethodEmoji(method) {
|
|
|
8890
9041
|
}
|
|
8891
9042
|
|
|
8892
9043
|
// src/resources/db-schema.ts
|
|
8893
|
-
import
|
|
9044
|
+
import path20 from "path";
|
|
8894
9045
|
var dbSchemaResourceTemplate = {
|
|
8895
9046
|
uri: "smartstack://schema/",
|
|
8896
9047
|
name: "SmartStack Database Schema",
|
|
@@ -8980,7 +9131,7 @@ async function parseEntity(filePath, rootPath, _config) {
|
|
|
8980
9131
|
tableName,
|
|
8981
9132
|
properties,
|
|
8982
9133
|
relationships,
|
|
8983
|
-
file:
|
|
9134
|
+
file: path20.relative(rootPath, filePath)
|
|
8984
9135
|
};
|
|
8985
9136
|
}
|
|
8986
9137
|
async function enrichFromConfigurations(entities, infrastructurePath, _config) {
|
|
@@ -9126,7 +9277,7 @@ function formatSchema(entities, filter, _config) {
|
|
|
9126
9277
|
}
|
|
9127
9278
|
|
|
9128
9279
|
// src/resources/entities.ts
|
|
9129
|
-
import
|
|
9280
|
+
import path21 from "path";
|
|
9130
9281
|
var entitiesResourceTemplate = {
|
|
9131
9282
|
uri: "smartstack://entities/",
|
|
9132
9283
|
name: "SmartStack Entities",
|
|
@@ -9186,7 +9337,7 @@ async function parseEntitySummary(filePath, rootPath, config) {
|
|
|
9186
9337
|
hasSoftDelete,
|
|
9187
9338
|
hasRowVersion,
|
|
9188
9339
|
file: filePath,
|
|
9189
|
-
relativePath:
|
|
9340
|
+
relativePath: path21.relative(rootPath, filePath)
|
|
9190
9341
|
};
|
|
9191
9342
|
}
|
|
9192
9343
|
function inferTableInfo(entityName, config) {
|