@atlashub/smartstack-cli 3.53.0 → 4.0.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.
@@ -471,8 +471,8 @@ var init_parseUtil = __esm({
471
471
  init_errors();
472
472
  init_en();
473
473
  makeIssue = (params) => {
474
- const { data, path: path30, errorMaps, issueData } = params;
475
- const fullPath = [...path30, ...issueData.path || []];
474
+ const { data, path: path32, errorMaps, issueData } = params;
475
+ const fullPath = [...path32, ...issueData.path || []];
476
476
  const fullIssue = {
477
477
  ...issueData,
478
478
  path: fullPath
@@ -786,11 +786,11 @@ var init_types = __esm({
786
786
  init_parseUtil();
787
787
  init_util();
788
788
  ParseInputLazyPath = class {
789
- constructor(parent, value, path30, key) {
789
+ constructor(parent, value, path32, key) {
790
790
  this._cachedPath = [];
791
791
  this.parent = parent;
792
792
  this.data = value;
793
- this._path = path30;
793
+ this._path = path32;
794
794
  this._key = key;
795
795
  }
796
796
  get path() {
@@ -4367,10 +4367,10 @@ function assignProp(target, prop, value) {
4367
4367
  configurable: true
4368
4368
  });
4369
4369
  }
4370
- function getElementAtPath(obj, path30) {
4371
- if (!path30)
4370
+ function getElementAtPath(obj, path32) {
4371
+ if (!path32)
4372
4372
  return obj;
4373
- return path30.reduce((acc, key) => acc?.[key], obj);
4373
+ return path32.reduce((acc, key) => acc?.[key], obj);
4374
4374
  }
4375
4375
  function promiseAllObject(promisesObj) {
4376
4376
  const keys = Object.keys(promisesObj);
@@ -4619,11 +4619,11 @@ function aborted(x, startIndex = 0) {
4619
4619
  }
4620
4620
  return false;
4621
4621
  }
4622
- function prefixIssues(path30, issues) {
4622
+ function prefixIssues(path32, issues) {
4623
4623
  return issues.map((iss) => {
4624
4624
  var _a;
4625
4625
  (_a = iss).path ?? (_a.path = []);
4626
- iss.path.unshift(path30);
4626
+ iss.path.unshift(path32);
4627
4627
  return iss;
4628
4628
  });
4629
4629
  }
@@ -14447,8 +14447,8 @@ var require_utils = __commonJS({
14447
14447
  }
14448
14448
  return ind;
14449
14449
  }
14450
- function removeDotSegments(path30) {
14451
- let input = path30;
14450
+ function removeDotSegments(path32) {
14451
+ let input = path32;
14452
14452
  const output = [];
14453
14453
  let nextSlash = -1;
14454
14454
  let len = 0;
@@ -14648,8 +14648,8 @@ var require_schemes = __commonJS({
14648
14648
  wsComponent.secure = void 0;
14649
14649
  }
14650
14650
  if (wsComponent.resourceName) {
14651
- const [path30, query] = wsComponent.resourceName.split("?");
14652
- wsComponent.path = path30 && path30 !== "/" ? path30 : void 0;
14651
+ const [path32, query] = wsComponent.resourceName.split("?");
14652
+ wsComponent.path = path32 && path32 !== "/" ? path32 : void 0;
14653
14653
  wsComponent.query = query;
14654
14654
  wsComponent.resourceName = void 0;
14655
14655
  }
@@ -23041,12 +23041,12 @@ var init_esm7 = __esm({
23041
23041
  /**
23042
23042
  * Get the Path object referenced by the string path, resolved from this Path
23043
23043
  */
23044
- resolve(path30) {
23045
- if (!path30) {
23044
+ resolve(path32) {
23045
+ if (!path32) {
23046
23046
  return this;
23047
23047
  }
23048
- const rootPath = this.getRootString(path30);
23049
- const dir = path30.substring(rootPath.length);
23048
+ const rootPath = this.getRootString(path32);
23049
+ const dir = path32.substring(rootPath.length);
23050
23050
  const dirParts = dir.split(this.splitSep);
23051
23051
  const result = rootPath ? this.getRoot(rootPath).#resolveParts(dirParts) : this.#resolveParts(dirParts);
23052
23052
  return result;
@@ -23798,8 +23798,8 @@ var init_esm7 = __esm({
23798
23798
  /**
23799
23799
  * @internal
23800
23800
  */
23801
- getRootString(path30) {
23802
- return win32.parse(path30).root;
23801
+ getRootString(path32) {
23802
+ return win32.parse(path32).root;
23803
23803
  }
23804
23804
  /**
23805
23805
  * @internal
@@ -23845,8 +23845,8 @@ var init_esm7 = __esm({
23845
23845
  /**
23846
23846
  * @internal
23847
23847
  */
23848
- getRootString(path30) {
23849
- return path30.startsWith("/") ? "/" : "";
23848
+ getRootString(path32) {
23849
+ return path32.startsWith("/") ? "/" : "";
23850
23850
  }
23851
23851
  /**
23852
23852
  * @internal
@@ -23935,11 +23935,11 @@ var init_esm7 = __esm({
23935
23935
  /**
23936
23936
  * Get the depth of a provided path, string, or the cwd
23937
23937
  */
23938
- depth(path30 = this.cwd) {
23939
- if (typeof path30 === "string") {
23940
- path30 = this.cwd.resolve(path30);
23938
+ depth(path32 = this.cwd) {
23939
+ if (typeof path32 === "string") {
23940
+ path32 = this.cwd.resolve(path32);
23941
23941
  }
23942
- return path30.depth();
23942
+ return path32.depth();
23943
23943
  }
23944
23944
  /**
23945
23945
  * Return the cache of child entries. Exposed so subclasses can create
@@ -24426,9 +24426,9 @@ var init_esm7 = __esm({
24426
24426
  process3();
24427
24427
  return results;
24428
24428
  }
24429
- chdir(path30 = this.cwd) {
24429
+ chdir(path32 = this.cwd) {
24430
24430
  const oldCwd = this.cwd;
24431
- this.cwd = typeof path30 === "string" ? this.cwd.resolve(path30) : path30;
24431
+ this.cwd = typeof path32 === "string" ? this.cwd.resolve(path32) : path32;
24432
24432
  this.cwd[setAsCwd](oldCwd);
24433
24433
  }
24434
24434
  };
@@ -24809,8 +24809,8 @@ var init_processor = __esm({
24809
24809
  }
24810
24810
  // match, absolute, ifdir
24811
24811
  entries() {
24812
- return [...this.store.entries()].map(([path30, n]) => [
24813
- path30,
24812
+ return [...this.store.entries()].map(([path32, n]) => [
24813
+ path32,
24814
24814
  !!(n & 2),
24815
24815
  !!(n & 1)
24816
24816
  ]);
@@ -25025,9 +25025,9 @@ var init_walker = __esm({
25025
25025
  signal;
25026
25026
  maxDepth;
25027
25027
  includeChildMatches;
25028
- constructor(patterns, path30, opts) {
25028
+ constructor(patterns, path32, opts) {
25029
25029
  this.patterns = patterns;
25030
- this.path = path30;
25030
+ this.path = path32;
25031
25031
  this.opts = opts;
25032
25032
  this.#sep = !opts.posix && opts.platform === "win32" ? "\\" : "/";
25033
25033
  this.includeChildMatches = opts.includeChildMatches !== false;
@@ -25046,11 +25046,11 @@ var init_walker = __esm({
25046
25046
  });
25047
25047
  }
25048
25048
  }
25049
- #ignored(path30) {
25050
- return this.seen.has(path30) || !!this.#ignore?.ignored?.(path30);
25049
+ #ignored(path32) {
25050
+ return this.seen.has(path32) || !!this.#ignore?.ignored?.(path32);
25051
25051
  }
25052
- #childrenIgnored(path30) {
25053
- return !!this.#ignore?.childrenIgnored?.(path30);
25052
+ #childrenIgnored(path32) {
25053
+ return !!this.#ignore?.childrenIgnored?.(path32);
25054
25054
  }
25055
25055
  // backpressure mechanism
25056
25056
  pause() {
@@ -25265,8 +25265,8 @@ var init_walker = __esm({
25265
25265
  };
25266
25266
  GlobWalker = class extends GlobUtil {
25267
25267
  matches = /* @__PURE__ */ new Set();
25268
- constructor(patterns, path30, opts) {
25269
- super(patterns, path30, opts);
25268
+ constructor(patterns, path32, opts) {
25269
+ super(patterns, path32, opts);
25270
25270
  }
25271
25271
  matchEmit(e) {
25272
25272
  this.matches.add(e);
@@ -25303,8 +25303,8 @@ var init_walker = __esm({
25303
25303
  };
25304
25304
  GlobStream = class extends GlobUtil {
25305
25305
  results;
25306
- constructor(patterns, path30, opts) {
25307
- super(patterns, path30, opts);
25306
+ constructor(patterns, path32, opts) {
25307
+ super(patterns, path32, opts);
25308
25308
  this.results = new Minipass({
25309
25309
  signal: this.signal,
25310
25310
  objectMode: true
@@ -25634,6 +25634,26 @@ var init_esm8 = __esm({
25634
25634
  });
25635
25635
 
25636
25636
  // src/mcp/utils/fs.ts
25637
+ var fs_exports = {};
25638
+ __export(fs_exports, {
25639
+ FileSystemError: () => FileSystemError,
25640
+ copyFile: () => copyFile,
25641
+ directoryExists: () => directoryExists,
25642
+ ensureDirectory: () => ensureDirectory,
25643
+ fileExists: () => fileExists,
25644
+ findDirectories: () => findDirectories,
25645
+ findFiles: () => findFiles,
25646
+ normalizePath: () => normalizePath,
25647
+ readJson: () => readJson,
25648
+ readText: () => readText,
25649
+ relativePath: () => relativePath,
25650
+ removeDirectory: () => removeDirectory,
25651
+ removeFile: () => removeFile,
25652
+ safeJoinPath: () => safeJoinPath,
25653
+ validatePathSecurity: () => validatePathSecurity,
25654
+ writeJson: () => writeJson,
25655
+ writeText: () => writeText
25656
+ });
25637
25657
  import { stat, mkdir, readFile, writeFile, cp, rm } from "fs/promises";
25638
25658
  import path3 from "path";
25639
25659
  function validatePathSecurity(targetPath, baseDir) {
@@ -25697,6 +25717,20 @@ async function readJson(filePath) {
25697
25717
  );
25698
25718
  }
25699
25719
  }
25720
+ async function writeJson(filePath, data) {
25721
+ try {
25722
+ await mkdir(path3.dirname(filePath), { recursive: true });
25723
+ await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
25724
+ } catch (error2) {
25725
+ const err = error2 instanceof Error ? error2 : new Error(String(error2));
25726
+ throw new FileSystemError(
25727
+ `Failed to write JSON file: ${filePath} - ${err.message}`,
25728
+ "writeJson",
25729
+ filePath,
25730
+ err
25731
+ );
25732
+ }
25733
+ }
25700
25734
  async function readText(filePath) {
25701
25735
  try {
25702
25736
  return await readFile(filePath, "utf-8");
@@ -25724,6 +25758,16 @@ async function writeText(filePath, content) {
25724
25758
  );
25725
25759
  }
25726
25760
  }
25761
+ async function copyFile(src, dest) {
25762
+ await mkdir(path3.dirname(dest), { recursive: true });
25763
+ await cp(src, dest, { recursive: true });
25764
+ }
25765
+ async function removeFile(filePath) {
25766
+ await rm(filePath, { force: true });
25767
+ }
25768
+ async function removeDirectory(dirPath) {
25769
+ await rm(dirPath, { recursive: true, force: true });
25770
+ }
25727
25771
  async function findFiles(pattern, options = {}) {
25728
25772
  const { cwd = process.cwd(), ignore = [] } = options;
25729
25773
  const files = await glob(pattern, {
@@ -25734,6 +25778,27 @@ async function findFiles(pattern, options = {}) {
25734
25778
  });
25735
25779
  return files;
25736
25780
  }
25781
+ async function findDirectories(pattern, options = {}) {
25782
+ const { cwd = process.cwd(), ignore = [] } = options;
25783
+ const dirs = await glob(pattern, {
25784
+ cwd,
25785
+ ignore: ["**/node_modules/**", "**/bin/**", "**/obj/**", ...ignore],
25786
+ absolute: true
25787
+ });
25788
+ const results = [];
25789
+ for (const dir of dirs) {
25790
+ if (await directoryExists(dir)) {
25791
+ results.push(dir);
25792
+ }
25793
+ }
25794
+ return results;
25795
+ }
25796
+ function relativePath(from, to) {
25797
+ return path3.relative(from, to).replace(/\\/g, "/");
25798
+ }
25799
+ function normalizePath(filePath) {
25800
+ return filePath.replace(/\\/g, "/");
25801
+ }
25737
25802
  var FileSystemError;
25738
25803
  var init_fs = __esm({
25739
25804
  "src/mcp/utils/fs.ts"() {
@@ -25741,10 +25806,10 @@ var init_fs = __esm({
25741
25806
  init_esm_shims();
25742
25807
  init_esm8();
25743
25808
  FileSystemError = class extends Error {
25744
- constructor(message, operation, path30, cause) {
25809
+ constructor(message, operation, path32, cause) {
25745
25810
  super(message);
25746
25811
  this.operation = operation;
25747
- this.path = path30;
25812
+ this.path = path32;
25748
25813
  this.cause = cause;
25749
25814
  this.name = "FileSystemError";
25750
25815
  }
@@ -25787,7 +25852,7 @@ function tenantModeToTemplateFlags(mode) {
25787
25852
  tenantMode: mode
25788
25853
  };
25789
25854
  }
25790
- var ProjectConfigSchema, SmartStackConfigSchema, ConventionsConfigSchema, EfCoreContextSchema, EfCoreConfigSchema, ScaffoldingConfigSchema, ConfigSchema, TenantModeSchema, ValidateConventionsInputSchema, CheckMigrationsInputSchema, EntityPropertySchema, ScaffoldExtensionInputSchema, ApiDocsInputSchema, SuggestMigrationInputSchema, GeneratePermissionsInputSchema, TestTypeSchema, TestTargetSchema, ScaffoldTestsInputSchema, AnalyzeTestCoverageInputSchema, ValidateTestConventionsInputSchema, SuggestTestScenariosInputSchema, SecurityCheckSchema, ValidateSecurityInputSchema, QualityMetricSchema, AnalyzeCodeQualityInputSchema, ScaffoldApiClientInputSchema, ScaffoldRoutesInputSchema, ValidateFrontendRoutesInputSchema, SlotDefinitionSchema, ScaffoldFrontendExtensionInputSchema, AnalyzeExtensionPointsInputSchema, AnalyzeHierarchyPatternsInputSchema, ReviewCodeCheckSchema, ReviewCodeInputSchema;
25855
+ var ProjectConfigSchema, SmartStackConfigSchema, ConventionsConfigSchema, EfCoreContextSchema, EfCoreConfigSchema, ScaffoldingConfigSchema, ConfigSchema, TenantModeSchema, ValidateConventionsInputSchema, CheckMigrationsInputSchema, EntityPropertySchema, ScaffoldExtensionInputSchema, ApiDocsInputSchema, SuggestMigrationInputSchema, GeneratePermissionsInputSchema, TestTypeSchema, TestTargetSchema, ScaffoldTestsInputSchema, AnalyzeTestCoverageInputSchema, ValidateTestConventionsInputSchema, SuggestTestScenariosInputSchema, SecurityCheckSchema, ValidateSecurityInputSchema, QualityMetricSchema, AnalyzeCodeQualityInputSchema, ScaffoldApiClientInputSchema, ScaffoldRoutesInputSchema, ValidateFrontendRoutesInputSchema, SlotDefinitionSchema, ScaffoldFrontendExtensionInputSchema, AnalyzeExtensionPointsInputSchema, ScaffoldDataExportInputSchema, PromptDataExportM2mInputSchema, AnalyzeHierarchyPatternsInputSchema, ReviewCodeCheckSchema, ReviewCodeInputSchema;
25791
25856
  var init_types3 = __esm({
25792
25857
  "src/mcp/types/index.ts"() {
25793
25858
  "use strict";
@@ -26067,6 +26132,26 @@ var init_types3 = __esm({
26067
26132
  target: external_exports.enum(["pages", "components", "forms", "tables", "all"]).default("all").describe("Type of components to analyze"),
26068
26133
  filter: external_exports.string().optional().describe('Filter by file name pattern (e.g., "User*")')
26069
26134
  });
26135
+ ScaffoldDataExportInputSchema = external_exports.object({
26136
+ name: external_exports.string().min(1).describe('Entity name in PascalCase (e.g., "Product", "Order")'),
26137
+ navRoute: external_exports.string().min(1).describe('NavRoute of the source entity (e.g., "business.sales.products") \u2014 used for permission derivation'),
26138
+ options: external_exports.object({
26139
+ entityProperties: external_exports.array(EntityPropertySchema).optional().describe("Properties for the export DTO"),
26140
+ rateLimitPerMinute: external_exports.number().int().positive().default(60).describe("Rate limit per minute (default: 60)"),
26141
+ maxPageSize: external_exports.number().int().positive().default(1e3).describe("Max page size (default: 1000)"),
26142
+ includeAuditFields: external_exports.boolean().default(true).describe("Include CreatedAt/UpdatedAt in DTO (default: true)"),
26143
+ includeFrontendClient: external_exports.boolean().default(false).describe("Generate TypeScript API client (default: false)"),
26144
+ dryRun: external_exports.boolean().default(false).describe("Preview without writing files (default: false)")
26145
+ }).optional()
26146
+ });
26147
+ PromptDataExportM2mInputSchema = external_exports.object({
26148
+ endpoints: external_exports.array(external_exports.string()).optional().describe('Endpoint codes to include (e.g., ["users", "tenants"]). If empty, discovers all available endpoints.'),
26149
+ clientId: external_exports.string().optional().describe("Optional clientId to pre-fill in the prompt"),
26150
+ options: external_exports.object({
26151
+ includeBasePrompt: external_exports.boolean().default(true).describe("Include authentication and common sections (default: true)"),
26152
+ format: external_exports.enum(["full", "endpoints-only"]).default("full").describe('Output format: "full" or "endpoints-only"')
26153
+ }).optional()
26154
+ });
26070
26155
  AnalyzeHierarchyPatternsInputSchema = external_exports.object({
26071
26156
  path: external_exports.string().optional().describe("Project path to analyze (default: SmartStack.app path)"),
26072
26157
  entityFilter: external_exports.string().optional().describe('Filter entities by name pattern (e.g., "User*", "*Group*")'),
@@ -26394,7 +26479,8 @@ var init_config = __esm({
26394
26479
  "ref_",
26395
26480
  "loc_",
26396
26481
  "lic_",
26397
- "tenant_"
26482
+ "tenant_",
26483
+ "ext_"
26398
26484
  ],
26399
26485
  customTablePrefixes: [],
26400
26486
  scopeTypes: ["Core", "Extension", "Partner", "Community"],
@@ -27258,8 +27344,8 @@ async function validateControllerRoutes(structure, _config, result) {
27258
27344
  const routeAttrMatch = content.match(/\[Route\s*\(\s*"([^"]+)"\s*\)\]/);
27259
27345
  if (!routeAttrMatch) continue;
27260
27346
  const routeValue = routeAttrMatch[1];
27261
- const relativePath = path8.relative(structure.api, file);
27262
- const dirParts = path8.dirname(relativePath).split(path8.sep);
27347
+ const relativePath2 = path8.relative(structure.api, file);
27348
+ const dirParts = path8.dirname(relativePath2).split(path8.sep);
27263
27349
  const moduleDir = dirParts.slice(0, Math.min(dirParts.length, 3)).join("/");
27264
27350
  if (!routesByDirectory.has(moduleDir)) {
27265
27351
  routesByDirectory.set(moduleDir, []);
@@ -29863,13 +29949,13 @@ var require_ast = __commonJS({
29863
29949
  helperExpression: function helperExpression(node) {
29864
29950
  return node.type === "SubExpression" || (node.type === "MustacheStatement" || node.type === "BlockStatement") && !!(node.params && node.params.length || node.hash);
29865
29951
  },
29866
- scopedId: function scopedId(path30) {
29867
- return /^\.|this\b/.test(path30.original);
29952
+ scopedId: function scopedId(path32) {
29953
+ return /^\.|this\b/.test(path32.original);
29868
29954
  },
29869
29955
  // an ID is simple if it only has one part, and that part is not
29870
29956
  // `..` or `this`.
29871
- simpleId: function simpleId(path30) {
29872
- return path30.parts.length === 1 && !AST2.helpers.scopedId(path30) && !path30.depth;
29957
+ simpleId: function simpleId(path32) {
29958
+ return path32.parts.length === 1 && !AST2.helpers.scopedId(path32) && !path32.depth;
29873
29959
  }
29874
29960
  }
29875
29961
  };
@@ -30943,12 +31029,12 @@ var require_helpers2 = __commonJS({
30943
31029
  loc
30944
31030
  };
30945
31031
  }
30946
- function prepareMustache(path30, params, hash, open, strip, locInfo) {
31032
+ function prepareMustache(path32, params, hash, open, strip, locInfo) {
30947
31033
  var escapeFlag = open.charAt(3) || open.charAt(2), escaped = escapeFlag !== "{" && escapeFlag !== "&";
30948
31034
  var decorator = /\*/.test(open);
30949
31035
  return {
30950
31036
  type: decorator ? "Decorator" : "MustacheStatement",
30951
- path: path30,
31037
+ path: path32,
30952
31038
  params,
30953
31039
  hash,
30954
31040
  escaped,
@@ -31220,9 +31306,9 @@ var require_compiler = __commonJS({
31220
31306
  },
31221
31307
  DecoratorBlock: function DecoratorBlock(decorator) {
31222
31308
  var program = decorator.program && this.compileProgram(decorator.program);
31223
- var params = this.setupFullMustacheParams(decorator, program, void 0), path30 = decorator.path;
31309
+ var params = this.setupFullMustacheParams(decorator, program, void 0), path32 = decorator.path;
31224
31310
  this.useDecorators = true;
31225
- this.opcode("registerDecorator", params.length, path30.original);
31311
+ this.opcode("registerDecorator", params.length, path32.original);
31226
31312
  },
31227
31313
  PartialStatement: function PartialStatement(partial2) {
31228
31314
  this.usePartial = true;
@@ -31286,46 +31372,46 @@ var require_compiler = __commonJS({
31286
31372
  }
31287
31373
  },
31288
31374
  ambiguousSexpr: function ambiguousSexpr(sexpr, program, inverse) {
31289
- var path30 = sexpr.path, name = path30.parts[0], isBlock = program != null || inverse != null;
31290
- this.opcode("getContext", path30.depth);
31375
+ var path32 = sexpr.path, name = path32.parts[0], isBlock = program != null || inverse != null;
31376
+ this.opcode("getContext", path32.depth);
31291
31377
  this.opcode("pushProgram", program);
31292
31378
  this.opcode("pushProgram", inverse);
31293
- path30.strict = true;
31294
- this.accept(path30);
31379
+ path32.strict = true;
31380
+ this.accept(path32);
31295
31381
  this.opcode("invokeAmbiguous", name, isBlock);
31296
31382
  },
31297
31383
  simpleSexpr: function simpleSexpr(sexpr) {
31298
- var path30 = sexpr.path;
31299
- path30.strict = true;
31300
- this.accept(path30);
31384
+ var path32 = sexpr.path;
31385
+ path32.strict = true;
31386
+ this.accept(path32);
31301
31387
  this.opcode("resolvePossibleLambda");
31302
31388
  },
31303
31389
  helperSexpr: function helperSexpr(sexpr, program, inverse) {
31304
- var params = this.setupFullMustacheParams(sexpr, program, inverse), path30 = sexpr.path, name = path30.parts[0];
31390
+ var params = this.setupFullMustacheParams(sexpr, program, inverse), path32 = sexpr.path, name = path32.parts[0];
31305
31391
  if (this.options.knownHelpers[name]) {
31306
31392
  this.opcode("invokeKnownHelper", params.length, name);
31307
31393
  } else if (this.options.knownHelpersOnly) {
31308
31394
  throw new _exception2["default"]("You specified knownHelpersOnly, but used the unknown helper " + name, sexpr);
31309
31395
  } else {
31310
- path30.strict = true;
31311
- path30.falsy = true;
31312
- this.accept(path30);
31313
- this.opcode("invokeHelper", params.length, path30.original, _ast2["default"].helpers.simpleId(path30));
31396
+ path32.strict = true;
31397
+ path32.falsy = true;
31398
+ this.accept(path32);
31399
+ this.opcode("invokeHelper", params.length, path32.original, _ast2["default"].helpers.simpleId(path32));
31314
31400
  }
31315
31401
  },
31316
- PathExpression: function PathExpression(path30) {
31317
- this.addDepth(path30.depth);
31318
- this.opcode("getContext", path30.depth);
31319
- var name = path30.parts[0], scoped = _ast2["default"].helpers.scopedId(path30), blockParamId = !path30.depth && !scoped && this.blockParamIndex(name);
31402
+ PathExpression: function PathExpression(path32) {
31403
+ this.addDepth(path32.depth);
31404
+ this.opcode("getContext", path32.depth);
31405
+ var name = path32.parts[0], scoped = _ast2["default"].helpers.scopedId(path32), blockParamId = !path32.depth && !scoped && this.blockParamIndex(name);
31320
31406
  if (blockParamId) {
31321
- this.opcode("lookupBlockParam", blockParamId, path30.parts);
31407
+ this.opcode("lookupBlockParam", blockParamId, path32.parts);
31322
31408
  } else if (!name) {
31323
31409
  this.opcode("pushContext");
31324
- } else if (path30.data) {
31410
+ } else if (path32.data) {
31325
31411
  this.options.data = true;
31326
- this.opcode("lookupData", path30.depth, path30.parts, path30.strict);
31412
+ this.opcode("lookupData", path32.depth, path32.parts, path32.strict);
31327
31413
  } else {
31328
- this.opcode("lookupOnContext", path30.parts, path30.falsy, path30.strict, scoped);
31414
+ this.opcode("lookupOnContext", path32.parts, path32.falsy, path32.strict, scoped);
31329
31415
  }
31330
31416
  },
31331
31417
  StringLiteral: function StringLiteral(string3) {
@@ -31681,16 +31767,16 @@ var require_util2 = __commonJS({
31681
31767
  }
31682
31768
  exports.urlGenerate = urlGenerate;
31683
31769
  function normalize2(aPath) {
31684
- var path30 = aPath;
31770
+ var path32 = aPath;
31685
31771
  var url2 = urlParse(aPath);
31686
31772
  if (url2) {
31687
31773
  if (!url2.path) {
31688
31774
  return aPath;
31689
31775
  }
31690
- path30 = url2.path;
31776
+ path32 = url2.path;
31691
31777
  }
31692
- var isAbsolute = exports.isAbsolute(path30);
31693
- var parts = path30.split(/\/+/);
31778
+ var isAbsolute = exports.isAbsolute(path32);
31779
+ var parts = path32.split(/\/+/);
31694
31780
  for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {
31695
31781
  part = parts[i];
31696
31782
  if (part === ".") {
@@ -31707,15 +31793,15 @@ var require_util2 = __commonJS({
31707
31793
  }
31708
31794
  }
31709
31795
  }
31710
- path30 = parts.join("/");
31711
- if (path30 === "") {
31712
- path30 = isAbsolute ? "/" : ".";
31796
+ path32 = parts.join("/");
31797
+ if (path32 === "") {
31798
+ path32 = isAbsolute ? "/" : ".";
31713
31799
  }
31714
31800
  if (url2) {
31715
- url2.path = path30;
31801
+ url2.path = path32;
31716
31802
  return urlGenerate(url2);
31717
31803
  }
31718
- return path30;
31804
+ return path32;
31719
31805
  }
31720
31806
  exports.normalize = normalize2;
31721
31807
  function join2(aRoot, aPath) {
@@ -34516,8 +34602,8 @@ var require_printer = __commonJS({
34516
34602
  return this.accept(sexpr.path) + " " + params + hash;
34517
34603
  };
34518
34604
  PrintVisitor.prototype.PathExpression = function(id) {
34519
- var path30 = id.parts.join("/");
34520
- return (id.data ? "@" : "") + "PATH:" + path30;
34605
+ var path32 = id.parts.join("/");
34606
+ return (id.data ? "@" : "") + "PATH:" + path32;
34521
34607
  };
34522
34608
  PrintVisitor.prototype.StringLiteral = function(string3) {
34523
34609
  return '"' + string3.value + '"';
@@ -46114,11 +46200,11 @@ var require_mime_types = __commonJS({
46114
46200
  }
46115
46201
  return exts[0];
46116
46202
  }
46117
- function lookup(path30) {
46118
- if (!path30 || typeof path30 !== "string") {
46203
+ function lookup(path32) {
46204
+ if (!path32 || typeof path32 !== "string") {
46119
46205
  return false;
46120
46206
  }
46121
- var extension2 = extname("x." + path30).toLowerCase().substr(1);
46207
+ var extension2 = extname("x." + path32).toLowerCase().substr(1);
46122
46208
  if (!extension2) {
46123
46209
  return false;
46124
46210
  }
@@ -47281,7 +47367,7 @@ var require_form_data = __commonJS({
47281
47367
  init_esm_shims();
47282
47368
  var CombinedStream = require_combined_stream();
47283
47369
  var util4 = __require("util");
47284
- var path30 = __require("path");
47370
+ var path32 = __require("path");
47285
47371
  var http3 = __require("http");
47286
47372
  var https2 = __require("https");
47287
47373
  var parseUrl = __require("url").parse;
@@ -47409,11 +47495,11 @@ var require_form_data = __commonJS({
47409
47495
  FormData3.prototype._getContentDisposition = function(value, options) {
47410
47496
  var filename;
47411
47497
  if (typeof options.filepath === "string") {
47412
- filename = path30.normalize(options.filepath).replace(/\\/g, "/");
47498
+ filename = path32.normalize(options.filepath).replace(/\\/g, "/");
47413
47499
  } else if (options.filename || value && (value.name || value.path)) {
47414
- filename = path30.basename(options.filename || value && (value.name || value.path));
47500
+ filename = path32.basename(options.filename || value && (value.name || value.path));
47415
47501
  } else if (value && value.readable && hasOwn(value, "httpVersion")) {
47416
- filename = path30.basename(value.client._httpMessage.path || "");
47502
+ filename = path32.basename(value.client._httpMessage.path || "");
47417
47503
  }
47418
47504
  if (filename) {
47419
47505
  return 'filename="' + filename + '"';
@@ -47612,9 +47698,9 @@ function isVisitable(thing) {
47612
47698
  function removeBrackets(key) {
47613
47699
  return utils_default.endsWith(key, "[]") ? key.slice(0, -2) : key;
47614
47700
  }
47615
- function renderKey(path30, key, dots) {
47616
- if (!path30) return key;
47617
- return path30.concat(key).map(function each(token, i) {
47701
+ function renderKey(path32, key, dots) {
47702
+ if (!path32) return key;
47703
+ return path32.concat(key).map(function each(token, i) {
47618
47704
  token = removeBrackets(token);
47619
47705
  return !dots && i ? "[" + token + "]" : token;
47620
47706
  }).join(dots ? "." : "");
@@ -47659,9 +47745,9 @@ function toFormData(obj, formData, options) {
47659
47745
  }
47660
47746
  return value;
47661
47747
  }
47662
- function defaultVisitor(value, key, path30) {
47748
+ function defaultVisitor(value, key, path32) {
47663
47749
  let arr = value;
47664
- if (value && !path30 && typeof value === "object") {
47750
+ if (value && !path32 && typeof value === "object") {
47665
47751
  if (utils_default.endsWith(key, "{}")) {
47666
47752
  key = metaTokens ? key : key.slice(0, -2);
47667
47753
  value = JSON.stringify(value);
@@ -47680,7 +47766,7 @@ function toFormData(obj, formData, options) {
47680
47766
  if (isVisitable(value)) {
47681
47767
  return true;
47682
47768
  }
47683
- formData.append(renderKey(path30, key, dots), convertValue(value));
47769
+ formData.append(renderKey(path32, key, dots), convertValue(value));
47684
47770
  return false;
47685
47771
  }
47686
47772
  const stack = [];
@@ -47689,10 +47775,10 @@ function toFormData(obj, formData, options) {
47689
47775
  convertValue,
47690
47776
  isVisitable
47691
47777
  });
47692
- function build(value, path30) {
47778
+ function build(value, path32) {
47693
47779
  if (utils_default.isUndefined(value)) return;
47694
47780
  if (stack.indexOf(value) !== -1) {
47695
- throw Error("Circular reference detected in " + path30.join("."));
47781
+ throw Error("Circular reference detected in " + path32.join("."));
47696
47782
  }
47697
47783
  stack.push(value);
47698
47784
  utils_default.forEach(value, function each(el, key) {
@@ -47700,11 +47786,11 @@ function toFormData(obj, formData, options) {
47700
47786
  formData,
47701
47787
  el,
47702
47788
  utils_default.isString(key) ? key.trim() : key,
47703
- path30,
47789
+ path32,
47704
47790
  exposedHelpers
47705
47791
  );
47706
47792
  if (result === true) {
47707
- build(el, path30 ? path30.concat(key) : [key]);
47793
+ build(el, path32 ? path32.concat(key) : [key]);
47708
47794
  }
47709
47795
  });
47710
47796
  stack.pop();
@@ -47990,7 +48076,7 @@ var init_platform = __esm({
47990
48076
  // node_modules/axios/lib/helpers/toURLEncodedForm.js
47991
48077
  function toURLEncodedForm(data, options) {
47992
48078
  return toFormData_default(data, new platform_default.classes.URLSearchParams(), {
47993
- visitor: function(value, key, path30, helpers) {
48079
+ visitor: function(value, key, path32, helpers) {
47994
48080
  if (platform_default.isNode && utils_default.isBuffer(value)) {
47995
48081
  this.append(key, value.toString("base64"));
47996
48082
  return false;
@@ -48029,11 +48115,11 @@ function arrayToObject(arr) {
48029
48115
  return obj;
48030
48116
  }
48031
48117
  function formDataToJSON(formData) {
48032
- function buildPath(path30, value, target, index) {
48033
- let name = path30[index++];
48118
+ function buildPath(path32, value, target, index) {
48119
+ let name = path32[index++];
48034
48120
  if (name === "__proto__") return true;
48035
48121
  const isNumericKey = Number.isFinite(+name);
48036
- const isLast = index >= path30.length;
48122
+ const isLast = index >= path32.length;
48037
48123
  name = !name && utils_default.isArray(target) ? target.length : name;
48038
48124
  if (isLast) {
48039
48125
  if (utils_default.hasOwnProp(target, name)) {
@@ -48046,7 +48132,7 @@ function formDataToJSON(formData) {
48046
48132
  if (!target[name] || !utils_default.isObject(target[name])) {
48047
48133
  target[name] = [];
48048
48134
  }
48049
- const result = buildPath(path30, value, target[name], index);
48135
+ const result = buildPath(path32, value, target[name], index);
48050
48136
  if (result && utils_default.isArray(target[name])) {
48051
48137
  target[name] = arrayToObject(target[name]);
48052
48138
  }
@@ -50946,9 +51032,9 @@ var init_http = __esm({
50946
51032
  auth = urlUsername + ":" + urlPassword;
50947
51033
  }
50948
51034
  auth && headers.delete("authorization");
50949
- let path30;
51035
+ let path32;
50950
51036
  try {
50951
- path30 = buildURL(
51037
+ path32 = buildURL(
50952
51038
  parsed.pathname + parsed.search,
50953
51039
  config2.params,
50954
51040
  config2.paramsSerializer
@@ -50966,7 +51052,7 @@ var init_http = __esm({
50966
51052
  false
50967
51053
  );
50968
51054
  const options = {
50969
- path: path30,
51055
+ path: path32,
50970
51056
  method,
50971
51057
  headers: headers.toJSON(),
50972
51058
  agents: { http: config2.httpAgent, https: config2.httpsAgent },
@@ -51219,14 +51305,14 @@ var init_cookies = __esm({
51219
51305
  cookies_default = platform_default.hasStandardBrowserEnv ? (
51220
51306
  // Standard browser envs support document.cookie
51221
51307
  {
51222
- write(name, value, expires, path30, domain, secure, sameSite) {
51308
+ write(name, value, expires, path32, domain, secure, sameSite) {
51223
51309
  if (typeof document === "undefined") return;
51224
51310
  const cookie = [`${name}=${encodeURIComponent(value)}`];
51225
51311
  if (utils_default.isNumber(expires)) {
51226
51312
  cookie.push(`expires=${new Date(expires).toUTCString()}`);
51227
51313
  }
51228
- if (utils_default.isString(path30)) {
51229
- cookie.push(`path=${path30}`);
51314
+ if (utils_default.isString(path32)) {
51315
+ cookie.push(`path=${path32}`);
51230
51316
  }
51231
51317
  if (utils_default.isString(domain)) {
51232
51318
  cookie.push(`domain=${domain}`);
@@ -57587,8 +57673,8 @@ function formatResult4(result, input) {
57587
57673
  lines.push("## Generated Files");
57588
57674
  lines.push("");
57589
57675
  for (const file of result.files) {
57590
- const relativePath = file.path.replace(/\\/g, "/").split("/src/").pop() || file.path;
57591
- lines.push(`### ${relativePath}`);
57676
+ const relativePath2 = file.path.replace(/\\/g, "/").split("/src/").pop() || file.path;
57677
+ lines.push(`### ${relativePath2}`);
57592
57678
  lines.push("");
57593
57679
  lines.push("```typescript");
57594
57680
  lines.push(file.content.substring(0, 1500) + (file.content.length > 1500 ? "\n// ... (truncated)" : ""));
@@ -58473,8 +58559,8 @@ function formatResult5(result, input) {
58473
58559
  lines.push("## Generated Files");
58474
58560
  lines.push("");
58475
58561
  for (const file of result.files) {
58476
- const relativePath = file.path.replace(/\\/g, "/").split("/src/").pop() || file.path;
58477
- lines.push(`### ${relativePath}`);
58562
+ const relativePath2 = file.path.replace(/\\/g, "/").split("/src/").pop() || file.path;
58563
+ lines.push(`### ${relativePath2}`);
58478
58564
  lines.push("");
58479
58565
  lines.push("```tsx");
58480
58566
  lines.push(file.content.substring(0, 2e3) + (file.content.length > 2e3 ? "\n// ... (truncated)" : ""));
@@ -58690,7 +58776,7 @@ async function validateApiClients(webPath, backendRoutes, result) {
58690
58776
  for (const file of clientFiles) {
58691
58777
  try {
58692
58778
  const content = await readText(file);
58693
- const relativePath = path20.relative(webPath, file);
58779
+ const relativePath2 = path20.relative(webPath, file);
58694
58780
  const usesRegistry = content.includes("getRoute('") || content.includes('getRoute("');
58695
58781
  if (!usesRegistry) {
58696
58782
  const hardcodedMatch = content.match(/apiClient\.(get|post|put|delete)\s*[<(]\s*['"`]([^'"`]+)['"`]/);
@@ -58698,7 +58784,7 @@ async function validateApiClients(webPath, backendRoutes, result) {
58698
58784
  result.apiClients.issues.push({
58699
58785
  type: "invalid-path",
58700
58786
  severity: "warning",
58701
- file: relativePath,
58787
+ file: relativePath2,
58702
58788
  message: `Hardcoded API path: ${hardcodedMatch[2]}`,
58703
58789
  suggestion: "Use getRoute() from navRoutes.generated.ts instead"
58704
58790
  });
@@ -58712,7 +58798,7 @@ async function validateApiClients(webPath, backendRoutes, result) {
58712
58798
  result.apiClients.issues.push({
58713
58799
  type: "missing-route",
58714
58800
  severity: "error",
58715
- file: relativePath,
58801
+ file: relativePath2,
58716
58802
  navRoute,
58717
58803
  message: `NavRoute "${navRoute}" not found in backend controllers`,
58718
58804
  suggestion: "Verify the NavRoute path or update the backend controller"
@@ -60093,11 +60179,11 @@ async function analyzeFile(filePath, srcPath, target) {
60093
60179
  try {
60094
60180
  const content = await readText(filePath);
60095
60181
  if (!content) return null;
60096
- const relativePath = path21.relative(srcPath, filePath).replace(/\\/g, "/");
60182
+ const relativePath2 = path21.relative(srcPath, filePath).replace(/\\/g, "/");
60097
60183
  const componentName = extractComponentName(content, filePath);
60098
60184
  const isPage = filePath.includes("/pages/") || filePath.includes("\\pages\\");
60099
60185
  const analysis = {
60100
- file: relativePath,
60186
+ file: relativePath2,
60101
60187
  componentName,
60102
60188
  type: isPage ? "page" : "component",
60103
60189
  suggestedSlots: [],
@@ -60107,7 +60193,7 @@ async function analyzeFile(filePath, srcPath, target) {
60107
60193
  imports: extractImports(content),
60108
60194
  lineCount: content.split("\n").length
60109
60195
  };
60110
- const slotPrefix = generateSlotPrefix(componentName, relativePath);
60196
+ const slotPrefix = generateSlotPrefix(componentName, relativePath2);
60111
60197
  if (target === "forms" && !analysis.hasForm) return null;
60112
60198
  if (target === "tables" && !analysis.hasTable) return null;
60113
60199
  if (isPage) {
@@ -62091,8 +62177,1105 @@ var init_analyze_code_quality = __esm({
62091
62177
  }
62092
62178
  });
62093
62179
 
62094
- // src/mcp/tools/analyze-hierarchy-patterns.ts
62180
+ // src/mcp/tools/scaffold-data-export.ts
62095
62181
  import path25 from "path";
62182
+ async function handleScaffoldDataExport(args, config2) {
62183
+ const input = ScaffoldDataExportInputSchema.parse(args);
62184
+ logger.info("Scaffolding Data Export", { name: input.name, navRoute: input.navRoute });
62185
+ const result = await scaffoldDataExport(input, config2);
62186
+ return formatResult9(result, input);
62187
+ }
62188
+ async function scaffoldDataExport(input, config2) {
62189
+ const result = {
62190
+ success: true,
62191
+ files: [],
62192
+ instructions: []
62193
+ };
62194
+ const { name, navRoute, options } = input;
62195
+ const dryRun = options?.dryRun ?? false;
62196
+ const includeAuditFields = options?.includeAuditFields ?? true;
62197
+ const includeFrontendClient = options?.includeFrontendClient ?? false;
62198
+ const rateLimitPerMinute = options?.rateLimitPerMinute ?? 60;
62199
+ const maxPageSize = options?.maxPageSize ?? 1e3;
62200
+ const entityProperties = options?.entityProperties ?? [];
62201
+ const nameLower = name.charAt(0).toLowerCase() + name.slice(1);
62202
+ const namePlural = pluralize(nameLower);
62203
+ const segments = navRoute.split(".");
62204
+ const permissionPath = segments.join(".");
62205
+ const baseNamespace = config2.conventions.namespaces.api.replace(".Api", "");
62206
+ const appSegment = segments.length >= 2 ? toPascalCase2(segments[1]) : "Default";
62207
+ const moduleSegment = segments.length >= 3 ? toPascalCase2(segments[2]) : name;
62208
+ const projectRoot = config2.smartstack.projectPath;
62209
+ const structure = await findSmartStackStructure(projectRoot);
62210
+ const apiPath = structure.api || path25.join(projectRoot, "Api");
62211
+ const applicationPath = structure.application || path25.join(projectRoot, "Application");
62212
+ const infrastructurePath = structure.infrastructure || path25.join(projectRoot, "Infrastructure");
62213
+ const webPath = structure.web || path25.join(projectRoot, "web");
62214
+ const controllerContent = generateExportController(name, namePlural, permissionPath, baseNamespace, maxPageSize);
62215
+ const controllerFile = path25.join(apiPath, "Controllers", "DataExport", "v1", `Export${name}Controller.cs`);
62216
+ if (!dryRun) {
62217
+ await ensureDirectory(path25.dirname(controllerFile));
62218
+ await writeText(controllerFile, controllerContent);
62219
+ }
62220
+ result.files.push({ path: controllerFile, content: controllerContent, type: "created" });
62221
+ const interfaceContent = generateExportServiceInterface(name, baseNamespace);
62222
+ const interfaceFile = path25.join(applicationPath, "Common", "Interfaces", `IExport${name}Service.cs`);
62223
+ if (!dryRun) {
62224
+ await ensureDirectory(path25.dirname(interfaceFile));
62225
+ await writeText(interfaceFile, interfaceContent);
62226
+ }
62227
+ result.files.push({ path: interfaceFile, content: interfaceContent, type: "created" });
62228
+ const serviceContent = generateExportService(name, namePlural, baseNamespace, entityProperties, includeAuditFields);
62229
+ const serviceFile = path25.join(infrastructurePath, "Services", "DataExport", `Export${name}Service.cs`);
62230
+ if (!dryRun) {
62231
+ await ensureDirectory(path25.dirname(serviceFile));
62232
+ await writeText(serviceFile, serviceContent);
62233
+ }
62234
+ result.files.push({ path: serviceFile, content: serviceContent, type: "created" });
62235
+ const dtoContent = generateExportDto(name, baseNamespace, entityProperties, includeAuditFields);
62236
+ const dtoFile = path25.join(applicationPath, "DataExport", "Dtos", `Export${name}Dto.cs`);
62237
+ if (!dryRun) {
62238
+ await ensureDirectory(path25.dirname(dtoFile));
62239
+ await writeText(dtoFile, dtoContent);
62240
+ }
62241
+ result.files.push({ path: dtoFile, content: dtoContent, type: "created" });
62242
+ const seedContent = generateSeedDataFragment(name, namePlural, permissionPath, appSegment, moduleSegment, rateLimitPerMinute, maxPageSize);
62243
+ const seedFile = path25.join(infrastructurePath, "Persistence", "Seeding", "Data", "DataExport", `Export${name}EndpointSeedData.cs`);
62244
+ if (!dryRun) {
62245
+ await ensureDirectory(path25.dirname(seedFile));
62246
+ await writeText(seedFile, seedContent);
62247
+ }
62248
+ result.files.push({ path: seedFile, content: seedContent, type: "created" });
62249
+ if (includeFrontendClient) {
62250
+ const clientContent = generateFrontendClient(name, nameLower, namePlural);
62251
+ const clientFile = path25.join(webPath, "src", "services", "api", `export${name}.ts`);
62252
+ if (!dryRun) {
62253
+ await ensureDirectory(path25.dirname(clientFile));
62254
+ await writeText(clientFile, clientContent);
62255
+ }
62256
+ result.files.push({ path: clientFile, content: clientContent, type: "created" });
62257
+ }
62258
+ result.instructions.push(`Register IExport${name}Service in DI container (Program.cs or DependencyInjection.cs)`);
62259
+ result.instructions.push(`Add the export permission "${permissionPath}.export" to PermissionConfiguration.cs`);
62260
+ result.instructions.push(`Add the seed data to DataExportEndpointConfiguration or call the fragment from your seeder`);
62261
+ result.instructions.push(`Create an EF Core migration to apply the new seed data`);
62262
+ result.instructions.push(`Update the KNOWN_ENDPOINTS registry in prompt-data-export-m2m.ts (MCP) and ExternalAppPromptTab.tsx (frontend) with the new endpoint metadata`);
62263
+ result.instructions.push(`Use prompt_data_export_m2m to generate an integration prompt for this endpoint`);
62264
+ if (includeFrontendClient) {
62265
+ result.instructions.push(`Import the client: import { export${name}Api } from './services/api/export${name}';`);
62266
+ }
62267
+ return result;
62268
+ }
62269
+ function generateExportController(name, namePlural, permissionPath, baseNamespace, maxPageSize) {
62270
+ return `using Microsoft.AspNetCore.Authorization;
62271
+ using Microsoft.AspNetCore.Mvc;
62272
+ using Microsoft.AspNetCore.RateLimiting;
62273
+ using ${baseNamespace}.Application.Common.Models;
62274
+ using ${baseNamespace}.Application.DataExport.Dtos;
62275
+ using ${baseNamespace}.Application.Common.Interfaces;
62276
+ using ${baseNamespace}.Api.Authorization;
62277
+
62278
+ namespace ${baseNamespace}.Api.Controllers.DataExport.v1;
62279
+
62280
+ /// <summary>
62281
+ /// Data Export endpoint for ${name} entities.
62282
+ /// Security: JWT authentication + RequirePermission + Rate limiting + DataExportAccessMiddleware
62283
+ /// Tenant isolation: Required tenantId query parameter
62284
+ /// </summary>
62285
+ [ApiController]
62286
+ [Route("api/v1/export")]
62287
+ [Authorize]
62288
+ [EnableRateLimiting("ExternalApp")]
62289
+ public class Export${name}Controller : ControllerBase
62290
+ {
62291
+ private readonly IExport${name}Service _exportService;
62292
+
62293
+ public Export${name}Controller(IExport${name}Service exportService)
62294
+ {
62295
+ _exportService = exportService;
62296
+ }
62297
+
62298
+ /// <summary>
62299
+ /// Export ${name} data with pagination, tenant isolation, and optional modified-since filter.
62300
+ /// </summary>
62301
+ [HttpGet("${namePlural}")]
62302
+ [RequirePermission(Permissions.${permissionPath.split(".").map((s) => toPascalCase2(s)).join(".")}.Export)]
62303
+ [ProducesResponseType(typeof(PaginatedResult<Export${name}Dto>), StatusCodes.Status200OK)]
62304
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
62305
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
62306
+ public async Task<ActionResult<PaginatedResult<Export${name}Dto>>> Export(
62307
+ [FromQuery] Guid tenantId,
62308
+ [FromQuery] int page = 1,
62309
+ [FromQuery] int pageSize = 100,
62310
+ [FromQuery] DateTime? modifiedSince = null,
62311
+ CancellationToken ct = default)
62312
+ {
62313
+ if (tenantId == Guid.Empty)
62314
+ {
62315
+ return BadRequest(new ProblemDetails
62316
+ {
62317
+ Title = "Invalid Tenant",
62318
+ Detail = "tenantId is required and must be a valid GUID",
62319
+ Status = StatusCodes.Status400BadRequest
62320
+ });
62321
+ }
62322
+
62323
+ pageSize = Math.Clamp(pageSize, 1, ${maxPageSize});
62324
+ page = Math.Max(1, page);
62325
+
62326
+ var result = await _exportService.ExportAsync(tenantId, page, pageSize, modifiedSince, ct);
62327
+ return Ok(result);
62328
+ }
62329
+ }
62330
+ `;
62331
+ }
62332
+ function generateExportServiceInterface(name, baseNamespace) {
62333
+ return `using ${baseNamespace}.Application.Common.Models;
62334
+ using ${baseNamespace}.Application.DataExport.Dtos;
62335
+
62336
+ namespace ${baseNamespace}.Application.Common.Interfaces;
62337
+
62338
+ /// <summary>
62339
+ /// Service interface for ${name} data export.
62340
+ /// Enforces tenant isolation via tenantId parameter.
62341
+ /// </summary>
62342
+ public interface IExport${name}Service
62343
+ {
62344
+ /// <summary>
62345
+ /// Export ${name} data with tenant isolation, pagination, and optional modified-since filter.
62346
+ /// </summary>
62347
+ Task<PaginatedResult<Export${name}Dto>> ExportAsync(
62348
+ Guid tenantId,
62349
+ int page = 1,
62350
+ int pageSize = 100,
62351
+ DateTime? modifiedSince = null,
62352
+ CancellationToken ct = default);
62353
+ }
62354
+ `;
62355
+ }
62356
+ function generateExportService(name, namePlural, baseNamespace, entityProperties, includeAuditFields) {
62357
+ const projectionFields = [" Id = x.Id"];
62358
+ for (const prop of entityProperties) {
62359
+ projectionFields.push(` ${prop.name} = x.${prop.name}`);
62360
+ }
62361
+ if (includeAuditFields) {
62362
+ projectionFields.push(" CreatedAt = x.CreatedAt");
62363
+ projectionFields.push(" UpdatedAt = x.UpdatedAt");
62364
+ }
62365
+ const projectionBlock = projectionFields.join(",\n");
62366
+ return `using Microsoft.EntityFrameworkCore;
62367
+ using Microsoft.Extensions.Logging;
62368
+ using ${baseNamespace}.Application.Common.Interfaces;
62369
+ using ${baseNamespace}.Application.Common.Models;
62370
+ using ${baseNamespace}.Application.DataExport.Dtos;
62371
+ using ${baseNamespace}.Infrastructure.Persistence;
62372
+
62373
+ namespace ${baseNamespace}.Infrastructure.Services.DataExport;
62374
+
62375
+ /// <summary>
62376
+ /// Data export service for ${name} entities.
62377
+ /// Enforces tenant isolation via tenantId parameter validation.
62378
+ /// </summary>
62379
+ public class Export${name}Service : IExport${name}Service
62380
+ {
62381
+ private readonly ApplicationDbContext _db;
62382
+ private readonly ICurrentUserService _currentUser;
62383
+ private readonly ILogger<Export${name}Service> _logger;
62384
+
62385
+ public Export${name}Service(
62386
+ ApplicationDbContext db,
62387
+ ICurrentUserService currentUser,
62388
+ ILogger<Export${name}Service> logger)
62389
+ {
62390
+ _db = db;
62391
+ _currentUser = currentUser;
62392
+ _logger = logger;
62393
+ }
62394
+
62395
+ public async Task<PaginatedResult<Export${name}Dto>> ExportAsync(
62396
+ Guid tenantId,
62397
+ int page = 1,
62398
+ int pageSize = 100,
62399
+ DateTime? modifiedSince = null,
62400
+ CancellationToken ct = default)
62401
+ {
62402
+ if (tenantId == Guid.Empty)
62403
+ {
62404
+ throw new ArgumentException("Tenant ID cannot be empty", nameof(tenantId));
62405
+ }
62406
+
62407
+ _logger.LogInformation("Data export requested for ${name} by {UserId}, tenant {TenantId}, page {Page}",
62408
+ _currentUser.UserId, tenantId, page);
62409
+
62410
+ var query = _db.${toPascalCase2(namePlural)}
62411
+ .Where(x => x.TenantId == tenantId)
62412
+ .AsNoTracking();
62413
+
62414
+ if (modifiedSince.HasValue)
62415
+ {
62416
+ query = query.Where(x => x.UpdatedAt >= modifiedSince.Value
62417
+ || x.CreatedAt >= modifiedSince.Value);
62418
+ }
62419
+
62420
+ var totalCount = await query.CountAsync(ct);
62421
+
62422
+ var items = await query
62423
+ .OrderBy(x => x.Id)
62424
+ .Skip((page - 1) * pageSize)
62425
+ .Take(pageSize)
62426
+ .Select(x => new Export${name}Dto
62427
+ {
62428
+ ${projectionBlock}
62429
+ })
62430
+ .ToListAsync(ct);
62431
+
62432
+ return new PaginatedResult<Export${name}Dto>(items, totalCount, page, pageSize);
62433
+ }
62434
+ }
62435
+ `;
62436
+ }
62437
+ function generateExportDto(name, baseNamespace, entityProperties, includeAuditFields) {
62438
+ const properties = [" public Guid Id { get; init; }"];
62439
+ for (const prop of entityProperties) {
62440
+ const nullableSuffix = prop.required ? "" : "?";
62441
+ properties.push(` public ${prop.type}${nullableSuffix} ${prop.name} { get; init; }`);
62442
+ }
62443
+ if (includeAuditFields) {
62444
+ properties.push(" public DateTime CreatedAt { get; init; }");
62445
+ properties.push(" public DateTime? UpdatedAt { get; init; }");
62446
+ }
62447
+ return `namespace ${baseNamespace}.Application.DataExport.Dtos;
62448
+
62449
+ /// <summary>
62450
+ /// Flat DTO for ${name} data export (external consumption).
62451
+ /// Auto-generated by SmartStack MCP \u2014 customize as needed.
62452
+ /// </summary>
62453
+ public class Export${name}Dto
62454
+ {
62455
+ ${properties.join("\n")}
62456
+ }
62457
+ `;
62458
+ }
62459
+ function generateSeedDataFragment(name, namePlural, permissionPath, appSegment, moduleSegment, rateLimitPerMinute, maxPageSize) {
62460
+ return `// ============================================================================
62461
+ // Data Export Endpoint Seed Data \u2014 ${name}
62462
+ // Auto-generated by SmartStack MCP \u2014 add to DataExportEndpointConfiguration.cs
62463
+ // ============================================================================
62464
+ //
62465
+ // Copy the HasData() block below into your DataExportEndpointConfiguration
62466
+ // or IClientSeedDataProvider to register this export endpoint.
62467
+ //
62468
+ // builder.HasData(new
62469
+ // {
62470
+ // Id = DeterministicGuid.Create("data-export:${namePlural}"),
62471
+ // Code = "${namePlural}",
62472
+ // Name = "${name} Export",
62473
+ // RouteTemplate = "/api/v1/export/${namePlural}",
62474
+ // RequiredPermission = "${permissionPath}.export",
62475
+ // EntityType = "${name}",
62476
+ // IsActive = true,
62477
+ // DefaultRateLimitPerMinute = ${rateLimitPerMinute},
62478
+ // DefaultMaxPageSize = ${maxPageSize},
62479
+ // CreatedAt = seedDate
62480
+ // });
62481
+ //
62482
+ // IMPORTANT: NavigationApplicationId and NavigationModuleId must reference
62483
+ // actual seed data IDs from NavigationApplicationSeedData and
62484
+ // NavigationModuleSeedData respectively.
62485
+ //
62486
+ // Permission to add to PermissionConfiguration.cs:
62487
+ // "${permissionPath}.export" \u2014 Export ${name} data via M2M API
62488
+ `;
62489
+ }
62490
+ function generateFrontendClient(name, nameLower, namePlural) {
62491
+ return `/**
62492
+ * ${name} Data Export API Client
62493
+ *
62494
+ * Auto-generated by SmartStack MCP - DO NOT EDIT MANUALLY
62495
+ * Endpoint: /api/v1/export/${namePlural}
62496
+ * Note: tenantId is required and must be passed for all requests
62497
+ */
62498
+
62499
+ import { apiClient } from '../lib/apiClient';
62500
+
62501
+ export interface Export${name}Dto {
62502
+ id: string;
62503
+ [key: string]: unknown;
62504
+ createdAt: string;
62505
+ updatedAt?: string;
62506
+ }
62507
+
62508
+ export interface ExportParams {
62509
+ tenantId: string; // Required: tenant identifier
62510
+ page?: number;
62511
+ pageSize?: number;
62512
+ modifiedSince?: string;
62513
+ }
62514
+
62515
+ export interface PaginatedExportResult<T> {
62516
+ items: T[];
62517
+ totalCount: number;
62518
+ page: number;
62519
+ pageSize: number;
62520
+ totalPages: number;
62521
+ hasPreviousPage: boolean;
62522
+ hasNextPage: boolean;
62523
+ }
62524
+
62525
+ const EXPORT_ENDPOINT = '/api/v1/export/${namePlural}';
62526
+
62527
+ export const export${name}Api = {
62528
+ /**
62529
+ * Fetch paginated export data for a specific tenant
62530
+ * @param tenantId - Required tenant identifier
62531
+ * @param params - Pagination and filter parameters
62532
+ */
62533
+ async getPage(tenantId: string, params?: Omit<ExportParams, 'tenantId'>): Promise<PaginatedExportResult<Export${name}Dto>> {
62534
+ const response = await apiClient.get<PaginatedExportResult<Export${name}Dto>>(EXPORT_ENDPOINT, {
62535
+ params: { tenantId, ...params }
62536
+ });
62537
+ return response.data;
62538
+ },
62539
+
62540
+ /**
62541
+ * Download all pages as a blob (CSV/JSON) for a specific tenant
62542
+ * @param tenantId - Required tenant identifier
62543
+ * @param format - Output format (default: json)
62544
+ */
62545
+ async downloadAll(tenantId: string, format: 'json' | 'csv' = 'json'): Promise<Blob> {
62546
+ const response = await apiClient.get(EXPORT_ENDPOINT, {
62547
+ params: { tenantId, pageSize: 1000, format },
62548
+ responseType: 'blob',
62549
+ });
62550
+ return response.data;
62551
+ },
62552
+ };
62553
+
62554
+ export default export${name}Api;
62555
+ `;
62556
+ }
62557
+ function formatResult9(result, input) {
62558
+ const lines = [];
62559
+ const namePlural = pluralize(input.name.charAt(0).toLowerCase() + input.name.slice(1));
62560
+ lines.push(`# Scaffold Data Export: ${input.name}`);
62561
+ lines.push("");
62562
+ if (input.options?.dryRun) {
62563
+ lines.push("> **DRY RUN** - No files were written");
62564
+ lines.push("");
62565
+ }
62566
+ lines.push("## Export Endpoint");
62567
+ lines.push("");
62568
+ lines.push(`- **Entity**: \`${input.name}\``);
62569
+ lines.push(`- **NavRoute**: \`${input.navRoute}\``);
62570
+ lines.push(`- **Permission**: \`${input.navRoute}.export\``);
62571
+ lines.push(`- **Endpoint**: \`GET /api/v1/export/${namePlural}\``);
62572
+ lines.push(`- **Rate Limit**: ${input.options?.rateLimitPerMinute ?? 60}/min`);
62573
+ lines.push(`- **Max Page Size**: ${input.options?.maxPageSize ?? 1e3}`);
62574
+ lines.push("");
62575
+ lines.push("## Security Layers");
62576
+ lines.push("");
62577
+ lines.push("1. **Authentication** \u2014 `[Authorize]` + JWT assertion from ExternalApplicationAuthService");
62578
+ lines.push(`2. **Authorization** \u2014 \`[RequirePermission("${input.navRoute}.export")]\``);
62579
+ lines.push("3. **Access Control** \u2014 DataExportAccessMiddleware verifies per-app endpoint access");
62580
+ lines.push(`4. **Rate Limiting** \u2014 \`[EnableRateLimiting("ExternalApp")]\` (${input.options?.rateLimitPerMinute ?? 60}/min)`);
62581
+ lines.push("");
62582
+ lines.push("## Generated Files");
62583
+ lines.push("");
62584
+ for (const file of result.files) {
62585
+ const relativePath2 = file.path.replace(/\\/g, "/");
62586
+ const parts = relativePath2.split("/");
62587
+ const srcIdx = parts.findIndex((p) => ["Controllers", "Application", "Infrastructure", "services", "Persistence"].includes(p));
62588
+ const displayPath = srcIdx >= 0 ? parts.slice(srcIdx).join("/") : parts.slice(-3).join("/");
62589
+ lines.push(`### ${displayPath}`);
62590
+ lines.push("");
62591
+ const ext2 = file.path.endsWith(".ts") ? "typescript" : "csharp";
62592
+ const truncated = file.content.length > 2e3 ? file.content.substring(0, 2e3) + "\n// ... (truncated)" : file.content;
62593
+ lines.push(`\`\`\`${ext2}`);
62594
+ lines.push(truncated);
62595
+ lines.push("```");
62596
+ lines.push("");
62597
+ }
62598
+ lines.push("## Next Steps");
62599
+ lines.push("");
62600
+ for (const instruction of result.instructions) {
62601
+ lines.push(`- ${instruction}`);
62602
+ }
62603
+ return lines.join("\n");
62604
+ }
62605
+ function toPascalCase2(s) {
62606
+ return s.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
62607
+ }
62608
+ function pluralize(s) {
62609
+ if (s.endsWith("y") && !["ay", "ey", "oy", "uy"].some((v) => s.endsWith(v))) {
62610
+ return s.slice(0, -1) + "ies";
62611
+ }
62612
+ if (s.endsWith("s") || s.endsWith("x") || s.endsWith("z") || s.endsWith("ch") || s.endsWith("sh")) {
62613
+ return s + "es";
62614
+ }
62615
+ return s + "s";
62616
+ }
62617
+ var scaffoldDataExportTool;
62618
+ var init_scaffold_data_export = __esm({
62619
+ "src/mcp/tools/scaffold-data-export.ts"() {
62620
+ "use strict";
62621
+ init_esm_shims();
62622
+ init_types3();
62623
+ init_logger();
62624
+ init_detector();
62625
+ init_fs();
62626
+ scaffoldDataExportTool = {
62627
+ name: "scaffold_data_export",
62628
+ description: `Generate Data Export API for a SmartStack entity.
62629
+
62630
+ Creates all code needed to expose an entity via the M2M Data Export API with 3-layer security:
62631
+ - Export Controller (versioned, rate-limited, permission-protected)
62632
+ - Export Service (interface + implementation with tenant isolation)
62633
+ - Export DTO (flat projection for external consumption)
62634
+ - Seed data fragment (DataExportEndpointConfiguration HasData)
62635
+ - Frontend client (optional, TypeScript blob download)
62636
+
62637
+ Example:
62638
+ scaffold_data_export name="Product" navRoute="business.sales.products"
62639
+
62640
+ The generated code follows the External Application & Data Export pattern
62641
+ documented in smartstack-api.md.`,
62642
+ inputSchema: {
62643
+ type: "object",
62644
+ properties: {
62645
+ name: {
62646
+ type: "string",
62647
+ description: 'Entity name in PascalCase (e.g., "Product", "Order")'
62648
+ },
62649
+ navRoute: {
62650
+ type: "string",
62651
+ description: 'NavRoute of the source entity (e.g., "business.sales.products") \u2014 used for permission derivation'
62652
+ },
62653
+ options: {
62654
+ type: "object",
62655
+ properties: {
62656
+ entityProperties: {
62657
+ type: "array",
62658
+ items: {
62659
+ type: "object",
62660
+ properties: {
62661
+ name: { type: "string" },
62662
+ type: { type: "string" },
62663
+ required: { type: "boolean" },
62664
+ maxLength: { type: "number" }
62665
+ },
62666
+ required: ["name", "type"]
62667
+ },
62668
+ description: "Properties for the export DTO"
62669
+ },
62670
+ rateLimitPerMinute: {
62671
+ type: "number",
62672
+ default: 60,
62673
+ description: "Rate limit per minute (default: 60)"
62674
+ },
62675
+ maxPageSize: {
62676
+ type: "number",
62677
+ default: 1e3,
62678
+ description: "Max page size (default: 1000)"
62679
+ },
62680
+ includeAuditFields: {
62681
+ type: "boolean",
62682
+ default: true,
62683
+ description: "Include CreatedAt/UpdatedAt in DTO (default: true)"
62684
+ },
62685
+ includeFrontendClient: {
62686
+ type: "boolean",
62687
+ default: false,
62688
+ description: "Generate TypeScript API client (default: false)"
62689
+ },
62690
+ dryRun: {
62691
+ type: "boolean",
62692
+ default: false,
62693
+ description: "Preview without writing files (default: false)"
62694
+ }
62695
+ }
62696
+ }
62697
+ },
62698
+ required: ["name", "navRoute"]
62699
+ }
62700
+ };
62701
+ }
62702
+ });
62703
+
62704
+ // src/mcp/tools/prompt-data-export-m2m.ts
62705
+ import path26 from "path";
62706
+ async function handlePromptDataExportM2m(args, config2) {
62707
+ const input = PromptDataExportM2mInputSchema.parse(args);
62708
+ logger.info("Generating M2M data export prompt", {
62709
+ endpoints: input.endpoints,
62710
+ clientId: input.clientId ? "***" : void 0,
62711
+ format: input.options?.format
62712
+ });
62713
+ let selectedEndpoints;
62714
+ if (input.endpoints && input.endpoints.length > 0) {
62715
+ selectedEndpoints = KNOWN_ENDPOINTS.filter((e) => input.endpoints.includes(e.code));
62716
+ const unknown2 = input.endpoints.filter((code) => !KNOWN_ENDPOINTS.find((e) => e.code === code));
62717
+ if (unknown2.length > 0) {
62718
+ const discovered = await discoverEndpointsFromProject(config2, unknown2);
62719
+ selectedEndpoints.push(...discovered);
62720
+ const stillUnknown = unknown2.filter((code) => !discovered.find((e) => e.code === code));
62721
+ if (stillUnknown.length > 0) {
62722
+ logger.warn("Unknown endpoint codes", { codes: stillUnknown });
62723
+ }
62724
+ }
62725
+ } else {
62726
+ selectedEndpoints = [...KNOWN_ENDPOINTS];
62727
+ const discovered = await discoverEndpointsFromProject(config2);
62728
+ for (const ep of discovered) {
62729
+ if (!selectedEndpoints.find((e) => e.code === ep.code)) {
62730
+ selectedEndpoints.push(ep);
62731
+ }
62732
+ }
62733
+ }
62734
+ if (selectedEndpoints.length === 0) {
62735
+ return "# No Endpoints Found\n\nNo data export endpoints were found. Use `scaffold_data_export` to create one.";
62736
+ }
62737
+ const format = input.options?.format ?? "full";
62738
+ const includeBase = input.options?.includeBasePrompt !== false;
62739
+ const clientId = input.clientId ?? "<your-client-id>";
62740
+ const prompt = buildPrompt(selectedEndpoints, clientId, format, includeBase);
62741
+ return `# M2M Data Export Integration Prompt
62742
+
62743
+ **Endpoints included**: ${selectedEndpoints.map((e) => e.code).join(", ")}
62744
+ **Format**: ${format}
62745
+
62746
+ ---
62747
+
62748
+ \`\`\`markdown
62749
+ ${prompt}
62750
+ \`\`\`
62751
+
62752
+ ---
62753
+
62754
+ > Copy the content above (inside the code block) and paste it into your VibeCoding session.
62755
+ > The AI will have all the context needed to implement the full M2M client.`;
62756
+ }
62757
+ function buildPrompt(endpoints, clientId, format, includeBase) {
62758
+ const lines = [];
62759
+ if (includeBase && format === "full") {
62760
+ lines.push("## Contexte");
62761
+ lines.push("");
62762
+ lines.push("Tu dois implementer un client HTTP qui consomme l'API DataExport de SmartStack en mode **machine-to-machine** (M2M). Il n'y a aucune interaction utilisateur : ton application s'authentifie avec un `clientId` et un `clientSecret`, obtient un token JWT, puis appelle les endpoints d'export de donnees.");
62763
+ lines.push("");
62764
+ lines.push("---");
62765
+ lines.push("");
62766
+ lines.push("## 1. Authentification (HMAC-JWT Assertion)");
62767
+ lines.push("");
62768
+ lines.push("L'authentification se fait en 2 etapes :");
62769
+ lines.push("");
62770
+ lines.push("### Etape 1 : Creer une assertion JWT signee HMAC-SHA256");
62771
+ lines.push("");
62772
+ lines.push("Ton application doit generer un JWT **signe avec le `clientSecret`** (HMAC-SHA256) :");
62773
+ lines.push("");
62774
+ lines.push("```");
62775
+ lines.push('Header: { "alg": "HS256", "typ": "JWT" }');
62776
+ lines.push("Payload: {");
62777
+ lines.push(` "sub": "${clientId}", // ton clientId`);
62778
+ lines.push(' "iat": <timestamp-unix-actuel>, // timestamp Unix actuel');
62779
+ lines.push(' "exp": <timestamp-unix + 300>, // expiration (max 5 min apres iat)');
62780
+ lines.push(' "jti": "<uuid-unique>" // nonce anti-replay');
62781
+ lines.push("}");
62782
+ lines.push("Signature: HMAC-SHA256(header.payload, Base64Decode(clientSecret))");
62783
+ lines.push("```");
62784
+ lines.push("");
62785
+ lines.push("**Important** : Le `clientSecret` est encode en Base64. Tu dois le **decoder** avant de l'utiliser comme cle HMAC.");
62786
+ lines.push("");
62787
+ lines.push("### Etape 2 : Echanger l'assertion contre un access token");
62788
+ lines.push("");
62789
+ lines.push("```http");
62790
+ lines.push("POST {baseUrl}/api/auth/external-app/token");
62791
+ lines.push("Content-Type: application/json");
62792
+ lines.push("");
62793
+ lines.push("{");
62794
+ lines.push(' "assertion": "<jwt-assertion-de-etape-1>"');
62795
+ lines.push("}");
62796
+ lines.push("```");
62797
+ lines.push("");
62798
+ lines.push("**Reponse succes (200)** :");
62799
+ lines.push("```json");
62800
+ lines.push("{");
62801
+ lines.push(' "accessToken": "eyJhbGciOiJIUz...",');
62802
+ lines.push(' "expiresAt": "2026-02-25T11:00:00Z",');
62803
+ lines.push(' "permissions": ["administration.users.read", ...]');
62804
+ lines.push("}");
62805
+ lines.push("```");
62806
+ lines.push("");
62807
+ lines.push("### Erreurs d'authentification");
62808
+ lines.push("");
62809
+ lines.push("| HTTP | errorCode | Cause |");
62810
+ lines.push("|------|-----------|-------|");
62811
+ lines.push("| 401 | `invalid_assertion` | JWT mal forme ou invalide |");
62812
+ lines.push("| 401 | `missing_client_id` | Claim `sub` absent du JWT |");
62813
+ lines.push("| 401 | `unknown_client` | ClientId inconnu |");
62814
+ lines.push("| 401 | `app_inactive` | Application desactivee |");
62815
+ lines.push("| 401 | `ip_blocked` | IP pas dans la whitelist |");
62816
+ lines.push("| 401 | `invalid_signature` | Signature HMAC invalide |");
62817
+ lines.push("");
62818
+ lines.push("### Regles de gestion du token");
62819
+ lines.push("");
62820
+ lines.push("- Cache le token et renouvelle-le 5 min avant `expiresAt`");
62821
+ lines.push("- N'appelle PAS `/token` a chaque requete");
62822
+ lines.push("");
62823
+ lines.push("---");
62824
+ lines.push("");
62825
+ }
62826
+ lines.push(`## ${format === "full" ? "2" : "1"}. Endpoints a implementer (${endpoints.length})`);
62827
+ lines.push("");
62828
+ for (const ep of endpoints) {
62829
+ lines.push(`### ${ep.name}`);
62830
+ lines.push("");
62831
+ lines.push("```http");
62832
+ lines.push(`${ep.method} {baseUrl}${ep.route}`);
62833
+ lines.push("Authorization: Bearer <accessToken>");
62834
+ lines.push("```");
62835
+ lines.push("");
62836
+ if (ep.params.length > 0) {
62837
+ lines.push("**Parametres (query string)** :");
62838
+ lines.push("");
62839
+ lines.push("| Parametre | Type | Defaut | Description |");
62840
+ lines.push("|-----------|------|--------|-------------|");
62841
+ for (const p of ep.params) {
62842
+ lines.push(`| \`${p.name}\` | ${p.type} | ${p.default} | ${p.description} |`);
62843
+ }
62844
+ lines.push("");
62845
+ } else {
62846
+ lines.push("**Aucun parametre** - appel direct sans query string.");
62847
+ lines.push("");
62848
+ }
62849
+ if (ep.paginated) {
62850
+ lines.push("**Reponse (paginee)** :");
62851
+ lines.push("```json");
62852
+ lines.push("{");
62853
+ lines.push(' "items": [');
62854
+ lines.push(" {");
62855
+ for (let i = 0; i < ep.responseFields.length; i++) {
62856
+ const f = ep.responseFields[i];
62857
+ const comma = i < ep.responseFields.length - 1 ? "," : "";
62858
+ lines.push(` "${f.name}": ${getJsonExample(f.type)}${comma} // ${f.type} - ${f.description}`);
62859
+ }
62860
+ lines.push(" }");
62861
+ lines.push(" ],");
62862
+ lines.push(' "page": 1,');
62863
+ lines.push(' "pageSize": 100,');
62864
+ lines.push(' "totalCount": 250,');
62865
+ lines.push(' "totalPages": 3,');
62866
+ lines.push(' "hasMore": true');
62867
+ lines.push("}");
62868
+ lines.push("```");
62869
+ } else {
62870
+ lines.push("**Reponse (tableau direct, NON pagine)** :");
62871
+ lines.push("```json");
62872
+ lines.push("[");
62873
+ lines.push(" {");
62874
+ for (let i = 0; i < ep.responseFields.length; i++) {
62875
+ const f = ep.responseFields[i];
62876
+ const comma = i < ep.responseFields.length - 1 ? "," : "";
62877
+ lines.push(` "${f.name}": ${getJsonExample(f.type)}${comma} // ${f.type} - ${f.description}`);
62878
+ }
62879
+ lines.push(" }");
62880
+ lines.push("]");
62881
+ lines.push("```");
62882
+ }
62883
+ lines.push("");
62884
+ lines.push(`**Rate limit** : ${ep.defaultRateLimit} requetes/minute | Page size max : ${ep.defaultMaxPageSize}`);
62885
+ if (ep.notes) {
62886
+ lines.push(`**Note** : ${ep.notes}`);
62887
+ }
62888
+ lines.push("");
62889
+ }
62890
+ if (format === "full") {
62891
+ lines.push("---");
62892
+ lines.push("");
62893
+ lines.push("## 3. Isolation par tenant");
62894
+ lines.push("");
62895
+ lines.push("**Important** : Tous les endpoints (sauf `navigation`) necessitent le parametre `tenantId` pour delimiter l'export a un tenant specifique. Cela assure la securite multi-tenant et evite les fuites de donnees.");
62896
+ lines.push("");
62897
+ lines.push("```");
62898
+ lines.push("GET /api/v1/export/users?tenantId=<uuid>&page=1");
62899
+ lines.push("GET /api/v1/export/roles?tenantId=<uuid>&page=1");
62900
+ lines.push("GET /api/v1/export/tickets?tenantId=<uuid>&page=1");
62901
+ lines.push("```");
62902
+ lines.push("");
62903
+ lines.push("Exemple avec boucle :");
62904
+ lines.push("```");
62905
+ lines.push("1. Recuprer la liste de tous les tenants via GET /api/v1/export/navigation");
62906
+ lines.push("2. Pour chaque tenant, exporter ses donnees : users, roles, tickets");
62907
+ lines.push("3. Concatener les resultats ou synchroniser par tenant");
62908
+ lines.push("```");
62909
+ lines.push("");
62910
+ lines.push("---");
62911
+ lines.push("");
62912
+ const hasPaginated = endpoints.some((e) => e.paginated);
62913
+ if (hasPaginated) {
62914
+ lines.push("## 4. Pagination");
62915
+ lines.push("");
62916
+ lines.push("Pour parcourir toutes les pages : boucle tant que `hasMore === true`, en incrementant `page`.");
62917
+ lines.push("Les resultats sont tries par `createdAt ASC` (ordre stable).");
62918
+ lines.push("");
62919
+ lines.push("---");
62920
+ lines.push("");
62921
+ lines.push("## 5. Synchronisation incrementale");
62922
+ lines.push("");
62923
+ lines.push("Utilise `modifiedSince` pour ne recuperer que les changements :");
62924
+ lines.push("1. Premiere synchro : appelle sans `modifiedSince` (recupere tout)");
62925
+ lines.push("2. Stocke `DateTime.UtcNow` du debut de la synchro");
62926
+ lines.push("3. Synchros suivantes : passe `modifiedSince=<timestamp-stocke>`");
62927
+ lines.push("");
62928
+ lines.push("Le filtre retourne les entites ou `UpdatedAt >= modifiedSince OR CreatedAt >= modifiedSince`.");
62929
+ lines.push("");
62930
+ lines.push("---");
62931
+ lines.push("");
62932
+ }
62933
+ lines.push("## 6. Rate Limiting");
62934
+ lines.push("");
62935
+ lines.push("| Aspect | Valeur |");
62936
+ lines.push("|--------|--------|");
62937
+ lines.push("| Algorithme | Fenetre fixe de 1 minute |");
62938
+ lines.push("| Cle de partition | {clientId}:{endpointCode} (independant par endpoint) |");
62939
+ lines.push("");
62940
+ lines.push("Quand tu depasses la limite :");
62941
+ lines.push("```");
62942
+ lines.push("HTTP/1.1 429 Too Many Requests");
62943
+ lines.push("Retry-After: 42");
62944
+ lines.push("```");
62945
+ lines.push("Lis le header `Retry-After` et attends. Implemente un backoff exponentiel.");
62946
+ lines.push("");
62947
+ lines.push("---");
62948
+ lines.push("");
62949
+ lines.push("## 7. Gestion des erreurs");
62950
+ lines.push("");
62951
+ lines.push("| HTTP | Action |");
62952
+ lines.push("|------|--------|");
62953
+ lines.push("| 200 | Traite les donnees |");
62954
+ lines.push("| 400 | TenantId manquant ou invalide - verifie le parametre |");
62955
+ lines.push("| 401 | Renouvelle le token et relance |");
62956
+ lines.push("| 403 | Acces export non accorde - contacte l'admin |");
62957
+ lines.push("| 404 | Tenant non trouve - verifie le tenantId |");
62958
+ lines.push("| 429 | Attends `Retry-After` secondes |");
62959
+ lines.push("| 500 | Retry avec backoff exponentiel |");
62960
+ lines.push("");
62961
+ lines.push("---");
62962
+ lines.push("");
62963
+ lines.push("## 8. Checklist d'implementation");
62964
+ lines.push("");
62965
+ lines.push("### 8.1 Configuration");
62966
+ lines.push("```");
62967
+ lines.push("SMARTSTACK_BASE_URL=https://smartstack.example.com");
62968
+ lines.push(`SMARTSTACK_CLIENT_ID=${clientId}`);
62969
+ lines.push("SMARTSTACK_CLIENT_SECRET=<base64-encoded-secret>");
62970
+ lines.push("TENANT_IDS=<comma-separated-uuids> # Ou recuperer dynamiquement via navigation");
62971
+ lines.push("```");
62972
+ lines.push("Ne hardcode JAMAIS le clientId/clientSecret. Utilise des variables d'environnement ou un vault.");
62973
+ lines.push("");
62974
+ lines.push("### 8.2 Service d'authentification");
62975
+ lines.push("```");
62976
+ lines.push("SmartStackAuthService:");
62977
+ lines.push(" - generateAssertion() \u2192 JWT HMAC-SHA256 signe avec le secret decode du Base64");
62978
+ lines.push(" - getAccessToken() \u2192 POST /api/auth/external-app/token");
62979
+ lines.push(" - cachedToken / expiresAt \u2192 cache le token, renouvelle 5 min avant expiration");
62980
+ lines.push("```");
62981
+ lines.push("");
62982
+ lines.push("### 8.3 Client HTTP generique");
62983
+ lines.push("```");
62984
+ lines.push("SmartStackExportClient:");
62985
+ lines.push(" - fetchExport(endpointCode, tenantId, page?, pageSize?, modifiedSince?, extraParams?)");
62986
+ lines.push(" \u2192 GET /api/v1/export/{endpointCode}?tenantId=<uuid>");
62987
+ lines.push(" - fetchAllPages(endpointCode, tenantId, pageSize?, modifiedSince?)");
62988
+ lines.push(" \u2192 boucle automatique sur toutes les pages avec tenantId");
62989
+ lines.push(" - Gere 400 (tenantId invalide), 401 (retry token), 404 (tenant non trouve), 429 (Retry-After), 5xx (backoff)");
62990
+ lines.push("```");
62991
+ lines.push("");
62992
+ lines.push("### 8.4 Synchroniseur incremental par tenant");
62993
+ lines.push("```");
62994
+ lines.push("SmartStackSyncService:");
62995
+ lines.push(` - endpoints: [${endpoints.map((e) => `"${e.code}"`).join(", ")}]`);
62996
+ lines.push(" - syncAllTenants() \u2192 pour chaque tenant, appelle syncTenant()");
62997
+ lines.push(" - syncTenant(tenantId):");
62998
+ lines.push(" 1. Pour chaque endpoint (sauf navigation):");
62999
+ lines.push(" a. Lit le dernier timestamp de synchro pour ce tenant+endpoint");
63000
+ lines.push(" b. Appelle fetchAllPages(endpoint, tenantId, modifiedSince)");
63001
+ lines.push(" c. Upsert les donnees (INSERT si nouveau, UPDATE si existant, par `id`)");
63002
+ lines.push(" d. Sauve le timestamp");
63003
+ lines.push("```");
63004
+ lines.push("");
63005
+ lines.push("### 8.5 Securite");
63006
+ lines.push("");
63007
+ lines.push("- Stocke le clientSecret dans un vault (Azure Key Vault, etc.)");
63008
+ lines.push("- L'assertion JWT a une duree courte (5 min max)");
63009
+ lines.push("- Ne log jamais le token d'acces");
63010
+ lines.push("- Toutes les requetes sont auditees cote SmartStack (IP, User-Agent)");
63011
+ }
63012
+ return lines.join("\n");
63013
+ }
63014
+ async function discoverEndpointsFromProject(config2, filterCodes) {
63015
+ const discovered = [];
63016
+ try {
63017
+ const structure = await findSmartStackStructure(config2.smartstack.projectPath);
63018
+ if (!structure.api) return discovered;
63019
+ const exportControllerDir = path26.join(structure.api, "Controllers", "DataExport", "v1");
63020
+ const { findFiles: findFiles2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
63021
+ const controllerFiles = await findFiles2(path26.join(exportControllerDir, "Export*Controller.cs"));
63022
+ for (const file of controllerFiles) {
63023
+ const content = await readText(file);
63024
+ if (!content) continue;
63025
+ const routeMatch = content.match(/\[HttpGet\("api\/v1\/export\/([^"]+)"\)\]/);
63026
+ if (!routeMatch) continue;
63027
+ const code = routeMatch[1];
63028
+ if (KNOWN_ENDPOINTS.find((e) => e.code === code)) continue;
63029
+ if (filterCodes && !filterCodes.includes(code)) continue;
63030
+ const classMatch = content.match(/class Export(\w+)Controller/);
63031
+ const entityName = classMatch ? classMatch[1] : code;
63032
+ discovered.push({
63033
+ code,
63034
+ name: `${entityName} Export`,
63035
+ route: `/api/v1/export/${code}`,
63036
+ method: "GET",
63037
+ paginated: content.includes("PaginatedExportResult"),
63038
+ params: [
63039
+ { name: "page", type: "int", default: "1", description: "Numero de page" },
63040
+ { name: "pageSize", type: "int", default: "100", description: "Elements par page" },
63041
+ { name: "modifiedSince", type: "DateTime?", default: "null", description: "Filtre incremental UTC" }
63042
+ ],
63043
+ responseFields: [
63044
+ { name: "id", type: "UUID", description: "Identifiant unique" },
63045
+ { name: "...", type: "...", description: `Voir Export${entityName}Dto pour la structure complete` }
63046
+ ],
63047
+ defaultRateLimit: 60,
63048
+ defaultMaxPageSize: 1e3,
63049
+ notes: `Endpoint decouvert automatiquement. Verifier le DTO Export${entityName}Dto pour la structure exacte.`
63050
+ });
63051
+ }
63052
+ } catch {
63053
+ logger.debug("Could not discover endpoints from project");
63054
+ }
63055
+ return discovered;
63056
+ }
63057
+ function getJsonExample(type) {
63058
+ switch (type) {
63059
+ case "UUID":
63060
+ return '"3fa85f64-5717-4562-b3fc-2c963f66afa6"';
63061
+ case "UUID?":
63062
+ return "null";
63063
+ case "string":
63064
+ return '"..."';
63065
+ case "string?":
63066
+ return '"..."';
63067
+ case "boolean":
63068
+ return "true";
63069
+ case "int":
63070
+ return "0";
63071
+ case "DateTime":
63072
+ return '"2026-01-15T10:30:00Z"';
63073
+ case "DateTime?":
63074
+ return "null";
63075
+ case "string[]":
63076
+ return '["item1", "item2"]';
63077
+ case "UUID[]":
63078
+ return '["uuid1", "uuid2"]';
63079
+ case "object[]":
63080
+ return '[{"id": "...", "code": "...", "label": "..."}]';
63081
+ default:
63082
+ return '"..."';
63083
+ }
63084
+ }
63085
+ var promptDataExportM2mTool, KNOWN_ENDPOINTS;
63086
+ var init_prompt_data_export_m2m = __esm({
63087
+ "src/mcp/tools/prompt-data-export-m2m.ts"() {
63088
+ "use strict";
63089
+ init_esm_shims();
63090
+ init_types3();
63091
+ init_logger();
63092
+ init_detector();
63093
+ init_fs();
63094
+ promptDataExportM2mTool = {
63095
+ name: "prompt_data_export_m2m",
63096
+ description: `Generate a ready-to-use M2M integration prompt for SmartStack Data Export API.
63097
+
63098
+ Creates a complete prompt containing:
63099
+ - HMAC-JWT authentication flow (assertion + token exchange)
63100
+ - Selected endpoint details (route, parameters, response structure)
63101
+ - Pagination and incremental sync patterns
63102
+ - Rate limiting and error handling
63103
+ - Implementation checklist (auth service, HTTP client, sync service)
63104
+
63105
+ The prompt is designed to be pasted into a VibeCoding session (Claude, Cursor, etc.)
63106
+ so the AI can implement the full client without additional context.
63107
+
63108
+ Example:
63109
+ prompt_data_export_m2m endpoints=["users","tenants","roles"]
63110
+ prompt_data_export_m2m endpoints=["users"] clientId="app_abc123"
63111
+
63112
+ If no endpoints specified, scans the project to discover all available export endpoints.`,
63113
+ inputSchema: {
63114
+ type: "object",
63115
+ properties: {
63116
+ endpoints: {
63117
+ type: "array",
63118
+ items: { type: "string" },
63119
+ description: 'Endpoint codes to include (e.g., ["users", "tenants", "roles"]). If empty, discovers all available endpoints.'
63120
+ },
63121
+ clientId: {
63122
+ type: "string",
63123
+ description: 'Optional clientId to pre-fill in the prompt (e.g., "app_abc123...")'
63124
+ },
63125
+ options: {
63126
+ type: "object",
63127
+ properties: {
63128
+ includeBasePrompt: {
63129
+ type: "boolean",
63130
+ default: true,
63131
+ description: "Include authentication and common sections (default: true)"
63132
+ },
63133
+ format: {
63134
+ type: "string",
63135
+ enum: ["full", "endpoints-only"],
63136
+ default: "full",
63137
+ description: 'Output format: "full" includes auth+endpoints+checklist, "endpoints-only" just endpoint details'
63138
+ }
63139
+ }
63140
+ }
63141
+ }
63142
+ }
63143
+ };
63144
+ KNOWN_ENDPOINTS = [
63145
+ {
63146
+ code: "users",
63147
+ name: "Users Export",
63148
+ route: "/api/v1/export/users",
63149
+ method: "GET",
63150
+ paginated: true,
63151
+ params: [
63152
+ { name: "tenantId", type: "UUID", default: "required", description: "ID du tenant pour limiter l'export a ses utilisateurs" },
63153
+ { name: "page", type: "int", default: "1", description: "Numero de page (commence a 1)" },
63154
+ { name: "pageSize", type: "int", default: "100", description: "Elements par page (max 1000)" },
63155
+ { name: "modifiedSince", type: "DateTime?", default: "null", description: "Filtre incremental UTC - entites creees ou modifiees apres cette date" },
63156
+ { name: "isActive", type: "bool?", default: "null", description: "Filtre par statut actif/inactif" }
63157
+ ],
63158
+ responseFields: [
63159
+ { name: "id", type: "UUID", description: "Identifiant unique" },
63160
+ { name: "email", type: "string", description: "Adresse email" },
63161
+ { name: "firstName", type: "string?", description: "Prenom" },
63162
+ { name: "lastName", type: "string?", description: "Nom de famille" },
63163
+ { name: "displayName", type: "string?", description: "Nom affiche" },
63164
+ { name: "isActive", type: "boolean", description: "Compte actif" },
63165
+ { name: "createdAt", type: "DateTime", description: "Date de creation (UTC)" },
63166
+ { name: "updatedAt", type: "DateTime?", description: "Date de derniere modification (UTC)" },
63167
+ { name: "roles", type: "string[]", description: "Noms des roles attribues" },
63168
+ { name: "tenantIds", type: "UUID[]", description: "IDs des tenants associes" }
63169
+ ],
63170
+ defaultRateLimit: 60,
63171
+ defaultMaxPageSize: 1e3
63172
+ },
63173
+ {
63174
+ code: "tenants",
63175
+ name: "Tenants Export",
63176
+ route: "/api/v1/export/tenants",
63177
+ method: "GET",
63178
+ paginated: true,
63179
+ params: [
63180
+ { name: "tenantId", type: "UUID", default: "required", description: "ID du tenant a exporter - retourne le tenant specifie et ses donnees associees" },
63181
+ { name: "page", type: "int", default: "1", description: "Numero de page (commence a 1)" },
63182
+ { name: "pageSize", type: "int", default: "100", description: "Elements par page (max 1000)" },
63183
+ { name: "modifiedSince", type: "DateTime?", default: "null", description: "Filtre incremental UTC" }
63184
+ ],
63185
+ responseFields: [
63186
+ { name: "id", type: "UUID", description: "Identifiant unique" },
63187
+ { name: "name", type: "string", description: "Nom du tenant" },
63188
+ { name: "slug", type: "string", description: "Slug URL-friendly" },
63189
+ { name: "type", type: "string", description: "Type de tenant" },
63190
+ { name: "status", type: "string", description: "Statut (Active, Suspended, etc.)" },
63191
+ { name: "description", type: "string?", description: "Description" },
63192
+ { name: "createdAt", type: "DateTime", description: "Date de creation (UTC)" },
63193
+ { name: "updatedAt", type: "DateTime?", description: "Date de derniere modification (UTC)" },
63194
+ { name: "memberCount", type: "int", description: "Nombre de membres" }
63195
+ ],
63196
+ defaultRateLimit: 60,
63197
+ defaultMaxPageSize: 1e3
63198
+ },
63199
+ {
63200
+ code: "roles",
63201
+ name: "Roles Export",
63202
+ route: "/api/v1/export/roles",
63203
+ method: "GET",
63204
+ paginated: true,
63205
+ params: [
63206
+ { name: "tenantId", type: "UUID", default: "required", description: "ID du tenant pour limiter l'export a ses roles" },
63207
+ { name: "page", type: "int", default: "1", description: "Numero de page (commence a 1)" },
63208
+ { name: "pageSize", type: "int", default: "100", description: "Elements par page (max 1000)" },
63209
+ { name: "modifiedSince", type: "DateTime?", default: "null", description: "Filtre incremental UTC" }
63210
+ ],
63211
+ responseFields: [
63212
+ { name: "id", type: "UUID", description: "Identifiant unique" },
63213
+ { name: "name", type: "string", description: "Nom du role" },
63214
+ { name: "shortName", type: "string?", description: "Nom court" },
63215
+ { name: "code", type: "string?", description: "Code technique" },
63216
+ { name: "category", type: "string", description: "Categorie (Global, Admin, Manager, Contributor, Viewer, Custom)" },
63217
+ { name: "description", type: "string?", description: "Description du role" },
63218
+ { name: "isSystem", type: "boolean", description: "Role systeme (non modifiable)" },
63219
+ { name: "createdAt", type: "DateTime", description: "Date de creation (UTC)" },
63220
+ { name: "updatedAt", type: "DateTime?", description: "Date de derniere modification (UTC)" },
63221
+ { name: "permissions", type: "string[]", description: "Codes des permissions attribuees" }
63222
+ ],
63223
+ defaultRateLimit: 60,
63224
+ defaultMaxPageSize: 1e3
63225
+ },
63226
+ {
63227
+ code: "tickets",
63228
+ name: "Tickets Export",
63229
+ route: "/api/v1/export/tickets",
63230
+ method: "GET",
63231
+ paginated: true,
63232
+ params: [
63233
+ { name: "tenantId", type: "UUID", default: "required", description: "ID du tenant pour limiter l'export a ses tickets" },
63234
+ { name: "page", type: "int", default: "1", description: "Numero de page (commence a 1)" },
63235
+ { name: "pageSize", type: "int", default: "100", description: "Elements par page (max 500)" },
63236
+ { name: "modifiedSince", type: "DateTime?", default: "null", description: "Filtre incremental UTC" }
63237
+ ],
63238
+ responseFields: [
63239
+ { name: "id", type: "UUID", description: "Identifiant unique" },
63240
+ { name: "title", type: "string", description: "Titre du ticket" },
63241
+ { name: "type", type: "string", description: "Type (Bug, Feature, Support, etc.)" },
63242
+ { name: "status", type: "string", description: "Statut (Open, InProgress, Resolved, Closed)" },
63243
+ { name: "priority", type: "string", description: "Priorite (Low, Medium, High, Critical)" },
63244
+ { name: "assignedToUserId", type: "UUID?", description: "ID utilisateur assigne" },
63245
+ { name: "createdByUserId", type: "UUID", description: "ID createur" },
63246
+ { name: "tenantId", type: "UUID?", description: "ID tenant associe" },
63247
+ { name: "createdAt", type: "DateTime", description: "Date de creation (UTC)" },
63248
+ { name: "updatedAt", type: "DateTime?", description: "Date de derniere modification (UTC)" },
63249
+ { name: "resolvedAt", type: "DateTime?", description: "Date de resolution (UTC)" },
63250
+ { name: "closedAt", type: "DateTime?", description: "Date de fermeture (UTC)" }
63251
+ ],
63252
+ defaultRateLimit: 30,
63253
+ defaultMaxPageSize: 500,
63254
+ notes: "Rate limit reduit a 30 req/min. Page size max 500."
63255
+ },
63256
+ {
63257
+ code: "navigation",
63258
+ name: "Navigation Export",
63259
+ route: "/api/v1/export/navigation",
63260
+ method: "GET",
63261
+ paginated: false,
63262
+ params: [],
63263
+ responseFields: [
63264
+ { name: "id", type: "UUID", description: "Identifiant de l'application" },
63265
+ { name: "code", type: "string", description: "Code technique de l'application" },
63266
+ { name: "label", type: "string", description: "Libelle affiche" },
63267
+ { name: "modules", type: "object[]", description: "Liste des modules (chacun: id, code, label)" }
63268
+ ],
63269
+ defaultRateLimit: 60,
63270
+ defaultMaxPageSize: 1e3,
63271
+ notes: "Endpoint NON pagine. Retourne directement un tableau JSON (pas d'envelope de pagination). Contient la structure de navigation : applications avec leurs modules."
63272
+ }
63273
+ ];
63274
+ }
63275
+ });
63276
+
63277
+ // src/mcp/tools/analyze-hierarchy-patterns.ts
63278
+ import path27 from "path";
62096
63279
  async function handleAnalyzeHierarchyPatterns(args, config2) {
62097
63280
  const input = AnalyzeHierarchyPatternsInputSchema.parse(args);
62098
63281
  const projectPath = input.path || config2.smartstack.projectPath;
@@ -62115,7 +63298,7 @@ async function handleAnalyzeHierarchyPatterns(args, config2) {
62115
63298
  recommendations,
62116
63299
  circularDependencies: detectCircularDependencies(entityGraph)
62117
63300
  };
62118
- return formatResult9(result, outputFormat, structure.root);
63301
+ return formatResult10(result, outputFormat, structure.root);
62119
63302
  }
62120
63303
  async function buildEntityGraph(domainPath, filter3) {
62121
63304
  const entityFiles = await findFiles("**/*.cs", { cwd: domainPath });
@@ -62171,7 +63354,7 @@ async function buildEntityGraph(domainPath, filter3) {
62171
63354
  return entityMap;
62172
63355
  }
62173
63356
  function parseEntity(content, file, domainPath) {
62174
- const fileName = path25.basename(file, ".cs");
63357
+ const fileName = path27.basename(file, ".cs");
62175
63358
  if (fileName.endsWith("Dto") || fileName.endsWith("Command") || fileName.endsWith("Query") || fileName.endsWith("Handler") || fileName.endsWith("Validator") || fileName.endsWith("Exception") || fileName.startsWith("I")) {
62176
63359
  return null;
62177
63360
  }
@@ -62215,7 +63398,7 @@ function parseEntity(content, file, domainPath) {
62215
63398
  const parentEntity = foreignKeys.length > 0 ? foreignKeys[0].referencedEntity : void 0;
62216
63399
  return {
62217
63400
  name: entityName,
62218
- file: path25.relative(domainPath, file),
63401
+ file: path27.relative(domainPath, file),
62219
63402
  baseClass: hasSystemEntity ? "SystemEntity" : "BaseEntity",
62220
63403
  isTenantAware: hasITenantEntity,
62221
63404
  isSystemEntity: hasSystemEntity,
@@ -62246,24 +63429,24 @@ function detectCircularDependencies(entityMap) {
62246
63429
  const cycles = [];
62247
63430
  for (const [name] of entityMap) {
62248
63431
  let dfs2 = function(current) {
62249
- if (path30.includes(current)) {
62250
- const cycleStart = path30.indexOf(current);
62251
- cycles.push([...path30.slice(cycleStart), current]);
63432
+ if (path32.includes(current)) {
63433
+ const cycleStart = path32.indexOf(current);
63434
+ cycles.push([...path32.slice(cycleStart), current]);
62252
63435
  return true;
62253
63436
  }
62254
63437
  if (visited.has(current)) return false;
62255
63438
  visited.add(current);
62256
- path30.push(current);
63439
+ path32.push(current);
62257
63440
  const node = entityMap.get(current);
62258
63441
  if (node?.parent) {
62259
63442
  dfs2(node.parent);
62260
63443
  }
62261
- path30.pop();
63444
+ path32.pop();
62262
63445
  return false;
62263
63446
  };
62264
63447
  var dfs = dfs2;
62265
63448
  const visited = /* @__PURE__ */ new Set();
62266
- const path30 = [];
63449
+ const path32 = [];
62267
63450
  dfs2(name);
62268
63451
  }
62269
63452
  const unique = /* @__PURE__ */ new Map();
@@ -62470,7 +63653,7 @@ function getImplementationSteps(pattern, node) {
62470
63653
  return [];
62471
63654
  }
62472
63655
  }
62473
- function formatResult9(result, format, _rootPath) {
63656
+ function formatResult10(result, format, _rootPath) {
62474
63657
  if (format === "json") {
62475
63658
  return JSON.stringify(result, null, 2);
62476
63659
  }
@@ -64875,13 +66058,13 @@ async function getFilesToAnalyze(projectPath, scope, _structure) {
64875
66058
  try {
64876
66059
  const fullPath = filePath.startsWith(projectPath) ? filePath : `${projectPath}/${filePath}`;
64877
66060
  const content = await readText(fullPath);
64878
- const relativePath = filePath.replace(projectPath, "").replace(/^[/\\]/, "");
66061
+ const relativePath2 = filePath.replace(projectPath, "").replace(/^[/\\]/, "");
64879
66062
  files.push({
64880
66063
  path: fullPath,
64881
- relativePath,
66064
+ relativePath: relativePath2,
64882
66065
  content,
64883
66066
  language: detectLanguage(filePath),
64884
- layer: detectLayer(relativePath),
66067
+ layer: detectLayer(relativePath2),
64885
66068
  lineCount: content.split("\n").length
64886
66069
  });
64887
66070
  } catch {
@@ -64911,8 +66094,8 @@ function detectLanguage(filePath) {
64911
66094
  return "other";
64912
66095
  }
64913
66096
  }
64914
- function detectLayer(relativePath) {
64915
- const lower = relativePath.toLowerCase();
66097
+ function detectLayer(relativePath2) {
66098
+ const lower = relativePath2.toLowerCase();
64916
66099
  if (lower.includes("/domain/") || lower.includes("\\domain\\")) return "domain";
64917
66100
  if (lower.includes("/application/") || lower.includes("\\application\\")) return "application";
64918
66101
  if (lower.includes("/infrastructure/") || lower.includes("\\infrastructure\\")) return "infrastructure";
@@ -66412,7 +67595,7 @@ var init_conventions = __esm({
66412
67595
  });
66413
67596
 
66414
67597
  // src/mcp/resources/project-info.ts
66415
- import path26 from "path";
67598
+ import path28 from "path";
66416
67599
  async function getProjectInfoResource(config2) {
66417
67600
  const projectPath = config2.smartstack.projectPath;
66418
67601
  const projectInfo = await detectProject(projectPath);
@@ -66443,16 +67626,16 @@ async function getProjectInfoResource(config2) {
66443
67626
  lines.push("```");
66444
67627
  lines.push(`${projectInfo.name}/`);
66445
67628
  if (structure.domain) {
66446
- lines.push(`\u251C\u2500\u2500 ${path26.basename(structure.domain)}/ # Domain layer (entities)`);
67629
+ lines.push(`\u251C\u2500\u2500 ${path28.basename(structure.domain)}/ # Domain layer (entities)`);
66447
67630
  }
66448
67631
  if (structure.application) {
66449
- lines.push(`\u251C\u2500\u2500 ${path26.basename(structure.application)}/ # Application layer (services)`);
67632
+ lines.push(`\u251C\u2500\u2500 ${path28.basename(structure.application)}/ # Application layer (services)`);
66450
67633
  }
66451
67634
  if (structure.infrastructure) {
66452
- lines.push(`\u251C\u2500\u2500 ${path26.basename(structure.infrastructure)}/ # Infrastructure (EF Core)`);
67635
+ lines.push(`\u251C\u2500\u2500 ${path28.basename(structure.infrastructure)}/ # Infrastructure (EF Core)`);
66453
67636
  }
66454
67637
  if (structure.api) {
66455
- lines.push(`\u251C\u2500\u2500 ${path26.basename(structure.api)}/ # API layer (controllers)`);
67638
+ lines.push(`\u251C\u2500\u2500 ${path28.basename(structure.api)}/ # API layer (controllers)`);
66456
67639
  }
66457
67640
  if (structure.web) {
66458
67641
  lines.push(`\u2514\u2500\u2500 web/smartstack-web/ # React frontend`);
@@ -66465,9 +67648,9 @@ async function getProjectInfoResource(config2) {
66465
67648
  lines.push("| Project | Path |");
66466
67649
  lines.push("|---------|------|");
66467
67650
  for (const csproj of projectInfo.csprojFiles) {
66468
- const name = path26.basename(csproj, ".csproj");
66469
- const relativePath = path26.relative(projectPath, csproj);
66470
- lines.push(`| ${name} | \`${relativePath}\` |`);
67651
+ const name = path28.basename(csproj, ".csproj");
67652
+ const relativePath2 = path28.relative(projectPath, csproj);
67653
+ lines.push(`| ${name} | \`${relativePath2}\` |`);
66471
67654
  }
66472
67655
  lines.push("");
66473
67656
  }
@@ -66476,10 +67659,10 @@ async function getProjectInfoResource(config2) {
66476
67659
  cwd: structure.migrations,
66477
67660
  ignore: ["*.Designer.cs"]
66478
67661
  });
66479
- const migrations = migrationFiles.map((f) => path26.basename(f)).filter((f) => !f.includes("ModelSnapshot") && !f.includes(".Designer.")).sort();
67662
+ const migrations = migrationFiles.map((f) => path28.basename(f)).filter((f) => !f.includes("ModelSnapshot") && !f.includes(".Designer.")).sort();
66480
67663
  lines.push("## EF Core Migrations");
66481
67664
  lines.push("");
66482
- lines.push(`**Location**: \`${path26.relative(projectPath, structure.migrations)}\``);
67665
+ lines.push(`**Location**: \`${path28.relative(projectPath, structure.migrations)}\``);
66483
67666
  lines.push(`**Total Migrations**: ${migrations.length}`);
66484
67667
  lines.push("");
66485
67668
  if (migrations.length > 0) {
@@ -66514,11 +67697,11 @@ async function getProjectInfoResource(config2) {
66514
67697
  lines.push("dotnet build");
66515
67698
  lines.push("");
66516
67699
  lines.push("# Run API");
66517
- lines.push(`cd ${structure.api ? path26.relative(projectPath, structure.api) : "src/Api"}`);
67700
+ lines.push(`cd ${structure.api ? path28.relative(projectPath, structure.api) : "src/Api"}`);
66518
67701
  lines.push("dotnet run");
66519
67702
  lines.push("");
66520
67703
  lines.push("# Run frontend");
66521
- lines.push(`cd ${structure.web ? path26.relative(projectPath, structure.web) : "web"}`);
67704
+ lines.push(`cd ${structure.web ? path28.relative(projectPath, structure.web) : "web"}`);
66522
67705
  lines.push("npm run dev");
66523
67706
  lines.push("");
66524
67707
  lines.push("# Create migration");
@@ -66556,7 +67739,7 @@ var init_project_info = __esm({
66556
67739
  });
66557
67740
 
66558
67741
  // src/mcp/resources/api-endpoints.ts
66559
- import path27 from "path";
67742
+ import path29 from "path";
66560
67743
  async function getApiEndpointsResource(config2, endpointFilter) {
66561
67744
  const structure = await findSmartStackStructure(config2.smartstack.projectPath);
66562
67745
  if (!structure.api) {
@@ -66575,7 +67758,7 @@ async function getApiEndpointsResource(config2, endpointFilter) {
66575
67758
  }
66576
67759
  async function parseController(filePath, _rootPath) {
66577
67760
  const content = await readText(filePath);
66578
- const fileName = path27.basename(filePath, ".cs");
67761
+ const fileName = path29.basename(filePath, ".cs");
66579
67762
  const controllerName = fileName.replace("Controller", "");
66580
67763
  const endpoints = [];
66581
67764
  const routeMatch = content.match(/\[Route\s*\(\s*"([^"]+)"\s*\)\]/);
@@ -66737,7 +67920,7 @@ var init_api_endpoints = __esm({
66737
67920
  });
66738
67921
 
66739
67922
  // src/mcp/resources/db-schema.ts
66740
- import path28 from "path";
67923
+ import path30 from "path";
66741
67924
  async function getDbSchemaResource(config2, tableFilter) {
66742
67925
  const structure = await findSmartStackStructure(config2.smartstack.projectPath);
66743
67926
  if (!structure.domain && !structure.infrastructure) {
@@ -66821,7 +68004,7 @@ async function parseEntity2(filePath, rootPath, _config) {
66821
68004
  tableName,
66822
68005
  properties,
66823
68006
  relationships,
66824
- file: path28.relative(rootPath, filePath)
68007
+ file: path30.relative(rootPath, filePath)
66825
68008
  };
66826
68009
  }
66827
68010
  async function enrichFromConfigurations(entities, infrastructurePath, _config) {
@@ -66982,7 +68165,7 @@ var init_db_schema = __esm({
66982
68165
  });
66983
68166
 
66984
68167
  // src/mcp/resources/entities.ts
66985
- import path29 from "path";
68168
+ import path31 from "path";
66986
68169
  async function getEntitiesResource(config2, entityFilter) {
66987
68170
  const structure = await findSmartStackStructure(config2.smartstack.projectPath);
66988
68171
  if (!structure.domain) {
@@ -67036,7 +68219,7 @@ async function parseEntitySummary(filePath, rootPath, config2) {
67036
68219
  hasSoftDelete,
67037
68220
  hasRowVersion,
67038
68221
  file: filePath,
67039
- relativePath: path29.relative(rootPath, filePath)
68222
+ relativePath: path31.relative(rootPath, filePath)
67040
68223
  };
67041
68224
  }
67042
68225
  function inferTableInfo(entityName, config2) {
@@ -67094,11 +68277,11 @@ function inferTableInfo(entityName, config2) {
67094
68277
  if (!validPrefixes.includes(tablePrefix)) {
67095
68278
  tablePrefix = "ref_";
67096
68279
  }
67097
- const tableName = `${tablePrefix}${pluralize(entityName)}`;
68280
+ const tableName = `${tablePrefix}${pluralize2(entityName)}`;
67098
68281
  const schema = config2.conventions.schemas.platform;
67099
68282
  return { tableName, tablePrefix, schema };
67100
68283
  }
67101
- function pluralize(name) {
68284
+ function pluralize2(name) {
67102
68285
  if (name.endsWith("y") && !/[aeiou]y$/i.test(name)) {
67103
68286
  return name.slice(0, -1) + "ies";
67104
68287
  }
@@ -67254,6 +68437,9 @@ async function createServer() {
67254
68437
  // Frontend Extension Tools
67255
68438
  scaffoldFrontendExtensionTool,
67256
68439
  analyzeExtensionPointsTool,
68440
+ // Data Export Tools
68441
+ scaffoldDataExportTool,
68442
+ promptDataExportM2mTool,
67257
68443
  // Security & Code Quality Tools
67258
68444
  validateSecurityTool,
67259
68445
  analyzeCodeQualityTool,
@@ -67323,6 +68509,13 @@ async function createServer() {
67323
68509
  case "analyze_extension_points":
67324
68510
  result = await handleAnalyzeExtensionPoints(args ?? {}, config2);
67325
68511
  break;
68512
+ // Data Export Tools
68513
+ case "scaffold_data_export":
68514
+ result = await handleScaffoldDataExport(args ?? {}, config2);
68515
+ break;
68516
+ case "prompt_data_export_m2m":
68517
+ result = await handlePromptDataExportM2m(args ?? {}, config2);
68518
+ break;
67326
68519
  // Security & Code Quality Tools
67327
68520
  case "validate_security":
67328
68521
  result = await handleValidateSecurity(args ?? {}, config2);
@@ -67451,6 +68644,8 @@ var init_server3 = __esm({
67451
68644
  init_scaffold_frontend_tests();
67452
68645
  init_validate_security();
67453
68646
  init_analyze_code_quality();
68647
+ init_scaffold_data_export();
68648
+ init_prompt_data_export_m2m();
67454
68649
  init_analyze_hierarchy_patterns();
67455
68650
  init_review_code();
67456
68651
  init_conventions();