@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 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, path24, cause) {
85
+ constructor(message, operation, path26, cause) {
86
86
  super(message);
87
87
  this.operation = operation;
88
- this.path = path24;
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 path20 from "path";
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 ${path20.basename(structure.domain)}/ # Domain layer (entities)`);
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 ${path20.basename(structure.application)}/ # Application layer (services)`);
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 ${path20.basename(structure.infrastructure)}/ # Infrastructure (EF Core)`);
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 ${path20.basename(structure.api)}/ # API layer (controllers)`);
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 = path20.basename(csproj, ".csproj");
10727
- const relativePath = path20.relative(projectPath, csproj);
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) => path20.basename(f)).filter((f) => !f.includes("ModelSnapshot") && !f.includes(".Designer.")).sort();
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**: \`${path20.relative(projectPath, structure.migrations)}\``);
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 ? path20.relative(projectPath, structure.api) : "src/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 ? path20.relative(projectPath, structure.web) : "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 path21 from "path";
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 = path21.basename(filePath, ".cs");
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 path22 from "path";
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: path22.relative(rootPath, filePath)
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 path23 from "path";
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: path23.relative(rootPath, filePath)
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
  }