@atlashub/smartstack-mcp 1.13.0 → 1.15.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 +1185 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -82,10 +82,10 @@ import { stat, mkdir, readFile, writeFile, cp, rm } from "fs/promises";
|
|
|
82
82
|
import path from "path";
|
|
83
83
|
import { glob } from "glob";
|
|
84
84
|
var FileSystemError = class extends Error {
|
|
85
|
-
constructor(message, operation,
|
|
85
|
+
constructor(message, operation, path26, cause) {
|
|
86
86
|
super(message);
|
|
87
87
|
this.operation = operation;
|
|
88
|
-
this.path =
|
|
88
|
+
this.path = path26;
|
|
89
89
|
this.cause = cause;
|
|
90
90
|
this.name = "FileSystemError";
|
|
91
91
|
}
|
|
@@ -558,7 +558,7 @@ var ConfigSchema = z.object({
|
|
|
558
558
|
});
|
|
559
559
|
var ValidateConventionsInputSchema = z.object({
|
|
560
560
|
path: z.string().optional().describe("Project path to validate (default: SmartStack.app path)"),
|
|
561
|
-
checks: z.array(z.enum(["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "all"])).default(["all"]).describe("Types of checks to perform")
|
|
561
|
+
checks: z.array(z.enum(["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs", "all"])).default(["all"]).describe("Types of checks to perform")
|
|
562
562
|
});
|
|
563
563
|
var CheckMigrationsInputSchema = z.object({
|
|
564
564
|
projectPath: z.string().optional().describe("EF Core project path"),
|
|
@@ -648,6 +648,39 @@ var SuggestTestScenariosInputSchema = z.object({
|
|
|
648
648
|
name: z.string().min(1).describe("Component name or file path"),
|
|
649
649
|
depth: z.enum(["basic", "comprehensive", "security-focused"]).default("comprehensive").describe("Depth of analysis")
|
|
650
650
|
});
|
|
651
|
+
var SecurityCheckSchema = z.enum([
|
|
652
|
+
"hardcoded-secrets",
|
|
653
|
+
"sql-injection",
|
|
654
|
+
"tenant-isolation",
|
|
655
|
+
"authorization",
|
|
656
|
+
"dangerous-functions",
|
|
657
|
+
"input-validation",
|
|
658
|
+
"xss",
|
|
659
|
+
"csrf",
|
|
660
|
+
"logging-sensitive",
|
|
661
|
+
"all"
|
|
662
|
+
]);
|
|
663
|
+
var ValidateSecurityInputSchema = z.object({
|
|
664
|
+
path: z.string().optional().describe("Project path to validate (default: SmartStack.app path)"),
|
|
665
|
+
checks: z.array(SecurityCheckSchema).default(["all"]).describe("Security checks to run"),
|
|
666
|
+
severity: z.enum(["blocking", "all"]).optional().describe("Filter results by severity")
|
|
667
|
+
});
|
|
668
|
+
var QualityMetricSchema = z.enum([
|
|
669
|
+
"cognitive-complexity",
|
|
670
|
+
"cyclomatic-complexity",
|
|
671
|
+
"function-size",
|
|
672
|
+
"nesting-depth",
|
|
673
|
+
"parameter-count",
|
|
674
|
+
"code-duplication",
|
|
675
|
+
"file-size",
|
|
676
|
+
"all"
|
|
677
|
+
]);
|
|
678
|
+
var AnalyzeCodeQualityInputSchema = z.object({
|
|
679
|
+
path: z.string().optional().describe("Project path to analyze (default: SmartStack.app path)"),
|
|
680
|
+
metrics: z.array(QualityMetricSchema).default(["all"]).describe("Metrics to analyze"),
|
|
681
|
+
threshold: z.enum(["strict", "normal", "lenient"]).default("normal").describe("Threshold level for violations"),
|
|
682
|
+
scope: z.enum(["changed", "all"]).default("all").describe("Analyze only changed files or all")
|
|
683
|
+
});
|
|
651
684
|
var ScaffoldApiClientInputSchema = z.object({
|
|
652
685
|
path: z.string().optional().describe("Path to SmartStack project root (defaults to configured project path)"),
|
|
653
686
|
navRoute: z.string().min(1).describe('NavRoute path (e.g., "platform.administration.users")'),
|
|
@@ -914,7 +947,7 @@ var validateConventionsTool = {
|
|
|
914
947
|
type: "array",
|
|
915
948
|
items: {
|
|
916
949
|
type: "string",
|
|
917
|
-
enum: ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "all"]
|
|
950
|
+
enum: ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs", "all"]
|
|
918
951
|
},
|
|
919
952
|
description: "Types of checks to perform",
|
|
920
953
|
default: ["all"]
|
|
@@ -925,7 +958,7 @@ var validateConventionsTool = {
|
|
|
925
958
|
async function handleValidateConventions(args, config) {
|
|
926
959
|
const input = ValidateConventionsInputSchema.parse(args);
|
|
927
960
|
const projectPath = input.path || config.smartstack.projectPath;
|
|
928
|
-
const checks = input.checks.includes("all") ? ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers"] : input.checks;
|
|
961
|
+
const checks = input.checks.includes("all") ? ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs"] : input.checks;
|
|
929
962
|
logger.info("Validating conventions", { projectPath, checks });
|
|
930
963
|
const result = {
|
|
931
964
|
valid: true,
|
|
@@ -955,6 +988,12 @@ async function handleValidateConventions(args, config) {
|
|
|
955
988
|
if (checks.includes("controllers")) {
|
|
956
989
|
await validateControllerRoutes(structure, config, result);
|
|
957
990
|
}
|
|
991
|
+
if (checks.includes("layouts")) {
|
|
992
|
+
await validateLayouts(structure, config, result);
|
|
993
|
+
}
|
|
994
|
+
if (checks.includes("tabs")) {
|
|
995
|
+
await validateTabs(structure, config, result);
|
|
996
|
+
}
|
|
958
997
|
result.valid = result.errors.length === 0;
|
|
959
998
|
result.summary = generateSummary(result, checks);
|
|
960
999
|
return formatResult(result);
|
|
@@ -1385,6 +1424,132 @@ async function validateControllerRoutes(structure, _config, result) {
|
|
|
1385
1424
|
});
|
|
1386
1425
|
}
|
|
1387
1426
|
}
|
|
1427
|
+
async function validateLayouts(structure, _config, result) {
|
|
1428
|
+
if (!structure.web) {
|
|
1429
|
+
result.warnings.push({
|
|
1430
|
+
type: "warning",
|
|
1431
|
+
category: "layouts",
|
|
1432
|
+
message: "Web project not found, skipping layout validation"
|
|
1433
|
+
});
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
const layoutFiles = await findFiles("**/layouts/**/*.tsx", { cwd: structure.web });
|
|
1437
|
+
if (layoutFiles.length === 0) {
|
|
1438
|
+
result.warnings.push({
|
|
1439
|
+
type: "warning",
|
|
1440
|
+
category: "layouts",
|
|
1441
|
+
message: "No layout files found in web/src/layouts/"
|
|
1442
|
+
});
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
for (const file of layoutFiles) {
|
|
1446
|
+
const content = await readText(file);
|
|
1447
|
+
const fileName = path6.basename(file);
|
|
1448
|
+
const lines = content.split("\n");
|
|
1449
|
+
let hasMaxWidth = false;
|
|
1450
|
+
for (const line of lines) {
|
|
1451
|
+
if (line.trim().startsWith("//") || line.trim().startsWith("*")) continue;
|
|
1452
|
+
if (/className.*max-w-/.test(line)) {
|
|
1453
|
+
hasMaxWidth = true;
|
|
1454
|
+
break;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
if (hasMaxWidth) {
|
|
1458
|
+
result.errors.push({
|
|
1459
|
+
type: "error",
|
|
1460
|
+
category: "layouts",
|
|
1461
|
+
message: `Layout "${fileName}" uses max-w-* constraint which limits content width`,
|
|
1462
|
+
file: path6.relative(structure.root, file),
|
|
1463
|
+
suggestion: "Remove max-w-* class. Content should occupy full available width."
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
let hasStandardPadding = false;
|
|
1467
|
+
for (const line of lines) {
|
|
1468
|
+
if (line.trim().startsWith("//") || line.trim().startsWith("*")) continue;
|
|
1469
|
+
if (/className.*lg:px-10/.test(line)) {
|
|
1470
|
+
hasStandardPadding = true;
|
|
1471
|
+
break;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
if (!hasStandardPadding) {
|
|
1475
|
+
result.warnings.push({
|
|
1476
|
+
type: "warning",
|
|
1477
|
+
category: "layouts",
|
|
1478
|
+
message: `Layout "${fileName}" may be missing standard horizontal padding`,
|
|
1479
|
+
file: path6.relative(structure.root, file),
|
|
1480
|
+
suggestion: "Use lg:px-10 for consistent horizontal padding across layouts"
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
let hasScrollPattern = false;
|
|
1484
|
+
for (const line of lines) {
|
|
1485
|
+
if (line.trim().startsWith("//") || line.trim().startsWith("*")) continue;
|
|
1486
|
+
if (/className.*h-full/.test(line) && /className.*overflow-auto/.test(line)) {
|
|
1487
|
+
hasScrollPattern = true;
|
|
1488
|
+
break;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
if (!hasScrollPattern) {
|
|
1492
|
+
result.warnings.push({
|
|
1493
|
+
type: "warning",
|
|
1494
|
+
category: "layouts",
|
|
1495
|
+
message: `Layout "${fileName}" may be missing scroll container pattern`,
|
|
1496
|
+
file: path6.relative(structure.root, file),
|
|
1497
|
+
suggestion: "Use h-full overflow-auto for proper internal scrolling"
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
async function validateTabs(structure, _config, result) {
|
|
1503
|
+
if (!structure.web) {
|
|
1504
|
+
result.warnings.push({
|
|
1505
|
+
type: "warning",
|
|
1506
|
+
category: "tabs",
|
|
1507
|
+
message: "Web project not found, skipping tab validation"
|
|
1508
|
+
});
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
const pageFiles = await findFiles("**/pages/**/*.tsx", { cwd: structure.web });
|
|
1512
|
+
let tabPagesCount = 0;
|
|
1513
|
+
let hookUsageCount = 0;
|
|
1514
|
+
for (const file of pageFiles) {
|
|
1515
|
+
const content = await readText(file);
|
|
1516
|
+
const fileName = path6.basename(file);
|
|
1517
|
+
const lines = content.split("\n");
|
|
1518
|
+
let hasTabPattern = false;
|
|
1519
|
+
for (const line of lines) {
|
|
1520
|
+
const trimmed = line.trim();
|
|
1521
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*") || trimmed === "") {
|
|
1522
|
+
continue;
|
|
1523
|
+
}
|
|
1524
|
+
if (/<Tabs|<TabList|activeTab\s*[,=})]|setActiveTab\s*[({]/.test(line)) {
|
|
1525
|
+
hasTabPattern = true;
|
|
1526
|
+
break;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
if (hasTabPattern) {
|
|
1530
|
+
tabPagesCount++;
|
|
1531
|
+
const usesHook = content.includes("useTabNavigation");
|
|
1532
|
+
if (usesHook) {
|
|
1533
|
+
hookUsageCount++;
|
|
1534
|
+
} else {
|
|
1535
|
+
result.errors.push({
|
|
1536
|
+
type: "error",
|
|
1537
|
+
category: "tabs",
|
|
1538
|
+
message: `Page "${fileName}" has tabs but doesn't use useTabNavigation hook`,
|
|
1539
|
+
file: path6.relative(structure.root, file),
|
|
1540
|
+
suggestion: "Use useTabNavigation hook to sync tab state with URL: const { activeTab, setActiveTab } = useTabNavigation(defaultTab, VALID_TABS)"
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
if (tabPagesCount > 0) {
|
|
1546
|
+
result.warnings.push({
|
|
1547
|
+
type: "warning",
|
|
1548
|
+
category: "tabs",
|
|
1549
|
+
message: `Tab summary: ${hookUsageCount}/${tabPagesCount} pages with tabs use useTabNavigation hook`
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1388
1553
|
function generateSummary(result, checks) {
|
|
1389
1554
|
const parts = [];
|
|
1390
1555
|
parts.push(`Checks performed: ${checks.join(", ")}`);
|
|
@@ -9603,6 +9768,919 @@ function generateRecommendations3(analyzed) {
|
|
|
9603
9768
|
return recommendations;
|
|
9604
9769
|
}
|
|
9605
9770
|
|
|
9771
|
+
// src/tools/validate-security.ts
|
|
9772
|
+
import path20 from "path";
|
|
9773
|
+
var validateSecurityTool = {
|
|
9774
|
+
name: "validate_security",
|
|
9775
|
+
description: "Validate SmartStack security patterns: multi-tenant isolation, authorization, input validation, secrets exposure, injection vulnerabilities, and OWASP Top 10 compliance.",
|
|
9776
|
+
inputSchema: {
|
|
9777
|
+
type: "object",
|
|
9778
|
+
properties: {
|
|
9779
|
+
path: {
|
|
9780
|
+
type: "string",
|
|
9781
|
+
description: "Project path to validate (default: SmartStack.app path from config)"
|
|
9782
|
+
},
|
|
9783
|
+
checks: {
|
|
9784
|
+
type: "array",
|
|
9785
|
+
items: {
|
|
9786
|
+
type: "string",
|
|
9787
|
+
enum: [
|
|
9788
|
+
"hardcoded-secrets",
|
|
9789
|
+
"sql-injection",
|
|
9790
|
+
"tenant-isolation",
|
|
9791
|
+
"authorization",
|
|
9792
|
+
"dangerous-functions",
|
|
9793
|
+
"input-validation",
|
|
9794
|
+
"xss",
|
|
9795
|
+
"csrf",
|
|
9796
|
+
"logging-sensitive",
|
|
9797
|
+
"all"
|
|
9798
|
+
]
|
|
9799
|
+
},
|
|
9800
|
+
description: "Security checks to run",
|
|
9801
|
+
default: ["all"]
|
|
9802
|
+
},
|
|
9803
|
+
severity: {
|
|
9804
|
+
type: "string",
|
|
9805
|
+
enum: ["blocking", "all"],
|
|
9806
|
+
description: "Filter results by severity"
|
|
9807
|
+
}
|
|
9808
|
+
}
|
|
9809
|
+
}
|
|
9810
|
+
};
|
|
9811
|
+
async function handleValidateSecurity(args, config) {
|
|
9812
|
+
const input = ValidateSecurityInputSchema.parse(args);
|
|
9813
|
+
const projectPath = input.path || config.smartstack.projectPath;
|
|
9814
|
+
const checksToRun = input.checks.includes("all") ? [
|
|
9815
|
+
"hardcoded-secrets",
|
|
9816
|
+
"sql-injection",
|
|
9817
|
+
"tenant-isolation",
|
|
9818
|
+
"authorization",
|
|
9819
|
+
"dangerous-functions",
|
|
9820
|
+
"input-validation"
|
|
9821
|
+
] : input.checks;
|
|
9822
|
+
logger.info("Validating security", { projectPath, checks: checksToRun });
|
|
9823
|
+
const result = {
|
|
9824
|
+
valid: true,
|
|
9825
|
+
summary: "",
|
|
9826
|
+
findings: [],
|
|
9827
|
+
stats: {
|
|
9828
|
+
blocking: 0,
|
|
9829
|
+
critical: 0,
|
|
9830
|
+
warning: 0,
|
|
9831
|
+
filesScanned: 0
|
|
9832
|
+
}
|
|
9833
|
+
};
|
|
9834
|
+
const structure = await findSmartStackStructure(projectPath);
|
|
9835
|
+
if (checksToRun.includes("hardcoded-secrets")) {
|
|
9836
|
+
await checkHardcodedSecrets(structure, result);
|
|
9837
|
+
}
|
|
9838
|
+
if (checksToRun.includes("sql-injection")) {
|
|
9839
|
+
await checkSqlInjection(structure, result);
|
|
9840
|
+
}
|
|
9841
|
+
if (checksToRun.includes("tenant-isolation")) {
|
|
9842
|
+
await checkTenantIsolation(structure, result);
|
|
9843
|
+
}
|
|
9844
|
+
if (checksToRun.includes("authorization")) {
|
|
9845
|
+
await checkAuthorization(structure, result);
|
|
9846
|
+
}
|
|
9847
|
+
if (checksToRun.includes("dangerous-functions")) {
|
|
9848
|
+
await checkDangerousFunctions(structure, result);
|
|
9849
|
+
}
|
|
9850
|
+
if (checksToRun.includes("input-validation")) {
|
|
9851
|
+
await checkInputValidation(structure, result);
|
|
9852
|
+
}
|
|
9853
|
+
result.stats.blocking = result.findings.filter((f) => f.severity === "blocking").length;
|
|
9854
|
+
result.stats.critical = result.findings.filter((f) => f.severity === "critical").length;
|
|
9855
|
+
result.stats.warning = result.findings.filter((f) => f.severity === "warning").length;
|
|
9856
|
+
if (input.severity === "blocking") {
|
|
9857
|
+
result.findings = result.findings.filter((f) => f.severity === "blocking");
|
|
9858
|
+
}
|
|
9859
|
+
result.valid = result.stats.blocking === 0;
|
|
9860
|
+
result.summary = result.valid ? `Security validation passed. ${result.stats.warning} warning(s) found.` : `Security validation FAILED. ${result.stats.blocking} blocking issue(s) found.`;
|
|
9861
|
+
return formatSecurityReport(result);
|
|
9862
|
+
}
|
|
9863
|
+
async function checkHardcodedSecrets(structure, result) {
|
|
9864
|
+
const secretPatterns = [
|
|
9865
|
+
// Credentials
|
|
9866
|
+
{ pattern: /(?:password|passwd|pwd)\s*[=:]\s*["'][^"']{4,}["']/gi, name: "password" },
|
|
9867
|
+
{ pattern: /(?:api[_-]?key|apikey)\s*[=:]\s*["'][^"']{8,}["']/gi, name: "API key" },
|
|
9868
|
+
{ pattern: /(?:secret|token)\s*[=:]\s*["'][^"']{8,}["']/gi, name: "secret/token" },
|
|
9869
|
+
// Connection strings with embedded passwords
|
|
9870
|
+
{ pattern: /Server=.+;.*Password=[^;]+/gi, name: "connection string with password" },
|
|
9871
|
+
{ pattern: /Data Source=.+;.*Password=[^;]+/gi, name: "connection string with password" },
|
|
9872
|
+
// Private keys
|
|
9873
|
+
{ pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/g, name: "private key" }
|
|
9874
|
+
];
|
|
9875
|
+
const filesToScan = await getFilesToScan(structure, ["**/*.cs", "**/*.ts", "**/*.tsx", "**/*.json"]);
|
|
9876
|
+
result.stats.filesScanned += filesToScan.length;
|
|
9877
|
+
for (const file of filesToScan) {
|
|
9878
|
+
if (isExcludedFile(file)) continue;
|
|
9879
|
+
const content = await readText(file);
|
|
9880
|
+
const lines = content.split("\n");
|
|
9881
|
+
for (const { pattern, name } of secretPatterns) {
|
|
9882
|
+
pattern.lastIndex = 0;
|
|
9883
|
+
let match;
|
|
9884
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
9885
|
+
const lineNumber = getLineNumber(content, match.index);
|
|
9886
|
+
const lineContent = lines[lineNumber - 1]?.trim() || "";
|
|
9887
|
+
if (isPlaceholderValue(lineContent)) continue;
|
|
9888
|
+
result.findings.push({
|
|
9889
|
+
severity: "blocking",
|
|
9890
|
+
category: "hardcoded-secrets",
|
|
9891
|
+
message: `Hardcoded ${name} detected`,
|
|
9892
|
+
file: path20.relative(structure.root, file),
|
|
9893
|
+
line: lineNumber,
|
|
9894
|
+
code: truncateCode(lineContent),
|
|
9895
|
+
suggestion: `Move ${name} to configuration or environment variables`,
|
|
9896
|
+
cweId: "CWE-798"
|
|
9897
|
+
});
|
|
9898
|
+
}
|
|
9899
|
+
}
|
|
9900
|
+
}
|
|
9901
|
+
}
|
|
9902
|
+
async function checkSqlInjection(structure, result) {
|
|
9903
|
+
const sqlInjectionPatterns = [
|
|
9904
|
+
// Raw SQL with string concatenation (the most dangerous pattern)
|
|
9905
|
+
{ pattern: /\.(?:FromSqlRaw|ExecuteSqlRaw)\s*\([^)]*\s*\+\s*/g, name: "concatenated SQL in FromSqlRaw/ExecuteSqlRaw" },
|
|
9906
|
+
// ADO.NET without parameters
|
|
9907
|
+
{ pattern: /new SqlCommand\s*\([^)]*\+/g, name: "concatenated SQL in SqlCommand" },
|
|
9908
|
+
{ pattern: /CommandText\s*=\s*[^;]*\+/g, name: "concatenated SQL in CommandText" }
|
|
9909
|
+
// Note: FromSqlInterpolated with $"" is safe and not flagged
|
|
9910
|
+
];
|
|
9911
|
+
const csFiles = await getFilesToScan(structure, ["**/*.cs"]);
|
|
9912
|
+
result.stats.filesScanned += csFiles.length;
|
|
9913
|
+
for (const file of csFiles) {
|
|
9914
|
+
if (isExcludedFile(file)) continue;
|
|
9915
|
+
const content = await readText(file);
|
|
9916
|
+
const lines = content.split("\n");
|
|
9917
|
+
for (const { pattern, name } of sqlInjectionPatterns) {
|
|
9918
|
+
pattern.lastIndex = 0;
|
|
9919
|
+
let match;
|
|
9920
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
9921
|
+
const lineNumber = getLineNumber(content, match.index);
|
|
9922
|
+
const lineContent = lines[lineNumber - 1]?.trim() || "";
|
|
9923
|
+
result.findings.push({
|
|
9924
|
+
severity: "blocking",
|
|
9925
|
+
category: "sql-injection",
|
|
9926
|
+
message: `Potential SQL injection: ${name}`,
|
|
9927
|
+
file: path20.relative(structure.root, file),
|
|
9928
|
+
line: lineNumber,
|
|
9929
|
+
code: truncateCode(lineContent),
|
|
9930
|
+
suggestion: 'Use parameterized queries: FromSqlRaw("SELECT * FROM x WHERE id = {0}", id) or FromSqlInterpolated',
|
|
9931
|
+
cweId: "CWE-89"
|
|
9932
|
+
});
|
|
9933
|
+
}
|
|
9934
|
+
}
|
|
9935
|
+
}
|
|
9936
|
+
}
|
|
9937
|
+
async function checkTenantIsolation(structure, result) {
|
|
9938
|
+
if (!structure.domain) {
|
|
9939
|
+
result.findings.push({
|
|
9940
|
+
severity: "warning",
|
|
9941
|
+
category: "tenant-isolation",
|
|
9942
|
+
message: "Domain project not found, skipping tenant isolation validation",
|
|
9943
|
+
file: "",
|
|
9944
|
+
suggestion: "Ensure project structure follows SmartStack conventions"
|
|
9945
|
+
});
|
|
9946
|
+
return;
|
|
9947
|
+
}
|
|
9948
|
+
const entityFiles = await findFiles("**/Entities/**/*.cs", { cwd: structure.domain });
|
|
9949
|
+
const tenantEntities = [];
|
|
9950
|
+
for (const file of entityFiles) {
|
|
9951
|
+
const content = await readText(file);
|
|
9952
|
+
if (content.includes("ITenantEntity") || content.includes(": TenantEntity")) {
|
|
9953
|
+
const entityMatch = content.match(/class\s+(\w+)/);
|
|
9954
|
+
if (entityMatch) {
|
|
9955
|
+
tenantEntities.push(entityMatch[1]);
|
|
9956
|
+
}
|
|
9957
|
+
}
|
|
9958
|
+
const createMethodMatches = content.matchAll(/public\s+static\s+\w+\s+Create\s*\(([^)]*)\)/g);
|
|
9959
|
+
for (const match of createMethodMatches) {
|
|
9960
|
+
const params = match[1];
|
|
9961
|
+
if ((content.includes("ITenantEntity") || content.includes(": TenantEntity")) && !params.includes("tenantId") && !params.includes("TenantId")) {
|
|
9962
|
+
const lineNumber = getLineNumber(content, match.index);
|
|
9963
|
+
result.findings.push({
|
|
9964
|
+
severity: "blocking",
|
|
9965
|
+
category: "tenant-isolation",
|
|
9966
|
+
message: "Create method missing tenantId parameter for tenant entity",
|
|
9967
|
+
file: path20.relative(structure.root, file),
|
|
9968
|
+
line: lineNumber,
|
|
9969
|
+
code: truncateCode(match[0]),
|
|
9970
|
+
suggestion: "Add Guid tenantId as first parameter: Create(Guid tenantId, ...)",
|
|
9971
|
+
cweId: "CWE-639"
|
|
9972
|
+
});
|
|
9973
|
+
}
|
|
9974
|
+
}
|
|
9975
|
+
}
|
|
9976
|
+
if (structure.application) {
|
|
9977
|
+
const serviceFiles = await findFiles("**/*Service.cs", { cwd: structure.application });
|
|
9978
|
+
for (const file of serviceFiles) {
|
|
9979
|
+
const content = await readText(file);
|
|
9980
|
+
const directAccessPattern = /_context\.\w+\.(?:First|Single|Find|ToList)\s*\(/g;
|
|
9981
|
+
let match;
|
|
9982
|
+
while ((match = directAccessPattern.exec(content)) !== null) {
|
|
9983
|
+
const surroundingCode = content.substring(Math.max(0, match.index - 100), match.index + 100);
|
|
9984
|
+
if (!surroundingCode.includes(".Where(") && !surroundingCode.includes("TenantId")) {
|
|
9985
|
+
const lineNumber = getLineNumber(content, match.index);
|
|
9986
|
+
result.findings.push({
|
|
9987
|
+
severity: "critical",
|
|
9988
|
+
category: "tenant-isolation",
|
|
9989
|
+
message: "Direct DbContext access without tenant filtering",
|
|
9990
|
+
file: path20.relative(structure.root, file),
|
|
9991
|
+
line: lineNumber,
|
|
9992
|
+
code: truncateCode(match[0]),
|
|
9993
|
+
suggestion: "Use repository with global tenant filter or add explicit TenantId filter",
|
|
9994
|
+
cweId: "CWE-639"
|
|
9995
|
+
});
|
|
9996
|
+
}
|
|
9997
|
+
}
|
|
9998
|
+
}
|
|
9999
|
+
}
|
|
10000
|
+
}
|
|
10001
|
+
async function checkAuthorization(structure, result) {
|
|
10002
|
+
const apiPath = structure.api || structure.apiCore;
|
|
10003
|
+
if (!apiPath) {
|
|
10004
|
+
result.findings.push({
|
|
10005
|
+
severity: "warning",
|
|
10006
|
+
category: "authorization",
|
|
10007
|
+
message: "API project not found, skipping authorization validation",
|
|
10008
|
+
file: "",
|
|
10009
|
+
suggestion: "Ensure project structure follows SmartStack conventions"
|
|
10010
|
+
});
|
|
10011
|
+
return;
|
|
10012
|
+
}
|
|
10013
|
+
const controllerFiles = await findFiles("**/Controllers/**/*.cs", { cwd: apiPath });
|
|
10014
|
+
result.stats.filesScanned += controllerFiles.length;
|
|
10015
|
+
for (const file of controllerFiles) {
|
|
10016
|
+
const content = await readText(file);
|
|
10017
|
+
if (content.includes("[ApiController]")) {
|
|
10018
|
+
const hasClassLevelAuth = content.includes("[Authorize]") || content.includes("[NavRoute(") || content.includes("[AllowAnonymous]");
|
|
10019
|
+
if (!hasClassLevelAuth) {
|
|
10020
|
+
const controllerMatch = content.match(/class\s+(\w+Controller)/);
|
|
10021
|
+
const controllerName = controllerMatch ? controllerMatch[1] : "Unknown";
|
|
10022
|
+
const lineNumber = controllerMatch ? getLineNumber(content, controllerMatch.index) : 1;
|
|
10023
|
+
result.findings.push({
|
|
10024
|
+
severity: "blocking",
|
|
10025
|
+
category: "authorization",
|
|
10026
|
+
message: `Controller ${controllerName} missing authorization attribute`,
|
|
10027
|
+
file: path20.relative(structure.root, file),
|
|
10028
|
+
line: lineNumber,
|
|
10029
|
+
suggestion: 'Add [NavRoute("context.application.module")] or [Authorize] attribute to the controller class',
|
|
10030
|
+
cweId: "CWE-862"
|
|
10031
|
+
});
|
|
10032
|
+
}
|
|
10033
|
+
}
|
|
10034
|
+
}
|
|
10035
|
+
}
|
|
10036
|
+
async function checkDangerousFunctions(structure, result) {
|
|
10037
|
+
const csharpPatterns = [
|
|
10038
|
+
{ pattern: /Process\.Start\s*\([^)]*(?:user|input|param|request)/gi, name: "Process.Start with user input" },
|
|
10039
|
+
{ pattern: /Assembly\.Load(?:From|File)?\s*\(/g, name: "Dynamic assembly loading" },
|
|
10040
|
+
{ pattern: /Type\.GetType\s*\([^)]*(?:user|input|param|request)/gi, name: "Type.GetType with user input" },
|
|
10041
|
+
{ pattern: /Activator\.CreateInstance\s*\([^)]*(?:user|input|param|request)/gi, name: "Activator.CreateInstance with user input" }
|
|
10042
|
+
];
|
|
10043
|
+
const typescriptPatterns = [
|
|
10044
|
+
{ pattern: /\beval\s*\(/g, name: "eval() usage" },
|
|
10045
|
+
{ pattern: /new\s+Function\s*\(/g, name: "new Function() usage" },
|
|
10046
|
+
{ pattern: /child_process\.exec\s*\(/g, name: "child_process.exec usage" },
|
|
10047
|
+
{ pattern: /\.innerHTML\s*=/g, name: "innerHTML assignment" },
|
|
10048
|
+
{ pattern: /dangerouslySetInnerHTML/g, name: "dangerouslySetInnerHTML usage" }
|
|
10049
|
+
];
|
|
10050
|
+
const csFiles = await getFilesToScan(structure, ["**/*.cs"]);
|
|
10051
|
+
for (const file of csFiles) {
|
|
10052
|
+
if (isExcludedFile(file)) continue;
|
|
10053
|
+
const content = await readText(file);
|
|
10054
|
+
await checkPatterns(file, content, csharpPatterns, structure, result);
|
|
10055
|
+
}
|
|
10056
|
+
const tsFiles = await getFilesToScan(structure, ["**/*.ts", "**/*.tsx"]);
|
|
10057
|
+
for (const file of tsFiles) {
|
|
10058
|
+
if (isExcludedFile(file)) continue;
|
|
10059
|
+
const content = await readText(file);
|
|
10060
|
+
await checkPatterns(file, content, typescriptPatterns, structure, result);
|
|
10061
|
+
}
|
|
10062
|
+
}
|
|
10063
|
+
async function checkPatterns(file, content, patterns, structure, result) {
|
|
10064
|
+
const lines = content.split("\n");
|
|
10065
|
+
for (const { pattern, name } of patterns) {
|
|
10066
|
+
pattern.lastIndex = 0;
|
|
10067
|
+
let match;
|
|
10068
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
10069
|
+
const lineNumber = getLineNumber(content, match.index);
|
|
10070
|
+
const lineContent = lines[lineNumber - 1]?.trim() || "";
|
|
10071
|
+
result.findings.push({
|
|
10072
|
+
severity: "critical",
|
|
10073
|
+
category: "dangerous-functions",
|
|
10074
|
+
message: `Dangerous function: ${name}`,
|
|
10075
|
+
file: path20.relative(structure.root, file),
|
|
10076
|
+
line: lineNumber,
|
|
10077
|
+
code: truncateCode(lineContent),
|
|
10078
|
+
suggestion: "Avoid using dangerous functions with user-controlled input. Use safe alternatives.",
|
|
10079
|
+
cweId: "CWE-78"
|
|
10080
|
+
});
|
|
10081
|
+
}
|
|
10082
|
+
}
|
|
10083
|
+
}
|
|
10084
|
+
async function checkInputValidation(structure, result) {
|
|
10085
|
+
if (!structure.application) {
|
|
10086
|
+
return;
|
|
10087
|
+
}
|
|
10088
|
+
const dtoFiles = await findFiles("**/*Dto.cs", { cwd: structure.application });
|
|
10089
|
+
const dtos = [];
|
|
10090
|
+
for (const file of dtoFiles) {
|
|
10091
|
+
const content = await readText(file);
|
|
10092
|
+
const dtoMatch = content.match(/class\s+(\w+Dto)/);
|
|
10093
|
+
if (dtoMatch) {
|
|
10094
|
+
dtos.push(dtoMatch[1]);
|
|
10095
|
+
}
|
|
10096
|
+
}
|
|
10097
|
+
const validatorFiles = await findFiles("**/*Validator.cs", { cwd: structure.application });
|
|
10098
|
+
const validators = [];
|
|
10099
|
+
for (const file of validatorFiles) {
|
|
10100
|
+
const content = await readText(file);
|
|
10101
|
+
const validatorMatch = content.match(/class\s+(\w+Validator)/);
|
|
10102
|
+
if (validatorMatch) {
|
|
10103
|
+
validators.push(validatorMatch[1]);
|
|
10104
|
+
}
|
|
10105
|
+
}
|
|
10106
|
+
for (const dto of dtos) {
|
|
10107
|
+
const expectedValidator = dto.replace(/Dto$/, "DtoValidator");
|
|
10108
|
+
const hasValidator = validators.some((v) => v === expectedValidator || v === `${dto}Validator`);
|
|
10109
|
+
if (!hasValidator) {
|
|
10110
|
+
result.findings.push({
|
|
10111
|
+
severity: "warning",
|
|
10112
|
+
category: "input-validation",
|
|
10113
|
+
message: `DTO ${dto} has no FluentValidation validator`,
|
|
10114
|
+
file: `Application/DTOs/${dto}.cs`,
|
|
10115
|
+
suggestion: `Create ${expectedValidator} class with FluentValidation rules`,
|
|
10116
|
+
cweId: "CWE-20"
|
|
10117
|
+
});
|
|
10118
|
+
}
|
|
10119
|
+
}
|
|
10120
|
+
}
|
|
10121
|
+
async function getFilesToScan(structure, patterns) {
|
|
10122
|
+
const allFiles = [];
|
|
10123
|
+
for (const pattern of patterns) {
|
|
10124
|
+
const files = await findFiles(pattern, { cwd: structure.root });
|
|
10125
|
+
for (const file of files) {
|
|
10126
|
+
try {
|
|
10127
|
+
validatePathSecurity(file, structure.root);
|
|
10128
|
+
allFiles.push(file);
|
|
10129
|
+
} catch {
|
|
10130
|
+
logger.warn("Skipping file outside project root", { file });
|
|
10131
|
+
}
|
|
10132
|
+
}
|
|
10133
|
+
}
|
|
10134
|
+
return [...new Set(allFiles)];
|
|
10135
|
+
}
|
|
10136
|
+
function isExcludedFile(filePath) {
|
|
10137
|
+
const exclusions = [
|
|
10138
|
+
/[/\\]bin[/\\]/,
|
|
10139
|
+
/[/\\]obj[/\\]/,
|
|
10140
|
+
/[/\\]node_modules[/\\]/,
|
|
10141
|
+
/\.example\./,
|
|
10142
|
+
/\.template\./,
|
|
10143
|
+
/\.test\./,
|
|
10144
|
+
/\.spec\./,
|
|
10145
|
+
/Tests[/\\]/,
|
|
10146
|
+
/appsettings\.Development\.json$/,
|
|
10147
|
+
/appsettings\.Template\.json$/
|
|
10148
|
+
];
|
|
10149
|
+
return exclusions.some((pattern) => pattern.test(filePath));
|
|
10150
|
+
}
|
|
10151
|
+
function isPlaceholderValue(line) {
|
|
10152
|
+
const placeholderPatterns = [
|
|
10153
|
+
/\${[A-Z_][A-Z0-9_]*}/,
|
|
10154
|
+
// ${VAR_NAME} - env var syntax
|
|
10155
|
+
/%[A-Z_][A-Z0-9_]*%/,
|
|
10156
|
+
// %VAR_NAME% - Windows env var
|
|
10157
|
+
/\{\{[A-Za-z_][A-Za-z0-9_]*\}\}/,
|
|
10158
|
+
// {{varName}} - template syntax
|
|
10159
|
+
/["']your[_-]?(?:password|secret|key|token)["']/i,
|
|
10160
|
+
// "your_password" placeholders
|
|
10161
|
+
/["']changeme["']/i,
|
|
10162
|
+
/["']<[A-Z_]+>["']/,
|
|
10163
|
+
// "<REPLACE_ME>" syntax
|
|
10164
|
+
/["']xxx+["']/i,
|
|
10165
|
+
// "xxx" or "XXXX" placeholders
|
|
10166
|
+
/=\s*["']["']/
|
|
10167
|
+
// Empty string assignments
|
|
10168
|
+
];
|
|
10169
|
+
return placeholderPatterns.some((pattern) => pattern.test(line));
|
|
10170
|
+
}
|
|
10171
|
+
function getLineNumber(content, index) {
|
|
10172
|
+
const normalizedContent = content.substring(0, index).replace(/\r\n/g, "\n");
|
|
10173
|
+
return normalizedContent.split("\n").length;
|
|
10174
|
+
}
|
|
10175
|
+
function truncateCode(code, maxLength = 80) {
|
|
10176
|
+
return code.length > maxLength ? code.substring(0, maxLength) + "..." : code;
|
|
10177
|
+
}
|
|
10178
|
+
function formatSecurityReport(result) {
|
|
10179
|
+
const lines = [];
|
|
10180
|
+
lines.push("# Security Validation Report");
|
|
10181
|
+
lines.push("");
|
|
10182
|
+
lines.push("## Summary");
|
|
10183
|
+
lines.push(`- **Status**: ${result.valid ? "\u2705 PASSED" : "\u274C FAILED"}`);
|
|
10184
|
+
lines.push(`- **Blocking**: ${result.stats.blocking}`);
|
|
10185
|
+
lines.push(`- **Critical**: ${result.stats.critical}`);
|
|
10186
|
+
lines.push(`- **Warnings**: ${result.stats.warning}`);
|
|
10187
|
+
lines.push(`- **Files Scanned**: ${result.stats.filesScanned}`);
|
|
10188
|
+
lines.push("");
|
|
10189
|
+
if (result.findings.length === 0) {
|
|
10190
|
+
lines.push("No security issues found.");
|
|
10191
|
+
return lines.join("\n");
|
|
10192
|
+
}
|
|
10193
|
+
const blocking = result.findings.filter((f) => f.severity === "blocking");
|
|
10194
|
+
const critical = result.findings.filter((f) => f.severity === "critical");
|
|
10195
|
+
const warnings = result.findings.filter((f) => f.severity === "warning");
|
|
10196
|
+
if (blocking.length > 0) {
|
|
10197
|
+
lines.push("## Blocking Issues");
|
|
10198
|
+
lines.push("");
|
|
10199
|
+
for (const finding of blocking) {
|
|
10200
|
+
formatFinding(finding, lines);
|
|
10201
|
+
}
|
|
10202
|
+
}
|
|
10203
|
+
if (critical.length > 0) {
|
|
10204
|
+
lines.push("## Critical Issues");
|
|
10205
|
+
lines.push("");
|
|
10206
|
+
for (const finding of critical) {
|
|
10207
|
+
formatFinding(finding, lines);
|
|
10208
|
+
}
|
|
10209
|
+
}
|
|
10210
|
+
if (warnings.length > 0) {
|
|
10211
|
+
lines.push("## Warnings");
|
|
10212
|
+
lines.push("");
|
|
10213
|
+
for (const finding of warnings) {
|
|
10214
|
+
formatFinding(finding, lines);
|
|
10215
|
+
}
|
|
10216
|
+
}
|
|
10217
|
+
return lines.join("\n");
|
|
10218
|
+
}
|
|
10219
|
+
function formatFinding(finding, lines) {
|
|
10220
|
+
lines.push(`### ${finding.category}: ${finding.message}`);
|
|
10221
|
+
if (finding.file) {
|
|
10222
|
+
lines.push(`- **File**: \`${finding.file}${finding.line ? `:${finding.line}` : ""}\``);
|
|
10223
|
+
}
|
|
10224
|
+
if (finding.code) {
|
|
10225
|
+
lines.push(`- **Code**: \`${finding.code}\``);
|
|
10226
|
+
}
|
|
10227
|
+
if (finding.cweId) {
|
|
10228
|
+
lines.push(`- **CWE**: ${finding.cweId}`);
|
|
10229
|
+
}
|
|
10230
|
+
lines.push(`- **Fix**: ${finding.suggestion}`);
|
|
10231
|
+
lines.push("");
|
|
10232
|
+
}
|
|
10233
|
+
|
|
10234
|
+
// src/tools/analyze-code-quality.ts
|
|
10235
|
+
import path21 from "path";
|
|
10236
|
+
var analyzeCodeQualityTool = {
|
|
10237
|
+
name: "analyze_code_quality",
|
|
10238
|
+
description: "Analyze code quality metrics for SmartStack projects: cognitive complexity, cyclomatic complexity, function size, nesting depth, and maintainability indicators.",
|
|
10239
|
+
inputSchema: {
|
|
10240
|
+
type: "object",
|
|
10241
|
+
properties: {
|
|
10242
|
+
path: {
|
|
10243
|
+
type: "string",
|
|
10244
|
+
description: "Project path to analyze (default: SmartStack.app path from config)"
|
|
10245
|
+
},
|
|
10246
|
+
metrics: {
|
|
10247
|
+
type: "array",
|
|
10248
|
+
items: {
|
|
10249
|
+
type: "string",
|
|
10250
|
+
enum: [
|
|
10251
|
+
"cognitive-complexity",
|
|
10252
|
+
"cyclomatic-complexity",
|
|
10253
|
+
"function-size",
|
|
10254
|
+
"nesting-depth",
|
|
10255
|
+
"parameter-count",
|
|
10256
|
+
"code-duplication",
|
|
10257
|
+
"file-size",
|
|
10258
|
+
"all"
|
|
10259
|
+
]
|
|
10260
|
+
},
|
|
10261
|
+
description: "Metrics to analyze",
|
|
10262
|
+
default: ["all"]
|
|
10263
|
+
},
|
|
10264
|
+
threshold: {
|
|
10265
|
+
type: "string",
|
|
10266
|
+
enum: ["strict", "normal", "lenient"],
|
|
10267
|
+
description: "Threshold level for violations",
|
|
10268
|
+
default: "normal"
|
|
10269
|
+
},
|
|
10270
|
+
scope: {
|
|
10271
|
+
type: "string",
|
|
10272
|
+
enum: ["changed", "all"],
|
|
10273
|
+
description: "Analyze only changed files or all",
|
|
10274
|
+
default: "all"
|
|
10275
|
+
}
|
|
10276
|
+
}
|
|
10277
|
+
}
|
|
10278
|
+
};
|
|
10279
|
+
var THRESHOLDS = {
|
|
10280
|
+
strict: {
|
|
10281
|
+
cognitiveComplexity: 10,
|
|
10282
|
+
cyclomaticComplexity: 8,
|
|
10283
|
+
functionSize: 30,
|
|
10284
|
+
nestingDepth: 2,
|
|
10285
|
+
parameterCount: 3,
|
|
10286
|
+
fileSize: 300
|
|
10287
|
+
},
|
|
10288
|
+
normal: {
|
|
10289
|
+
cognitiveComplexity: 15,
|
|
10290
|
+
cyclomaticComplexity: 10,
|
|
10291
|
+
functionSize: 50,
|
|
10292
|
+
nestingDepth: 3,
|
|
10293
|
+
parameterCount: 4,
|
|
10294
|
+
fileSize: 500
|
|
10295
|
+
},
|
|
10296
|
+
lenient: {
|
|
10297
|
+
cognitiveComplexity: 25,
|
|
10298
|
+
cyclomaticComplexity: 15,
|
|
10299
|
+
functionSize: 80,
|
|
10300
|
+
nestingDepth: 4,
|
|
10301
|
+
parameterCount: 5,
|
|
10302
|
+
fileSize: 800
|
|
10303
|
+
}
|
|
10304
|
+
};
|
|
10305
|
+
async function handleAnalyzeCodeQuality(args, config) {
|
|
10306
|
+
const input = AnalyzeCodeQualityInputSchema.parse(args);
|
|
10307
|
+
const projectPath = input.path || config.smartstack.projectPath;
|
|
10308
|
+
const thresholdLevel = input.threshold;
|
|
10309
|
+
const thresholds = THRESHOLDS[thresholdLevel];
|
|
10310
|
+
logger.info("Analyzing code quality", { projectPath, threshold: thresholdLevel });
|
|
10311
|
+
const structure = await findSmartStackStructure(projectPath);
|
|
10312
|
+
const allFunctionMetrics = [];
|
|
10313
|
+
const fileMetrics = /* @__PURE__ */ new Map();
|
|
10314
|
+
const csFiles = await findFiles("**/*.cs", { cwd: structure.root });
|
|
10315
|
+
const filteredCsFiles = csFiles.filter((f) => !isExcludedPath(f));
|
|
10316
|
+
for (const file of filteredCsFiles) {
|
|
10317
|
+
const content = await readText(file);
|
|
10318
|
+
const relPath = path21.relative(structure.root, file);
|
|
10319
|
+
const lineCount = content.split("\n").length;
|
|
10320
|
+
const functions = extractCSharpFunctions(content, relPath);
|
|
10321
|
+
allFunctionMetrics.push(...functions);
|
|
10322
|
+
fileMetrics.set(relPath, { lineCount, functions: functions.length });
|
|
10323
|
+
}
|
|
10324
|
+
const tsFiles = await findFiles("**/*.{ts,tsx}", { cwd: structure.root });
|
|
10325
|
+
const filteredTsFiles = tsFiles.filter((f) => !isExcludedPath(f));
|
|
10326
|
+
for (const file of filteredTsFiles) {
|
|
10327
|
+
const content = await readText(file);
|
|
10328
|
+
const relPath = path21.relative(structure.root, file);
|
|
10329
|
+
const lineCount = content.split("\n").length;
|
|
10330
|
+
const functions = extractTypeScriptFunctions(content, relPath);
|
|
10331
|
+
allFunctionMetrics.push(...functions);
|
|
10332
|
+
fileMetrics.set(relPath, { lineCount, functions: functions.length });
|
|
10333
|
+
}
|
|
10334
|
+
const metrics = calculateMetrics(allFunctionMetrics, fileMetrics, thresholds);
|
|
10335
|
+
const hotspots = identifyHotspots(allFunctionMetrics, fileMetrics, thresholds);
|
|
10336
|
+
const summary = calculateSummary(allFunctionMetrics, fileMetrics, metrics, hotspots);
|
|
10337
|
+
const result = {
|
|
10338
|
+
summary,
|
|
10339
|
+
metrics,
|
|
10340
|
+
hotspots
|
|
10341
|
+
};
|
|
10342
|
+
return formatQualityReport(result, thresholds);
|
|
10343
|
+
}
|
|
10344
|
+
function extractCSharpFunctions(content, file) {
|
|
10345
|
+
const functions = [];
|
|
10346
|
+
const lines = content.split("\n");
|
|
10347
|
+
const methodPattern = /\b(public|private|protected|internal)\s+(?:async\s+)?(?:static\s+)?(?:virtual\s+)?(?:override\s+)?(?:[\w<>,\s\[\]]+)\s+(\w+)\s*\(([^)]*)\)/gm;
|
|
10348
|
+
let match;
|
|
10349
|
+
while ((match = methodPattern.exec(content)) !== null) {
|
|
10350
|
+
const methodName = match[2];
|
|
10351
|
+
const params = match[3];
|
|
10352
|
+
const startLine = getLineNumber2(content, match.index);
|
|
10353
|
+
const endLine = findMethodEnd(lines, startLine - 1);
|
|
10354
|
+
const lineCount = endLine - startLine + 1;
|
|
10355
|
+
const parameterCount = params.trim() ? params.split(",").length : 0;
|
|
10356
|
+
const methodBody = lines.slice(startLine - 1, endLine).join("\n");
|
|
10357
|
+
const cognitiveComplexity = calculateCognitiveComplexity(methodBody);
|
|
10358
|
+
const cyclomaticComplexity = calculateCyclomaticComplexity(methodBody);
|
|
10359
|
+
const maxNestingDepth = calculateNestingDepth(methodBody);
|
|
10360
|
+
functions.push({
|
|
10361
|
+
name: methodName,
|
|
10362
|
+
file,
|
|
10363
|
+
startLine,
|
|
10364
|
+
endLine,
|
|
10365
|
+
lineCount,
|
|
10366
|
+
parameterCount,
|
|
10367
|
+
maxNestingDepth,
|
|
10368
|
+
cognitiveComplexity,
|
|
10369
|
+
cyclomaticComplexity
|
|
10370
|
+
});
|
|
10371
|
+
}
|
|
10372
|
+
return functions;
|
|
10373
|
+
}
|
|
10374
|
+
function extractTypeScriptFunctions(content, file) {
|
|
10375
|
+
const functions = [];
|
|
10376
|
+
const lines = content.split("\n");
|
|
10377
|
+
const processedFunctions = /* @__PURE__ */ new Set();
|
|
10378
|
+
const patterns = [
|
|
10379
|
+
// Arrow functions assigned to const/let
|
|
10380
|
+
/(?:const|let)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w+)?\s*=>/g,
|
|
10381
|
+
// Function declarations
|
|
10382
|
+
/(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/g,
|
|
10383
|
+
// Class methods (removed ^ anchor for consistency)
|
|
10384
|
+
/\b(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*(?::\s*[\w<>,\s\[\]]+)?\s*\{/gm
|
|
10385
|
+
];
|
|
10386
|
+
for (const pattern of patterns) {
|
|
10387
|
+
pattern.lastIndex = 0;
|
|
10388
|
+
let match;
|
|
10389
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
10390
|
+
const funcName = match[1];
|
|
10391
|
+
const params = match[2] || "";
|
|
10392
|
+
const startLine = getLineNumber2(content, match.index);
|
|
10393
|
+
if (["if", "for", "while", "switch", "catch", "constructor"].includes(funcName)) continue;
|
|
10394
|
+
const funcKey = `${file}:${startLine}:${funcName}`;
|
|
10395
|
+
if (processedFunctions.has(funcKey)) continue;
|
|
10396
|
+
processedFunctions.add(funcKey);
|
|
10397
|
+
const endLine = findMethodEnd(lines, startLine - 1);
|
|
10398
|
+
const lineCount = endLine - startLine + 1;
|
|
10399
|
+
const parameterCount = params.trim() ? params.split(",").length : 0;
|
|
10400
|
+
const methodBody = lines.slice(startLine - 1, endLine).join("\n");
|
|
10401
|
+
const cognitiveComplexity = calculateCognitiveComplexity(methodBody);
|
|
10402
|
+
const cyclomaticComplexity = calculateCyclomaticComplexity(methodBody);
|
|
10403
|
+
const maxNestingDepth = calculateNestingDepth(methodBody);
|
|
10404
|
+
functions.push({
|
|
10405
|
+
name: funcName,
|
|
10406
|
+
file,
|
|
10407
|
+
startLine,
|
|
10408
|
+
endLine,
|
|
10409
|
+
lineCount,
|
|
10410
|
+
parameterCount,
|
|
10411
|
+
maxNestingDepth,
|
|
10412
|
+
cognitiveComplexity,
|
|
10413
|
+
cyclomaticComplexity
|
|
10414
|
+
});
|
|
10415
|
+
}
|
|
10416
|
+
}
|
|
10417
|
+
return functions;
|
|
10418
|
+
}
|
|
10419
|
+
function findMethodEnd(lines, startIndex) {
|
|
10420
|
+
let braceCount = 0;
|
|
10421
|
+
let foundFirstBrace = false;
|
|
10422
|
+
for (let i = startIndex; i < lines.length; i++) {
|
|
10423
|
+
const line = stripStringsAndComments(lines[i]);
|
|
10424
|
+
for (const char of line) {
|
|
10425
|
+
if (char === "{") {
|
|
10426
|
+
braceCount++;
|
|
10427
|
+
foundFirstBrace = true;
|
|
10428
|
+
} else if (char === "}") {
|
|
10429
|
+
braceCount--;
|
|
10430
|
+
if (foundFirstBrace && braceCount === 0) {
|
|
10431
|
+
return i + 1;
|
|
10432
|
+
}
|
|
10433
|
+
}
|
|
10434
|
+
}
|
|
10435
|
+
}
|
|
10436
|
+
return startIndex + 1;
|
|
10437
|
+
}
|
|
10438
|
+
function stripStringsAndComments(line) {
|
|
10439
|
+
let result = line.replace(/\/\/.*$/, "");
|
|
10440
|
+
result = result.replace(/"(?:[^"\\]|\\.)*"/g, '""');
|
|
10441
|
+
result = result.replace(/'(?:[^'\\]|\\.)*'/g, "''");
|
|
10442
|
+
result = result.replace(/`(?:[^`\\]|\\.)*`/g, "``");
|
|
10443
|
+
return result;
|
|
10444
|
+
}
|
|
10445
|
+
function calculateCognitiveComplexity(code) {
|
|
10446
|
+
let complexity = 0;
|
|
10447
|
+
let nestingLevel = 0;
|
|
10448
|
+
const lines = code.split("\n");
|
|
10449
|
+
for (const line of lines) {
|
|
10450
|
+
const trimmed = line.trim();
|
|
10451
|
+
const controlFlowPatterns = [
|
|
10452
|
+
/\bif\s*\(/g,
|
|
10453
|
+
/\belse\s+if\s*\(/g,
|
|
10454
|
+
/\bfor\s*\(/g,
|
|
10455
|
+
/\bforeach\s*\(/g,
|
|
10456
|
+
/\bwhile\s*\(/g,
|
|
10457
|
+
/\bdo\s*\{/g,
|
|
10458
|
+
/\bswitch\s*\(/g,
|
|
10459
|
+
/\bcatch\s*\(/g
|
|
10460
|
+
];
|
|
10461
|
+
for (const pattern of controlFlowPatterns) {
|
|
10462
|
+
const matches = trimmed.match(pattern) || [];
|
|
10463
|
+
complexity += matches.length * (1 + nestingLevel);
|
|
10464
|
+
}
|
|
10465
|
+
const logicalPatterns = [/&&/g, /\|\|/g, /\?\?/g, /\?\./g];
|
|
10466
|
+
for (const pattern of logicalPatterns) {
|
|
10467
|
+
const matches = trimmed.match(pattern) || [];
|
|
10468
|
+
complexity += matches.length;
|
|
10469
|
+
}
|
|
10470
|
+
const ternaryMatches = trimmed.match(/\?[^?:]+:/g) || [];
|
|
10471
|
+
complexity += ternaryMatches.length;
|
|
10472
|
+
const openBraces = (trimmed.match(/\{/g) || []).length;
|
|
10473
|
+
const closeBraces = (trimmed.match(/\}/g) || []).length;
|
|
10474
|
+
nestingLevel += openBraces - closeBraces;
|
|
10475
|
+
nestingLevel = Math.max(0, nestingLevel);
|
|
10476
|
+
}
|
|
10477
|
+
return complexity;
|
|
10478
|
+
}
|
|
10479
|
+
function calculateCyclomaticComplexity(code) {
|
|
10480
|
+
let complexity = 1;
|
|
10481
|
+
const patterns = [
|
|
10482
|
+
/\bif\b/g,
|
|
10483
|
+
/\belse\s+if\b/g,
|
|
10484
|
+
/\bwhile\b/g,
|
|
10485
|
+
/\bfor\b/g,
|
|
10486
|
+
/\bforeach\b/g,
|
|
10487
|
+
/\bcase\b/g,
|
|
10488
|
+
/\bcatch\b/g,
|
|
10489
|
+
/\?\?/g,
|
|
10490
|
+
/\?\./g,
|
|
10491
|
+
/&&/g,
|
|
10492
|
+
/\|\|/g
|
|
10493
|
+
];
|
|
10494
|
+
for (const pattern of patterns) {
|
|
10495
|
+
const matches = code.match(pattern) || [];
|
|
10496
|
+
complexity += matches.length;
|
|
10497
|
+
}
|
|
10498
|
+
return complexity;
|
|
10499
|
+
}
|
|
10500
|
+
function calculateNestingDepth(code) {
|
|
10501
|
+
let maxDepth = 0;
|
|
10502
|
+
let currentDepth = 0;
|
|
10503
|
+
for (const char of code) {
|
|
10504
|
+
if (char === "{") {
|
|
10505
|
+
currentDepth++;
|
|
10506
|
+
maxDepth = Math.max(maxDepth, currentDepth);
|
|
10507
|
+
} else if (char === "}") {
|
|
10508
|
+
currentDepth = Math.max(0, currentDepth - 1);
|
|
10509
|
+
}
|
|
10510
|
+
}
|
|
10511
|
+
return maxDepth;
|
|
10512
|
+
}
|
|
10513
|
+
function calculateMetrics(functions, fileMetrics, thresholds) {
|
|
10514
|
+
const createStat = (values, threshold) => {
|
|
10515
|
+
const filtered = values.filter((v) => v > 0);
|
|
10516
|
+
const avg = filtered.length > 0 ? filtered.reduce((a, b) => a + b, 0) / filtered.length : 0;
|
|
10517
|
+
const max = filtered.length > 0 ? Math.max(...filtered) : 0;
|
|
10518
|
+
const violations = filtered.filter((v) => v > threshold).length;
|
|
10519
|
+
return {
|
|
10520
|
+
average: Math.round(avg * 10) / 10,
|
|
10521
|
+
max,
|
|
10522
|
+
threshold,
|
|
10523
|
+
violations
|
|
10524
|
+
};
|
|
10525
|
+
};
|
|
10526
|
+
const fileSizes = Array.from(fileMetrics.values()).map((f) => f.lineCount);
|
|
10527
|
+
return {
|
|
10528
|
+
cognitiveComplexity: createStat(
|
|
10529
|
+
functions.map((f) => f.cognitiveComplexity),
|
|
10530
|
+
thresholds.cognitiveComplexity
|
|
10531
|
+
),
|
|
10532
|
+
cyclomaticComplexity: createStat(
|
|
10533
|
+
functions.map((f) => f.cyclomaticComplexity),
|
|
10534
|
+
thresholds.cyclomaticComplexity
|
|
10535
|
+
),
|
|
10536
|
+
functionSize: createStat(
|
|
10537
|
+
functions.map((f) => f.lineCount),
|
|
10538
|
+
thresholds.functionSize
|
|
10539
|
+
),
|
|
10540
|
+
nestingDepth: createStat(
|
|
10541
|
+
functions.map((f) => f.maxNestingDepth),
|
|
10542
|
+
thresholds.nestingDepth
|
|
10543
|
+
),
|
|
10544
|
+
fileSize: createStat(fileSizes, thresholds.fileSize)
|
|
10545
|
+
};
|
|
10546
|
+
}
|
|
10547
|
+
function identifyHotspots(functions, fileMetrics, thresholds) {
|
|
10548
|
+
const hotspots = [];
|
|
10549
|
+
for (const func of functions) {
|
|
10550
|
+
const issues = [];
|
|
10551
|
+
if (func.cognitiveComplexity > thresholds.cognitiveComplexity) {
|
|
10552
|
+
issues.push(`Cognitive complexity: ${func.cognitiveComplexity} (threshold: ${thresholds.cognitiveComplexity})`);
|
|
10553
|
+
}
|
|
10554
|
+
if (func.cyclomaticComplexity > thresholds.cyclomaticComplexity) {
|
|
10555
|
+
issues.push(`Cyclomatic complexity: ${func.cyclomaticComplexity} (threshold: ${thresholds.cyclomaticComplexity})`);
|
|
10556
|
+
}
|
|
10557
|
+
if (func.lineCount > thresholds.functionSize) {
|
|
10558
|
+
issues.push(`Lines: ${func.lineCount} (threshold: ${thresholds.functionSize})`);
|
|
10559
|
+
}
|
|
10560
|
+
if (func.maxNestingDepth > thresholds.nestingDepth) {
|
|
10561
|
+
issues.push(`Nesting depth: ${func.maxNestingDepth} (threshold: ${thresholds.nestingDepth})`);
|
|
10562
|
+
}
|
|
10563
|
+
if (issues.length > 0) {
|
|
10564
|
+
const severity = issues.length >= 3 ? "high" : issues.length === 2 ? "medium" : "low";
|
|
10565
|
+
hotspots.push({
|
|
10566
|
+
file: func.file,
|
|
10567
|
+
function: func.name,
|
|
10568
|
+
issues,
|
|
10569
|
+
severity,
|
|
10570
|
+
metrics: {
|
|
10571
|
+
cognitiveComplexity: func.cognitiveComplexity,
|
|
10572
|
+
cyclomaticComplexity: func.cyclomaticComplexity,
|
|
10573
|
+
lineCount: func.lineCount,
|
|
10574
|
+
nestingDepth: func.maxNestingDepth
|
|
10575
|
+
}
|
|
10576
|
+
});
|
|
10577
|
+
}
|
|
10578
|
+
}
|
|
10579
|
+
for (const [file, metrics] of fileMetrics) {
|
|
10580
|
+
if (metrics.lineCount > thresholds.fileSize) {
|
|
10581
|
+
hotspots.push({
|
|
10582
|
+
file,
|
|
10583
|
+
issues: [`File size: ${metrics.lineCount} lines (threshold: ${thresholds.fileSize})`],
|
|
10584
|
+
severity: "medium",
|
|
10585
|
+
metrics: {
|
|
10586
|
+
lineCount: metrics.lineCount
|
|
10587
|
+
}
|
|
10588
|
+
});
|
|
10589
|
+
}
|
|
10590
|
+
}
|
|
10591
|
+
const severityOrder = { high: 0, medium: 1, low: 2 };
|
|
10592
|
+
hotspots.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
10593
|
+
return hotspots.slice(0, 20);
|
|
10594
|
+
}
|
|
10595
|
+
function calculateSummary(functions, fileMetrics, metrics, hotspots) {
|
|
10596
|
+
const totalViolations = metrics.cognitiveComplexity.violations + metrics.cyclomaticComplexity.violations + metrics.functionSize.violations + metrics.nestingDepth.violations + metrics.fileSize.violations;
|
|
10597
|
+
const totalFunctions = functions.length;
|
|
10598
|
+
const totalFiles = fileMetrics.size;
|
|
10599
|
+
const violationRate = totalFunctions > 0 ? totalViolations / totalFunctions : 0;
|
|
10600
|
+
const score = Math.max(0, Math.round(100 - violationRate * 100));
|
|
10601
|
+
let grade;
|
|
10602
|
+
if (score >= 90) grade = "A";
|
|
10603
|
+
else if (score >= 80) grade = "B";
|
|
10604
|
+
else if (score >= 70) grade = "C";
|
|
10605
|
+
else if (score >= 60) grade = "D";
|
|
10606
|
+
else grade = "F";
|
|
10607
|
+
return {
|
|
10608
|
+
score,
|
|
10609
|
+
grade,
|
|
10610
|
+
filesAnalyzed: totalFiles,
|
|
10611
|
+
functionsAnalyzed: totalFunctions,
|
|
10612
|
+
issuesFound: hotspots.length
|
|
10613
|
+
};
|
|
10614
|
+
}
|
|
10615
|
+
function isExcludedPath(filePath) {
|
|
10616
|
+
const exclusions = [
|
|
10617
|
+
/[/\\]bin[/\\]/,
|
|
10618
|
+
/[/\\]obj[/\\]/,
|
|
10619
|
+
/[/\\]node_modules[/\\]/,
|
|
10620
|
+
/[/\\]Migrations[/\\]/,
|
|
10621
|
+
/\.test\./,
|
|
10622
|
+
/\.spec\./,
|
|
10623
|
+
/Tests[/\\]/,
|
|
10624
|
+
/\.d\.ts$/,
|
|
10625
|
+
/\.min\./
|
|
10626
|
+
];
|
|
10627
|
+
return exclusions.some((pattern) => pattern.test(filePath));
|
|
10628
|
+
}
|
|
10629
|
+
function getLineNumber2(content, index) {
|
|
10630
|
+
const normalizedContent = content.substring(0, index).replace(/\r\n/g, "\n");
|
|
10631
|
+
return normalizedContent.split("\n").length;
|
|
10632
|
+
}
|
|
10633
|
+
function formatQualityReport(result, thresholds) {
|
|
10634
|
+
const lines = [];
|
|
10635
|
+
lines.push("# Code Quality Report");
|
|
10636
|
+
lines.push("");
|
|
10637
|
+
lines.push("## Summary");
|
|
10638
|
+
lines.push(`- **Score**: ${result.summary.score}/100 (Grade: ${result.summary.grade})`);
|
|
10639
|
+
lines.push(`- **Files analyzed**: ${result.summary.filesAnalyzed}`);
|
|
10640
|
+
lines.push(`- **Functions analyzed**: ${result.summary.functionsAnalyzed}`);
|
|
10641
|
+
lines.push(`- **Issues found**: ${result.summary.issuesFound}`);
|
|
10642
|
+
lines.push("");
|
|
10643
|
+
lines.push("## Metrics Overview");
|
|
10644
|
+
lines.push("");
|
|
10645
|
+
lines.push("| Metric | Average | Max | Threshold | Status |");
|
|
10646
|
+
lines.push("|--------|---------|-----|-----------|--------|");
|
|
10647
|
+
const formatMetricRow = (name, stat2) => {
|
|
10648
|
+
const status = stat2.violations > 0 ? `${stat2.violations} violations` : "\u2705 OK";
|
|
10649
|
+
return `| ${name} | ${stat2.average} | ${stat2.max} | ${stat2.threshold} | ${status} |`;
|
|
10650
|
+
};
|
|
10651
|
+
lines.push(formatMetricRow("Cognitive Complexity", result.metrics.cognitiveComplexity));
|
|
10652
|
+
lines.push(formatMetricRow("Cyclomatic Complexity", result.metrics.cyclomaticComplexity));
|
|
10653
|
+
lines.push(formatMetricRow("Function Size", result.metrics.functionSize));
|
|
10654
|
+
lines.push(formatMetricRow("Nesting Depth", result.metrics.nestingDepth));
|
|
10655
|
+
lines.push(formatMetricRow("File Size", result.metrics.fileSize));
|
|
10656
|
+
lines.push("");
|
|
10657
|
+
if (result.hotspots.length > 0) {
|
|
10658
|
+
lines.push("## Hotspots (Needs Attention)");
|
|
10659
|
+
lines.push("");
|
|
10660
|
+
for (const hotspot of result.hotspots) {
|
|
10661
|
+
const severityEmoji = hotspot.severity === "high" ? "\u{1F534}" : hotspot.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
10662
|
+
const location = hotspot.function ? `\`${hotspot.function}\` (${hotspot.file})` : `\`${hotspot.file}\``;
|
|
10663
|
+
lines.push(`### ${severityEmoji} ${hotspot.severity.toUpperCase()}: ${location}`);
|
|
10664
|
+
for (const issue of hotspot.issues) {
|
|
10665
|
+
lines.push(`- ${issue}`);
|
|
10666
|
+
}
|
|
10667
|
+
if (hotspot.function) {
|
|
10668
|
+
if (hotspot.metrics.cognitiveComplexity && hotspot.metrics.cognitiveComplexity > thresholds.cognitiveComplexity) {
|
|
10669
|
+
lines.push(`- **Recommendation**: Extract logic into smaller, focused methods`);
|
|
10670
|
+
} else if (hotspot.metrics.lineCount && hotspot.metrics.lineCount > thresholds.functionSize) {
|
|
10671
|
+
lines.push(`- **Recommendation**: Split into multiple functions with single responsibilities`);
|
|
10672
|
+
}
|
|
10673
|
+
} else {
|
|
10674
|
+
lines.push(`- **Recommendation**: Consider splitting this file into smaller modules`);
|
|
10675
|
+
}
|
|
10676
|
+
lines.push("");
|
|
10677
|
+
}
|
|
10678
|
+
} else {
|
|
10679
|
+
lines.push("No hotspots found. Code quality is within acceptable thresholds.");
|
|
10680
|
+
}
|
|
10681
|
+
return lines.join("\n");
|
|
10682
|
+
}
|
|
10683
|
+
|
|
9606
10684
|
// src/resources/conventions.ts
|
|
9607
10685
|
var conventionsResourceTemplate = {
|
|
9608
10686
|
uri: "smartstack://conventions",
|
|
@@ -10468,6 +11546,76 @@ All tenant-related tables use the \`tenant_\` prefix:
|
|
|
10468
11546
|
|
|
10469
11547
|
---
|
|
10470
11548
|
|
|
11549
|
+
## 10. Frontend Conventions
|
|
11550
|
+
|
|
11551
|
+
### Layout Standards
|
|
11552
|
+
|
|
11553
|
+
All context layouts MUST follow the AdminLayout pattern for consistent user experience.
|
|
11554
|
+
|
|
11555
|
+
#### Standard Structure
|
|
11556
|
+
|
|
11557
|
+
\`\`\`tsx
|
|
11558
|
+
<main className={\`pt-16 transition-all duration-300 \${
|
|
11559
|
+
showSidebar ? (isCollapsed ? 'lg:pl-16' : 'lg:pl-64') : ''
|
|
11560
|
+
}\`}>
|
|
11561
|
+
<div className="p-4 sm:p-6 lg:px-10 h-full overflow-auto">
|
|
11562
|
+
<Outlet />
|
|
11563
|
+
</div>
|
|
11564
|
+
</main>
|
|
11565
|
+
\`\`\`
|
|
11566
|
+
|
|
11567
|
+
#### Layout Rules
|
|
11568
|
+
|
|
11569
|
+
| Rule | Description |
|
|
11570
|
+
|------|-------------|
|
|
11571
|
+
| No \`max-w-*\` | Content MUST occupy full available width |
|
|
11572
|
+
| Horizontal padding | Use \`lg:px-10\` for consistent padding |
|
|
11573
|
+
| Scroll container | Use \`h-full overflow-auto\` for internal scrolling |
|
|
11574
|
+
| Sidebar conditional | \`showSidebar = !!currentAppCode\` |
|
|
11575
|
+
|
|
11576
|
+
### Tab Navigation with URL
|
|
11577
|
+
|
|
11578
|
+
Pages with tabs MUST synchronize tab state with URL for shareable links and browser navigation.
|
|
11579
|
+
|
|
11580
|
+
#### useTabNavigation Hook
|
|
11581
|
+
|
|
11582
|
+
\`\`\`tsx
|
|
11583
|
+
import { useTabNavigation } from '@/hooks/useTabNavigation';
|
|
11584
|
+
|
|
11585
|
+
// 1. Define valid tabs
|
|
11586
|
+
const VALID_TABS = ['info', 'settings', 'permissions'] as const;
|
|
11587
|
+
type TabId = typeof VALID_TABS[number];
|
|
11588
|
+
|
|
11589
|
+
// 2. Use the hook
|
|
11590
|
+
const { activeTab, setActiveTab } = useTabNavigation<TabId>('info', VALID_TABS);
|
|
11591
|
+
|
|
11592
|
+
// 3. Use in JSX
|
|
11593
|
+
<button onClick={() => setActiveTab('settings')}>Settings</button>
|
|
11594
|
+
\`\`\`
|
|
11595
|
+
|
|
11596
|
+
#### URL Behavior
|
|
11597
|
+
|
|
11598
|
+
| URL | Active Tab |
|
|
11599
|
+
|-----|------------|
|
|
11600
|
+
| \`/page\` | Default tab (e.g., \`info\`) |
|
|
11601
|
+
| \`/page?tab=settings\` | \`settings\` |
|
|
11602
|
+
| \`/page?tab=invalid\` | Default tab (fallback) |
|
|
11603
|
+
|
|
11604
|
+
#### Hook Characteristics
|
|
11605
|
+
|
|
11606
|
+
- URL is clean for default tab (no \`?tab=\` parameter)
|
|
11607
|
+
- Invalid tabs fallback to default
|
|
11608
|
+
- Uses \`replace: true\` to avoid polluting browser history
|
|
11609
|
+
|
|
11610
|
+
### Frontend Validation Checks
|
|
11611
|
+
|
|
11612
|
+
| Check | Description |
|
|
11613
|
+
|-------|-------------|
|
|
11614
|
+
| \`layouts\` | Verify no \`max-w-*\` in content wrapper, standard padding present |
|
|
11615
|
+
| \`tabs\` | Verify \`useTabNavigation\` hook usage for pages with tabs |
|
|
11616
|
+
|
|
11617
|
+
---
|
|
11618
|
+
|
|
10471
11619
|
## Quick Reference
|
|
10472
11620
|
|
|
10473
11621
|
| Category | Convention | Example |
|
|
@@ -10500,6 +11648,10 @@ All tenant-related tables use the \`tenant_\` prefix:
|
|
|
10500
11648
|
| Validation | \`withValidation: true\` | FluentValidation validators |
|
|
10501
11649
|
| Repository | \`withRepository: true\` | Interface + Implementation |
|
|
10502
11650
|
| Migration naming | \`suggest_migration\` | {context}_v{version}_{seq}_{Desc} |
|
|
11651
|
+
| Layout wrapper | No \`max-w-*\` | Content fills available width |
|
|
11652
|
+
| Layout padding | \`lg:px-10\` | Standard horizontal padding |
|
|
11653
|
+
| Tab navigation | \`useTabNavigation\` hook | Sync tabs with URL |
|
|
11654
|
+
| Tab URL | \`?tab=name\` | Shareable deep links to tabs |
|
|
10503
11655
|
|
|
10504
11656
|
---
|
|
10505
11657
|
|
|
@@ -10664,7 +11816,7 @@ Run specific or all checks:
|
|
|
10664
11816
|
}
|
|
10665
11817
|
|
|
10666
11818
|
// src/resources/project-info.ts
|
|
10667
|
-
import
|
|
11819
|
+
import path22 from "path";
|
|
10668
11820
|
var projectInfoResourceTemplate = {
|
|
10669
11821
|
uri: "smartstack://project",
|
|
10670
11822
|
name: "SmartStack Project Info",
|
|
@@ -10701,16 +11853,16 @@ async function getProjectInfoResource(config) {
|
|
|
10701
11853
|
lines.push("```");
|
|
10702
11854
|
lines.push(`${projectInfo.name}/`);
|
|
10703
11855
|
if (structure.domain) {
|
|
10704
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
11856
|
+
lines.push(`\u251C\u2500\u2500 ${path22.basename(structure.domain)}/ # Domain layer (entities)`);
|
|
10705
11857
|
}
|
|
10706
11858
|
if (structure.application) {
|
|
10707
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
11859
|
+
lines.push(`\u251C\u2500\u2500 ${path22.basename(structure.application)}/ # Application layer (services)`);
|
|
10708
11860
|
}
|
|
10709
11861
|
if (structure.infrastructure) {
|
|
10710
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
11862
|
+
lines.push(`\u251C\u2500\u2500 ${path22.basename(structure.infrastructure)}/ # Infrastructure (EF Core)`);
|
|
10711
11863
|
}
|
|
10712
11864
|
if (structure.api) {
|
|
10713
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
11865
|
+
lines.push(`\u251C\u2500\u2500 ${path22.basename(structure.api)}/ # API layer (controllers)`);
|
|
10714
11866
|
}
|
|
10715
11867
|
if (structure.web) {
|
|
10716
11868
|
lines.push(`\u2514\u2500\u2500 web/smartstack-web/ # React frontend`);
|
|
@@ -10723,8 +11875,8 @@ async function getProjectInfoResource(config) {
|
|
|
10723
11875
|
lines.push("| Project | Path |");
|
|
10724
11876
|
lines.push("|---------|------|");
|
|
10725
11877
|
for (const csproj of projectInfo.csprojFiles) {
|
|
10726
|
-
const name =
|
|
10727
|
-
const relativePath =
|
|
11878
|
+
const name = path22.basename(csproj, ".csproj");
|
|
11879
|
+
const relativePath = path22.relative(projectPath, csproj);
|
|
10728
11880
|
lines.push(`| ${name} | \`${relativePath}\` |`);
|
|
10729
11881
|
}
|
|
10730
11882
|
lines.push("");
|
|
@@ -10734,10 +11886,10 @@ async function getProjectInfoResource(config) {
|
|
|
10734
11886
|
cwd: structure.migrations,
|
|
10735
11887
|
ignore: ["*.Designer.cs"]
|
|
10736
11888
|
});
|
|
10737
|
-
const migrations = migrationFiles.map((f) =>
|
|
11889
|
+
const migrations = migrationFiles.map((f) => path22.basename(f)).filter((f) => !f.includes("ModelSnapshot") && !f.includes(".Designer.")).sort();
|
|
10738
11890
|
lines.push("## EF Core Migrations");
|
|
10739
11891
|
lines.push("");
|
|
10740
|
-
lines.push(`**Location**: \`${
|
|
11892
|
+
lines.push(`**Location**: \`${path22.relative(projectPath, structure.migrations)}\``);
|
|
10741
11893
|
lines.push(`**Total Migrations**: ${migrations.length}`);
|
|
10742
11894
|
lines.push("");
|
|
10743
11895
|
if (migrations.length > 0) {
|
|
@@ -10772,11 +11924,11 @@ async function getProjectInfoResource(config) {
|
|
|
10772
11924
|
lines.push("dotnet build");
|
|
10773
11925
|
lines.push("");
|
|
10774
11926
|
lines.push("# Run API");
|
|
10775
|
-
lines.push(`cd ${structure.api ?
|
|
11927
|
+
lines.push(`cd ${structure.api ? path22.relative(projectPath, structure.api) : "src/Api"}`);
|
|
10776
11928
|
lines.push("dotnet run");
|
|
10777
11929
|
lines.push("");
|
|
10778
11930
|
lines.push("# Run frontend");
|
|
10779
|
-
lines.push(`cd ${structure.web ?
|
|
11931
|
+
lines.push(`cd ${structure.web ? path22.relative(projectPath, structure.web) : "web"}`);
|
|
10780
11932
|
lines.push("npm run dev");
|
|
10781
11933
|
lines.push("");
|
|
10782
11934
|
lines.push("# Create migration");
|
|
@@ -10799,7 +11951,7 @@ async function getProjectInfoResource(config) {
|
|
|
10799
11951
|
}
|
|
10800
11952
|
|
|
10801
11953
|
// src/resources/api-endpoints.ts
|
|
10802
|
-
import
|
|
11954
|
+
import path23 from "path";
|
|
10803
11955
|
var apiEndpointsResourceTemplate = {
|
|
10804
11956
|
uri: "smartstack://api/",
|
|
10805
11957
|
name: "SmartStack API Endpoints",
|
|
@@ -10824,7 +11976,7 @@ async function getApiEndpointsResource(config, endpointFilter) {
|
|
|
10824
11976
|
}
|
|
10825
11977
|
async function parseController(filePath, _rootPath) {
|
|
10826
11978
|
const content = await readText(filePath);
|
|
10827
|
-
const fileName =
|
|
11979
|
+
const fileName = path23.basename(filePath, ".cs");
|
|
10828
11980
|
const controllerName = fileName.replace("Controller", "");
|
|
10829
11981
|
const endpoints = [];
|
|
10830
11982
|
const routeMatch = content.match(/\[Route\s*\(\s*"([^"]+)"\s*\)\]/);
|
|
@@ -10971,7 +12123,7 @@ function getMethodEmoji(method) {
|
|
|
10971
12123
|
}
|
|
10972
12124
|
|
|
10973
12125
|
// src/resources/db-schema.ts
|
|
10974
|
-
import
|
|
12126
|
+
import path24 from "path";
|
|
10975
12127
|
var dbSchemaResourceTemplate = {
|
|
10976
12128
|
uri: "smartstack://schema/",
|
|
10977
12129
|
name: "SmartStack Database Schema",
|
|
@@ -11061,7 +12213,7 @@ async function parseEntity(filePath, rootPath, _config) {
|
|
|
11061
12213
|
tableName,
|
|
11062
12214
|
properties,
|
|
11063
12215
|
relationships,
|
|
11064
|
-
file:
|
|
12216
|
+
file: path24.relative(rootPath, filePath)
|
|
11065
12217
|
};
|
|
11066
12218
|
}
|
|
11067
12219
|
async function enrichFromConfigurations(entities, infrastructurePath, _config) {
|
|
@@ -11207,7 +12359,7 @@ function formatSchema(entities, filter, _config) {
|
|
|
11207
12359
|
}
|
|
11208
12360
|
|
|
11209
12361
|
// src/resources/entities.ts
|
|
11210
|
-
import
|
|
12362
|
+
import path25 from "path";
|
|
11211
12363
|
var entitiesResourceTemplate = {
|
|
11212
12364
|
uri: "smartstack://entities/",
|
|
11213
12365
|
name: "SmartStack Entities",
|
|
@@ -11267,7 +12419,7 @@ async function parseEntitySummary(filePath, rootPath, config) {
|
|
|
11267
12419
|
hasSoftDelete,
|
|
11268
12420
|
hasRowVersion,
|
|
11269
12421
|
file: filePath,
|
|
11270
|
-
relativePath:
|
|
12422
|
+
relativePath: path25.relative(rootPath, filePath)
|
|
11271
12423
|
};
|
|
11272
12424
|
}
|
|
11273
12425
|
function inferTableInfo(entityName, config) {
|
|
@@ -11467,7 +12619,10 @@ async function createServer() {
|
|
|
11467
12619
|
validateFrontendRoutesTool,
|
|
11468
12620
|
// Frontend Extension Tools
|
|
11469
12621
|
scaffoldFrontendExtensionTool,
|
|
11470
|
-
analyzeExtensionPointsTool
|
|
12622
|
+
analyzeExtensionPointsTool,
|
|
12623
|
+
// Security & Code Quality Tools
|
|
12624
|
+
validateSecurityTool,
|
|
12625
|
+
analyzeCodeQualityTool
|
|
11471
12626
|
]
|
|
11472
12627
|
};
|
|
11473
12628
|
});
|
|
@@ -11526,6 +12681,13 @@ async function createServer() {
|
|
|
11526
12681
|
case "analyze_extension_points":
|
|
11527
12682
|
result = await handleAnalyzeExtensionPoints(args ?? {}, config);
|
|
11528
12683
|
break;
|
|
12684
|
+
// Security & Code Quality Tools
|
|
12685
|
+
case "validate_security":
|
|
12686
|
+
result = await handleValidateSecurity(args ?? {}, config);
|
|
12687
|
+
break;
|
|
12688
|
+
case "analyze_code_quality":
|
|
12689
|
+
result = await handleAnalyzeCodeQuality(args ?? {}, config);
|
|
12690
|
+
break;
|
|
11529
12691
|
default:
|
|
11530
12692
|
throw new Error(`Unknown tool: ${name}`);
|
|
11531
12693
|
}
|