@atlashub/smartstack-mcp 1.10.0 → 1.14.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
@@ -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"),
@@ -914,7 +914,7 @@ var validateConventionsTool = {
914
914
  type: "array",
915
915
  items: {
916
916
  type: "string",
917
- enum: ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "all"]
917
+ enum: ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs", "all"]
918
918
  },
919
919
  description: "Types of checks to perform",
920
920
  default: ["all"]
@@ -925,7 +925,7 @@ var validateConventionsTool = {
925
925
  async function handleValidateConventions(args, config) {
926
926
  const input = ValidateConventionsInputSchema.parse(args);
927
927
  const projectPath = input.path || config.smartstack.projectPath;
928
- const checks = input.checks.includes("all") ? ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers"] : input.checks;
928
+ const checks = input.checks.includes("all") ? ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs"] : input.checks;
929
929
  logger.info("Validating conventions", { projectPath, checks });
930
930
  const result = {
931
931
  valid: true,
@@ -955,6 +955,12 @@ async function handleValidateConventions(args, config) {
955
955
  if (checks.includes("controllers")) {
956
956
  await validateControllerRoutes(structure, config, result);
957
957
  }
958
+ if (checks.includes("layouts")) {
959
+ await validateLayouts(structure, config, result);
960
+ }
961
+ if (checks.includes("tabs")) {
962
+ await validateTabs(structure, config, result);
963
+ }
958
964
  result.valid = result.errors.length === 0;
959
965
  result.summary = generateSummary(result, checks);
960
966
  return formatResult(result);
@@ -1385,6 +1391,132 @@ async function validateControllerRoutes(structure, _config, result) {
1385
1391
  });
1386
1392
  }
1387
1393
  }
1394
+ async function validateLayouts(structure, _config, result) {
1395
+ if (!structure.web) {
1396
+ result.warnings.push({
1397
+ type: "warning",
1398
+ category: "layouts",
1399
+ message: "Web project not found, skipping layout validation"
1400
+ });
1401
+ return;
1402
+ }
1403
+ const layoutFiles = await findFiles("**/layouts/**/*.tsx", { cwd: structure.web });
1404
+ if (layoutFiles.length === 0) {
1405
+ result.warnings.push({
1406
+ type: "warning",
1407
+ category: "layouts",
1408
+ message: "No layout files found in web/src/layouts/"
1409
+ });
1410
+ return;
1411
+ }
1412
+ for (const file of layoutFiles) {
1413
+ const content = await readText(file);
1414
+ const fileName = path6.basename(file);
1415
+ const lines = content.split("\n");
1416
+ let hasMaxWidth = false;
1417
+ for (const line of lines) {
1418
+ if (line.trim().startsWith("//") || line.trim().startsWith("*")) continue;
1419
+ if (/className.*max-w-/.test(line)) {
1420
+ hasMaxWidth = true;
1421
+ break;
1422
+ }
1423
+ }
1424
+ if (hasMaxWidth) {
1425
+ result.errors.push({
1426
+ type: "error",
1427
+ category: "layouts",
1428
+ message: `Layout "${fileName}" uses max-w-* constraint which limits content width`,
1429
+ file: path6.relative(structure.root, file),
1430
+ suggestion: "Remove max-w-* class. Content should occupy full available width."
1431
+ });
1432
+ }
1433
+ let hasStandardPadding = false;
1434
+ for (const line of lines) {
1435
+ if (line.trim().startsWith("//") || line.trim().startsWith("*")) continue;
1436
+ if (/className.*lg:px-10/.test(line)) {
1437
+ hasStandardPadding = true;
1438
+ break;
1439
+ }
1440
+ }
1441
+ if (!hasStandardPadding) {
1442
+ result.warnings.push({
1443
+ type: "warning",
1444
+ category: "layouts",
1445
+ message: `Layout "${fileName}" may be missing standard horizontal padding`,
1446
+ file: path6.relative(structure.root, file),
1447
+ suggestion: "Use lg:px-10 for consistent horizontal padding across layouts"
1448
+ });
1449
+ }
1450
+ let hasScrollPattern = false;
1451
+ for (const line of lines) {
1452
+ if (line.trim().startsWith("//") || line.trim().startsWith("*")) continue;
1453
+ if (/className.*h-full/.test(line) && /className.*overflow-auto/.test(line)) {
1454
+ hasScrollPattern = true;
1455
+ break;
1456
+ }
1457
+ }
1458
+ if (!hasScrollPattern) {
1459
+ result.warnings.push({
1460
+ type: "warning",
1461
+ category: "layouts",
1462
+ message: `Layout "${fileName}" may be missing scroll container pattern`,
1463
+ file: path6.relative(structure.root, file),
1464
+ suggestion: "Use h-full overflow-auto for proper internal scrolling"
1465
+ });
1466
+ }
1467
+ }
1468
+ }
1469
+ async function validateTabs(structure, _config, result) {
1470
+ if (!structure.web) {
1471
+ result.warnings.push({
1472
+ type: "warning",
1473
+ category: "tabs",
1474
+ message: "Web project not found, skipping tab validation"
1475
+ });
1476
+ return;
1477
+ }
1478
+ const pageFiles = await findFiles("**/pages/**/*.tsx", { cwd: structure.web });
1479
+ let tabPagesCount = 0;
1480
+ let hookUsageCount = 0;
1481
+ for (const file of pageFiles) {
1482
+ const content = await readText(file);
1483
+ const fileName = path6.basename(file);
1484
+ const lines = content.split("\n");
1485
+ let hasTabPattern = false;
1486
+ for (const line of lines) {
1487
+ const trimmed = line.trim();
1488
+ if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*") || trimmed === "") {
1489
+ continue;
1490
+ }
1491
+ if (/<Tabs|<TabList|activeTab\s*[,=})]|setActiveTab\s*[({]/.test(line)) {
1492
+ hasTabPattern = true;
1493
+ break;
1494
+ }
1495
+ }
1496
+ if (hasTabPattern) {
1497
+ tabPagesCount++;
1498
+ const usesHook = content.includes("useTabNavigation");
1499
+ if (usesHook) {
1500
+ hookUsageCount++;
1501
+ } else {
1502
+ result.errors.push({
1503
+ type: "error",
1504
+ category: "tabs",
1505
+ message: `Page "${fileName}" has tabs but doesn't use useTabNavigation hook`,
1506
+ file: path6.relative(structure.root, file),
1507
+ suggestion: "Use useTabNavigation hook to sync tab state with URL: const { activeTab, setActiveTab } = useTabNavigation(defaultTab, VALID_TABS)"
1508
+ });
1509
+ }
1510
+ }
1511
+ }
1512
+ if (tabPagesCount > 0) {
1513
+ result.warnings.push({
1514
+ type: "warning",
1515
+ category: "tabs",
1516
+ message: `Tab summary: ${hookUsageCount}/${tabPagesCount} pages with tabs use useTabNavigation hook`
1517
+ });
1518
+ }
1519
+ }
1388
1520
  function generateSummary(result, checks) {
1389
1521
  const parts = [];
1390
1522
  parts.push(`Checks performed: ${checks.join(", ")}`);
@@ -1822,6 +1954,10 @@ var scaffoldExtensionTool = {
1822
1954
  type: "boolean",
1823
1955
  description: "For feature type: generate repository pattern"
1824
1956
  },
1957
+ withSeedData: {
1958
+ type: "boolean",
1959
+ description: "For entity type: generate centralized SeedData file in Seeding/Data/{Domain}/"
1960
+ },
1825
1961
  entityProperties: {
1826
1962
  type: "array",
1827
1963
  items: {
@@ -2346,6 +2482,119 @@ public class {{name}}Configuration : IEntityTypeConfiguration<{{name}}>
2346
2482
  result.instructions.push("- CreatedAt, UpdatedAt, CreatedBy, UpdatedBy (audit)");
2347
2483
  result.instructions.push("- IsDeleted, DeletedAt, DeletedBy (soft delete)");
2348
2484
  result.instructions.push("- RowVersion (concurrency)");
2485
+ if (options?.withSeedData) {
2486
+ result.instructions.push("");
2487
+ result.instructions.push("### Seed Data");
2488
+ await scaffoldSeedData(name, options, structure, config, result, dryRun);
2489
+ }
2490
+ }
2491
+ async function scaffoldSeedData(name, options, structure, config, result, dryRun = false) {
2492
+ const tablePrefix = options?.tablePrefix || "ref_";
2493
+ const domainMap = {
2494
+ "auth_": "Authorization",
2495
+ "nav_": "Navigation",
2496
+ "usr_": "User",
2497
+ "wkf_": "Communications",
2498
+ "cfg_": "Configuration",
2499
+ "ai_": "AI",
2500
+ "entra_": "Entra",
2501
+ "ref_": "Reference",
2502
+ "support_": "Support",
2503
+ "loc_": "Localization"
2504
+ };
2505
+ const domain = options?.seedDataDomain || domainMap[tablePrefix] || "Reference";
2506
+ const isSystemEntity = options?.isSystemEntity || false;
2507
+ const seedDataTemplate = `using SmartStack.Domain.{{domainNamespace}};
2508
+
2509
+ namespace SmartStack.Infrastructure.Persistence.Seeding.Data.{{domain}};
2510
+
2511
+ /// <summary>
2512
+ /// Donnees seed pour {{name}}.
2513
+ /// Centralise les IDs et donnees d'initialisation.
2514
+ /// </summary>
2515
+ public static class {{name}}SeedData
2516
+ {
2517
+ // ============================================================
2518
+ // IDs - Documenter chaque ID avec son role
2519
+ // ============================================================
2520
+
2521
+ /// <summary>ID exemple - A remplacer par vos IDs</summary>
2522
+ public static readonly Guid ExampleId = Guid.Parse("{{exampleGuid}}");
2523
+
2524
+ // ============================================================
2525
+ // CODES / CONSTANTS
2526
+ // ============================================================
2527
+
2528
+ public const string ExampleCode = "example";
2529
+
2530
+ // ============================================================
2531
+ // SEED DATA
2532
+ // ============================================================
2533
+
2534
+ /// <summary>
2535
+ /// Retourne toutes les donnees seed pour {{name}}.
2536
+ /// Appel\xE9 depuis {{name}}Configuration.HasData()
2537
+ /// </summary>
2538
+ public static object[] GetSeedData()
2539
+ {
2540
+ var seedDate = SeedConstants.SeedDate;
2541
+
2542
+ return new object[]
2543
+ {
2544
+ // Exemple - A remplacer par vos donnees
2545
+ new
2546
+ {
2547
+ Id = ExampleId,
2548
+ {{#unless isSystemEntity}}
2549
+ TenantId = (Guid?)null, // Seed data systeme sans tenant
2550
+ {{/unless}}
2551
+ Code = ExampleCode,
2552
+ // TODO: Ajouter les proprietes specifiques
2553
+ IsDeleted = false,
2554
+ CreatedAt = seedDate
2555
+ }
2556
+ };
2557
+ }
2558
+ }
2559
+ `;
2560
+ const exampleGuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
2561
+ const r = Math.random() * 16 | 0;
2562
+ const v = c === "x" ? r : r & 3 | 8;
2563
+ return v.toString(16);
2564
+ });
2565
+ const context = {
2566
+ name,
2567
+ domain,
2568
+ domainNamespace: domain,
2569
+ isSystemEntity,
2570
+ exampleGuid
2571
+ };
2572
+ const seedDataContent = Handlebars.compile(seedDataTemplate)(context);
2573
+ const projectRoot = config.smartstack.projectPath;
2574
+ const infraPath = structure.infrastructure || path8.join(projectRoot, "Infrastructure");
2575
+ const seedDataPath = path8.join(infraPath, "Persistence", "Seeding", "Data", domain);
2576
+ const seedDataFilePath = path8.join(seedDataPath, `${name}SeedData.cs`);
2577
+ validatePathSecurity(seedDataFilePath, projectRoot);
2578
+ if (!dryRun) {
2579
+ await ensureDirectory(seedDataPath);
2580
+ await writeText(seedDataFilePath, seedDataContent);
2581
+ }
2582
+ result.files.push({ path: seedDataFilePath, content: seedDataContent, type: "created" });
2583
+ result.instructions.push(`SeedData file generated in Seeding/Data/${domain}/`);
2584
+ result.instructions.push("");
2585
+ result.instructions.push("Update your Configuration to use centralized SeedData:");
2586
+ result.instructions.push("```csharp");
2587
+ result.instructions.push(`// In ${name}Configuration.cs`);
2588
+ result.instructions.push(`using SmartStack.Infrastructure.Persistence.Seeding.Data.${domain};`);
2589
+ result.instructions.push("");
2590
+ result.instructions.push(`builder.HasData(${name}SeedData.GetSeedData());`);
2591
+ result.instructions.push("```");
2592
+ result.instructions.push("");
2593
+ result.instructions.push("Pattern to follow:");
2594
+ result.instructions.push("1. Define public static readonly Guid IDs");
2595
+ result.instructions.push("2. Define const string codes");
2596
+ result.instructions.push("3. Implement GetSeedData() returning object[]");
2597
+ result.instructions.push("4. Reference other SeedData IDs for foreign keys");
2349
2598
  }
2350
2599
  async function scaffoldController(name, options, structure, config, result, dryRun = false) {
2351
2600
  const namespace = options?.namespace || `${config.conventions.namespaces.api}.Controllers`;
@@ -3658,7 +3907,7 @@ async function handleSuggestMigration(args, config) {
3658
3907
  async function findExistingMigrations(structure, config, context) {
3659
3908
  const migrations = [];
3660
3909
  const infraPath = structure.infrastructure || path10.join(config.smartstack.projectPath, "Infrastructure");
3661
- const migrationsPath = path10.join(infraPath, "Migrations");
3910
+ const migrationsPath = path10.join(infraPath, "Persistence", "Migrations");
3662
3911
  try {
3663
3912
  const migrationFiles = await findFiles("*.cs", { cwd: migrationsPath });
3664
3913
  const migrationPattern = /^(\w+)_v(\d+\.\d+\.\d+)_(\d+)_(\w+)\.cs$/;
@@ -3705,7 +3954,6 @@ function compareVersions2(a, b) {
3705
3954
  }
3706
3955
 
3707
3956
  // src/tools/generate-permissions.ts
3708
- import Handlebars2 from "handlebars";
3709
3957
  import path11 from "path";
3710
3958
  var HTTP_METHOD_TO_ACTION = {
3711
3959
  "GET": "read",
@@ -3722,17 +3970,21 @@ var generatePermissionsTool = {
3722
3970
  name: "generate_permissions",
3723
3971
  description: `Generate RBAC permissions for SmartStack controllers.
3724
3972
 
3725
- Automatically creates permissions following the convention: {navRoute}.{action}
3973
+ Analyzes NavRoute attributes and outputs HasData() C# code to add to PermissionConfiguration.cs.
3974
+
3975
+ IMPORTANT: This tool does NOT generate migrations with raw SQL (forbidden by SmartStack conventions).
3976
+ Instead, it outputs HasData() code that must be manually added to the Configuration file.
3726
3977
 
3727
3978
  Example:
3728
3979
  navRoute: "platform.administration.entra"
3729
- Generates:
3980
+ Outputs HasData() code for:
3981
+ - platform.administration.entra.*
3730
3982
  - platform.administration.entra.read
3731
3983
  - platform.administration.entra.create
3732
3984
  - platform.administration.entra.update
3733
3985
  - platform.administration.entra.delete
3734
3986
 
3735
- Can also generate EF Core migration to seed permissions in database.`,
3987
+ After adding to PermissionConfiguration.cs, run: dotnet ef migrations add <MigrationName>`,
3736
3988
  inputSchema: {
3737
3989
  type: "object",
3738
3990
  properties: {
@@ -3750,15 +4002,10 @@ Can also generate EF Core migration to seed permissions in database.`,
3750
4002
  default: true,
3751
4003
  description: "Include standard CRUD actions (read, create, update, delete)"
3752
4004
  },
3753
- generateMigration: {
4005
+ includeWildcard: {
3754
4006
  type: "boolean",
3755
4007
  default: true,
3756
- description: "Generate EF Core migration to seed permissions in database"
3757
- },
3758
- dryRun: {
3759
- type: "boolean",
3760
- default: false,
3761
- description: "Preview without writing files or creating migration"
4008
+ description: "Include wildcard permission (e.g., personal.myspace.tenants.*)"
3762
4009
  }
3763
4010
  }
3764
4011
  }
@@ -3769,8 +4016,7 @@ async function handleGeneratePermissions(args, config) {
3769
4016
  navRoute: args.navRoute,
3770
4017
  actions: args.actions || [],
3771
4018
  includeStandardActions: args.includeStandardActions !== false,
3772
- generateMigration: args.generateMigration !== false,
3773
- dryRun: args.dryRun === true
4019
+ includeWildcard: args.includeWildcard !== false
3774
4020
  };
3775
4021
  const structure = await findSmartStackStructure(config.smartstack.projectPath);
3776
4022
  if (!structure) {
@@ -3782,16 +4028,17 @@ async function handleGeneratePermissions(args, config) {
3782
4028
  permissions = generatePermissionsForNavRoute(
3783
4029
  options.navRoute,
3784
4030
  options.actions || [],
3785
- options.includeStandardActions || false
4031
+ options.includeStandardActions || false,
4032
+ options.includeWildcard || false
3786
4033
  );
3787
- report = `## Permissions Generated for NavRoute: ${options.navRoute}
4034
+ report = `## Permissions for NavRoute: ${options.navRoute}
3788
4035
 
3789
4036
  `;
3790
4037
  report += formatPermissionsReport(permissions);
3791
4038
  } else {
3792
4039
  const scannedPermissions = await scanControllersForPermissions(structure.api || structure.root);
3793
4040
  permissions = scannedPermissions;
3794
- report = `## Permissions Generated from All Controllers
4041
+ report = `## Permissions from All Controllers
3795
4042
 
3796
4043
  `;
3797
4044
  report += `Total controllers scanned: ${getUniqueNavRouteCount(permissions)}
@@ -3801,53 +4048,51 @@ async function handleGeneratePermissions(args, config) {
3801
4048
  `;
3802
4049
  report += formatPermissionsReport(permissions);
3803
4050
  }
3804
- if (options.generateMigration && !options.dryRun) {
3805
- const migrationResult = await generatePermissionMigration(
3806
- structure.api || structure.root,
3807
- permissions,
3808
- config
3809
- );
3810
- report += `
4051
+ report += `
3811
4052
 
3812
- ## Migration Generated
4053
+ ## HasData() Code for PermissionConfiguration.cs
3813
4054
 
3814
4055
  `;
3815
- report += `File: ${migrationResult.filePath}
4056
+ report += `\u26A0\uFE0F **IMPORTANT**: Do NOT use migrationBuilder.Sql() - it's forbidden by SmartStack conventions.
3816
4057
  `;
3817
- report += `Migration name: ${migrationResult.migrationName}
4058
+ report += `Add this code to \`PermissionConfiguration.cs\` in the HasData() section, then create a migration.
3818
4059
 
3819
4060
  `;
3820
- report += `### Next Steps
4061
+ report += generateHasDataCode(permissions, options.navRoute || "custom");
4062
+ report += `
4063
+
4064
+ ### Next Steps
3821
4065
 
3822
4066
  `;
3823
- report += `1. Review the generated migration
3824
- `;
3825
- report += `2. Run: \`dotnet ef database update\`
4067
+ report += `1. Add the module ID variable in GetSeedData() method
3826
4068
  `;
3827
- report += `3. Verify permissions in auth_Permissions table
4069
+ report += `2. Add the HasData entries to the return array
3828
4070
  `;
3829
- } else if (options.dryRun) {
3830
- report += `
3831
-
3832
- ## Dry Run Mode
3833
-
4071
+ report += `3. Run: \`dotnet ef migrations add <MigrationName> -o Persistence/Migrations\`
3834
4072
  `;
3835
- report += `No files were created. Remove 'dryRun: true' to generate migration.
4073
+ report += `4. Run: \`dotnet ef database update\`
3836
4074
  `;
3837
- }
3838
4075
  return report;
3839
4076
  } catch (error) {
3840
4077
  logger.error("Error generating permissions:", error);
3841
4078
  throw error;
3842
4079
  }
3843
4080
  }
3844
- function generatePermissionsForNavRoute(navRoute, customActions, includeStandardActions) {
4081
+ function generatePermissionsForNavRoute(navRoute, customActions, includeStandardActions, includeWildcard = true) {
3845
4082
  const permissions = [];
3846
4083
  const parts = navRoute.split(".");
3847
4084
  const context = parts[0];
3848
4085
  if (parts.length < 3) {
3849
4086
  throw new Error(`Invalid NavRoute format: ${navRoute}. Expected format: context.application.module`);
3850
4087
  }
4088
+ if (includeWildcard) {
4089
+ permissions.push({
4090
+ code: `${navRoute}.*`,
4091
+ name: formatPermissionName(navRoute, "Full Access"),
4092
+ description: `Full ${parts[parts.length - 1]} management`,
4093
+ category: context
4094
+ });
4095
+ }
3851
4096
  const actions = includeStandardActions ? [...STANDARD_ACTIONS, ...customActions] : customActions;
3852
4097
  for (const action of actions) {
3853
4098
  const code = `${navRoute}.${action}`;
@@ -3986,87 +4231,54 @@ function getUniqueNavRouteCount(permissions) {
3986
4231
  );
3987
4232
  return navRoutes.size;
3988
4233
  }
3989
- async function generatePermissionMigration(backendRoot, permissions, config) {
3990
- const templatePath = path11.join(
3991
- process.cwd(),
3992
- "templates",
3993
- "migrations",
3994
- "seed-permissions.cs.hbs"
3995
- );
3996
- let template;
3997
- try {
3998
- template = await readText(templatePath);
3999
- } catch {
4000
- template = getSeedPermissionsTemplate();
4001
- }
4002
- const handlebars = Handlebars2.create();
4003
- const compiled = handlebars.compile(template);
4004
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T.]/g, "").slice(0, 14);
4005
- const migrationName = `SeedPermissions_${timestamp}`;
4006
- const content = compiled({
4007
- migrationName,
4008
- permissions,
4009
- timestamp
4010
- });
4011
- const migrationsPath = path11.join(
4012
- backendRoot,
4013
- "Infrastructure",
4014
- "Data",
4015
- "Migrations",
4016
- "Core"
4017
- );
4018
- await ensureDirectory(migrationsPath);
4019
- const filePath = path11.join(migrationsPath, `${migrationName}.cs`);
4020
- validatePathSecurity(filePath, config.smartstack.projectPath);
4021
- await writeText(filePath, content);
4022
- logger.info(`Generated permission migration: ${filePath}`);
4023
- return {
4024
- filePath,
4025
- migrationName
4026
- };
4027
- }
4028
- function getSeedPermissionsTemplate() {
4029
- return `using Microsoft.EntityFrameworkCore.Migrations;
4030
-
4031
- namespace SmartStack.Infrastructure.Data.Migrations.Core;
4234
+ function generateHasDataCode(permissions, navRoute) {
4235
+ const parts = navRoute.split(".");
4236
+ const moduleName = parts.length >= 3 ? parts[parts.length - 1] : "custom";
4237
+ const moduleVarName = `${moduleName}ModuleId`;
4238
+ let code = "```csharp\n";
4239
+ code += `// 1. Add module ID variable (get from NavigationModuleSeedData.cs)
4240
+ `;
4241
+ code += `var ${moduleVarName} = Guid.Parse("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"); // TODO: Replace with actual ID
4032
4242
 
4033
- /// <summary>
4034
- /// Seed permissions generated from NavRoute controllers
4035
- /// Generated on {{timestamp}}
4036
- /// </summary>
4037
- public partial class {{migrationName}} : Migration
4038
- {
4039
- protected override void Up(MigrationBuilder migrationBuilder)
4040
- {
4041
- // Insert permissions
4042
- {{#each permissions}}
4043
- migrationBuilder.Sql(@"
4044
- IF NOT EXISTS (SELECT 1 FROM core.auth_Permissions WHERE Code = '{{code}}')
4045
- BEGIN
4046
- INSERT INTO core.auth_Permissions (Id, Code, Name, Description, IsDeleted, CreatedAt, UpdatedAt)
4047
- VALUES (
4048
- NEWID(),
4049
- '{{code}}',
4050
- '{{name}}',
4051
- '{{description}}',
4052
- 0,
4053
- GETUTCDATE(),
4054
- GETUTCDATE()
4055
- );
4056
- END
4057
- ");
4058
- {{/each}}
4059
- }
4060
-
4061
- protected override void Down(MigrationBuilder migrationBuilder)
4062
- {
4063
- // Remove seeded permissions
4064
- {{#each permissions}}
4065
- migrationBuilder.Sql(@"DELETE FROM core.auth_Permissions WHERE Code = '{{code}}'");
4066
- {{/each}}
4243
+ `;
4244
+ code += `// 2. Add these entries to the HasData() return array:
4245
+ `;
4246
+ for (const perm of permissions) {
4247
+ const isWildcard = perm.code.endsWith(".*");
4248
+ const action = perm.code.split(".").pop();
4249
+ const guidPlaceholder = generatePlaceholderGuid();
4250
+ if (isWildcard) {
4251
+ code += `new { Id = Guid.Parse("${guidPlaceholder}"), Path = "${perm.code}", Level = PermissionLevel.Module, IsWildcard = true, ModuleId = ${moduleVarName}, Description = "${perm.description}", CreatedAt = seedDate },
4252
+ `;
4253
+ } else {
4254
+ const actionEnum = getActionEnum(action || "read");
4255
+ code += `new { Id = Guid.Parse("${guidPlaceholder}"), Path = "${perm.code}", Level = PermissionLevel.Module, Action = PermissionAction.${actionEnum}, IsWildcard = false, ModuleId = ${moduleVarName}, Description = "${perm.description}", CreatedAt = seedDate },
4256
+ `;
4067
4257
  }
4068
- }
4258
+ }
4259
+ code += "```\n";
4260
+ code += "\n\u26A0\uFE0F **IMPORTANT**: Replace all GUIDs with randomly generated ones using:\n";
4261
+ code += "```powershell\n";
4262
+ code += `1..${permissions.length} | ForEach-Object { [guid]::NewGuid().ToString() }
4069
4263
  `;
4264
+ code += "```\n";
4265
+ return code;
4266
+ }
4267
+ function generatePlaceholderGuid() {
4268
+ return "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX";
4269
+ }
4270
+ function getActionEnum(action) {
4271
+ const actionMap = {
4272
+ "read": "Read",
4273
+ "create": "Create",
4274
+ "update": "Update",
4275
+ "delete": "Delete",
4276
+ "assign": "Assign",
4277
+ "execute": "Execute",
4278
+ "export": "Export",
4279
+ "import": "Import"
4280
+ };
4281
+ return actionMap[action.toLowerCase()] || action.charAt(0).toUpperCase() + action.slice(1);
4070
4282
  }
4071
4283
  async function readDirectoryRecursive(dir) {
4072
4284
  const files = [];
@@ -4086,7 +4298,7 @@ async function readDirectoryRecursive(dir) {
4086
4298
  }
4087
4299
 
4088
4300
  // src/tools/scaffold-tests.ts
4089
- import Handlebars3 from "handlebars";
4301
+ import Handlebars2 from "handlebars";
4090
4302
  import path12 from "path";
4091
4303
  var scaffoldTestsTool = {
4092
4304
  name: "scaffold_tests",
@@ -4166,15 +4378,15 @@ var scaffoldTestsTool = {
4166
4378
  required: ["target", "name"]
4167
4379
  }
4168
4380
  };
4169
- Handlebars3.registerHelper("pascalCase", (str) => {
4381
+ Handlebars2.registerHelper("pascalCase", (str) => {
4170
4382
  if (!str) return "";
4171
4383
  return str.charAt(0).toUpperCase() + str.slice(1);
4172
4384
  });
4173
- Handlebars3.registerHelper("camelCase", (str) => {
4385
+ Handlebars2.registerHelper("camelCase", (str) => {
4174
4386
  if (!str) return "";
4175
4387
  return str.charAt(0).toLowerCase() + str.slice(1);
4176
4388
  });
4177
- Handlebars3.registerHelper("unless", function(conditional, options) {
4389
+ Handlebars2.registerHelper("unless", function(conditional, options) {
4178
4390
  if (!conditional) {
4179
4391
  return options.fn(this);
4180
4392
  }
@@ -5516,7 +5728,7 @@ async function scaffoldEntityTests(name, options, testTypes, structure, config,
5516
5728
  ...options
5517
5729
  };
5518
5730
  if (testTypes.includes("unit")) {
5519
- const content = Handlebars3.compile(entityTestTemplate)(context);
5731
+ const content = Handlebars2.compile(entityTestTemplate)(context);
5520
5732
  const testPath = path12.join(structure.root, "Tests", "Unit", "Domain", `${name}Tests.cs`);
5521
5733
  validatePathSecurity(testPath, structure.root);
5522
5734
  if (!dryRun) {
@@ -5530,7 +5742,7 @@ async function scaffoldEntityTests(name, options, testTypes, structure, config,
5530
5742
  });
5531
5743
  }
5532
5744
  if (testTypes.includes("security")) {
5533
- const securityContent = Handlebars3.compile(securityTestTemplate)({
5745
+ const securityContent = Handlebars2.compile(securityTestTemplate)({
5534
5746
  ...context,
5535
5747
  nameLower: name.charAt(0).toLowerCase() + name.slice(1),
5536
5748
  apiNamespace: config.conventions.namespaces.api
@@ -5562,7 +5774,7 @@ async function scaffoldServiceTests(name, options, testTypes, structure, config,
5562
5774
  ...options
5563
5775
  };
5564
5776
  if (testTypes.includes("unit")) {
5565
- const content = Handlebars3.compile(serviceTestTemplate)(context);
5777
+ const content = Handlebars2.compile(serviceTestTemplate)(context);
5566
5778
  const testPath = path12.join(structure.root, "Tests", "Unit", "Services", `${name}ServiceTests.cs`);
5567
5779
  validatePathSecurity(testPath, structure.root);
5568
5780
  if (!dryRun) {
@@ -5592,7 +5804,7 @@ async function scaffoldControllerTests(name, options, testTypes, structure, conf
5592
5804
  ...options
5593
5805
  };
5594
5806
  if (testTypes.includes("integration")) {
5595
- const content = Handlebars3.compile(controllerTestTemplate)(context);
5807
+ const content = Handlebars2.compile(controllerTestTemplate)(context);
5596
5808
  const testPath = path12.join(structure.root, "Tests", "Integration", "Controllers", `${name}ControllerTests.cs`);
5597
5809
  validatePathSecurity(testPath, structure.root);
5598
5810
  if (!dryRun) {
@@ -5606,7 +5818,7 @@ async function scaffoldControllerTests(name, options, testTypes, structure, conf
5606
5818
  });
5607
5819
  }
5608
5820
  if (testTypes.includes("security")) {
5609
- const securityContent = Handlebars3.compile(securityTestTemplate)(context);
5821
+ const securityContent = Handlebars2.compile(securityTestTemplate)(context);
5610
5822
  const securityPath = path12.join(structure.root, "Tests", "Security", `${name}SecurityTests.cs`);
5611
5823
  validatePathSecurity(securityPath, structure.root);
5612
5824
  if (!dryRun) {
@@ -5631,7 +5843,7 @@ async function scaffoldValidatorTests(name, options, testTypes, structure, confi
5631
5843
  ...options
5632
5844
  };
5633
5845
  if (testTypes.includes("unit")) {
5634
- const content = Handlebars3.compile(validatorTestTemplate)(context);
5846
+ const content = Handlebars2.compile(validatorTestTemplate)(context);
5635
5847
  const testPath = path12.join(structure.root, "Tests", "Unit", "Validators", `${name}ValidatorTests.cs`);
5636
5848
  validatePathSecurity(testPath, structure.root);
5637
5849
  if (!dryRun) {
@@ -5658,7 +5870,7 @@ async function scaffoldRepositoryTests(name, options, testTypes, structure, conf
5658
5870
  ...options
5659
5871
  };
5660
5872
  if (testTypes.includes("integration")) {
5661
- const content = Handlebars3.compile(repositoryTestTemplate)(context);
5873
+ const content = Handlebars2.compile(repositoryTestTemplate)(context);
5662
5874
  const testPath = path12.join(structure.root, "Tests", "Integration", "Repositories", `${name}RepositoryTests.cs`);
5663
5875
  validatePathSecurity(testPath, structure.root);
5664
5876
  if (!dryRun) {
@@ -10388,6 +10600,76 @@ All tenant-related tables use the \`tenant_\` prefix:
10388
10600
 
10389
10601
  ---
10390
10602
 
10603
+ ## 10. Frontend Conventions
10604
+
10605
+ ### Layout Standards
10606
+
10607
+ All context layouts MUST follow the AdminLayout pattern for consistent user experience.
10608
+
10609
+ #### Standard Structure
10610
+
10611
+ \`\`\`tsx
10612
+ <main className={\`pt-16 transition-all duration-300 \${
10613
+ showSidebar ? (isCollapsed ? 'lg:pl-16' : 'lg:pl-64') : ''
10614
+ }\`}>
10615
+ <div className="p-4 sm:p-6 lg:px-10 h-full overflow-auto">
10616
+ <Outlet />
10617
+ </div>
10618
+ </main>
10619
+ \`\`\`
10620
+
10621
+ #### Layout Rules
10622
+
10623
+ | Rule | Description |
10624
+ |------|-------------|
10625
+ | No \`max-w-*\` | Content MUST occupy full available width |
10626
+ | Horizontal padding | Use \`lg:px-10\` for consistent padding |
10627
+ | Scroll container | Use \`h-full overflow-auto\` for internal scrolling |
10628
+ | Sidebar conditional | \`showSidebar = !!currentAppCode\` |
10629
+
10630
+ ### Tab Navigation with URL
10631
+
10632
+ Pages with tabs MUST synchronize tab state with URL for shareable links and browser navigation.
10633
+
10634
+ #### useTabNavigation Hook
10635
+
10636
+ \`\`\`tsx
10637
+ import { useTabNavigation } from '@/hooks/useTabNavigation';
10638
+
10639
+ // 1. Define valid tabs
10640
+ const VALID_TABS = ['info', 'settings', 'permissions'] as const;
10641
+ type TabId = typeof VALID_TABS[number];
10642
+
10643
+ // 2. Use the hook
10644
+ const { activeTab, setActiveTab } = useTabNavigation<TabId>('info', VALID_TABS);
10645
+
10646
+ // 3. Use in JSX
10647
+ <button onClick={() => setActiveTab('settings')}>Settings</button>
10648
+ \`\`\`
10649
+
10650
+ #### URL Behavior
10651
+
10652
+ | URL | Active Tab |
10653
+ |-----|------------|
10654
+ | \`/page\` | Default tab (e.g., \`info\`) |
10655
+ | \`/page?tab=settings\` | \`settings\` |
10656
+ | \`/page?tab=invalid\` | Default tab (fallback) |
10657
+
10658
+ #### Hook Characteristics
10659
+
10660
+ - URL is clean for default tab (no \`?tab=\` parameter)
10661
+ - Invalid tabs fallback to default
10662
+ - Uses \`replace: true\` to avoid polluting browser history
10663
+
10664
+ ### Frontend Validation Checks
10665
+
10666
+ | Check | Description |
10667
+ |-------|-------------|
10668
+ | \`layouts\` | Verify no \`max-w-*\` in content wrapper, standard padding present |
10669
+ | \`tabs\` | Verify \`useTabNavigation\` hook usage for pages with tabs |
10670
+
10671
+ ---
10672
+
10391
10673
  ## Quick Reference
10392
10674
 
10393
10675
  | Category | Convention | Example |
@@ -10420,6 +10702,10 @@ All tenant-related tables use the \`tenant_\` prefix:
10420
10702
  | Validation | \`withValidation: true\` | FluentValidation validators |
10421
10703
  | Repository | \`withRepository: true\` | Interface + Implementation |
10422
10704
  | Migration naming | \`suggest_migration\` | {context}_v{version}_{seq}_{Desc} |
10705
+ | Layout wrapper | No \`max-w-*\` | Content fills available width |
10706
+ | Layout padding | \`lg:px-10\` | Standard horizontal padding |
10707
+ | Tab navigation | \`useTabNavigation\` hook | Sync tabs with URL |
10708
+ | Tab URL | \`?tab=name\` | Shareable deep links to tabs |
10423
10709
 
10424
10710
  ---
10425
10711