@hyphaene/hexa-ts-kit 1.4.2 → 1.5.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.
@@ -1,5 +1,5 @@
1
1
  // src/commands/lint.ts
2
- import { resolve as resolve2 } from "path";
2
+ import { resolve as resolve3 } from "path";
3
3
  import { execSync } from "child_process";
4
4
 
5
5
  // src/lint/checkers/structure/colocation.ts
@@ -751,9 +751,57 @@ var rulesRegistry = {
751
751
  invalid: "contracts/CTR-007-invalid.ts",
752
752
  valid: "contracts/CTR-007-valid.ts"
753
753
  }
754
+ },
755
+ // ==========================================================================
756
+ // Vault Schema Rules (VAULT-*) - Critical rules for AST parsing
757
+ // ==========================================================================
758
+ "VAULT-IMP-001": {
759
+ title: "External schema import",
760
+ severity: "error",
761
+ category: "vault",
762
+ why: "External imports break static AST parsing. Duplicate schema with @sync-check.",
763
+ autoFixable: false
764
+ },
765
+ "VAULT-AST-001": {
766
+ title: "Forbidden z.lazy()",
767
+ severity: "error",
768
+ category: "vault",
769
+ why: "z.lazy() creates recursive schemas that cannot be parsed statically.",
770
+ autoFixable: false
771
+ },
772
+ "VAULT-AST-002": {
773
+ title: "Forbidden conditional spread",
774
+ severity: "error",
775
+ category: "vault",
776
+ why: "Conditional spreads make schema structure non-deterministic.",
777
+ autoFixable: false
778
+ },
779
+ "VAULT-AST-003": {
780
+ title: "Forbidden function generator",
781
+ severity: "error",
782
+ category: "vault",
783
+ why: "Schemas returned by functions cannot be parsed statically.",
784
+ autoFixable: false
785
+ },
786
+ "VAULT-AST-004": {
787
+ title: "Enum with non-literal values",
788
+ severity: "error",
789
+ category: "vault",
790
+ why: "z.enum() must contain only string literals, not variables.",
791
+ autoFixable: false
792
+ },
793
+ "VAULT-AST-006": {
794
+ title: "z.object() with non-literal argument",
795
+ severity: "error",
796
+ category: "vault",
797
+ why: "z.object() must receive an object literal, not a variable.",
798
+ autoFixable: false
754
799
  }
755
800
  };
756
801
  var allRuleIds = Object.keys(rulesRegistry);
802
+ function getRuleSeverity(ruleId) {
803
+ return rulesRegistry[ruleId].severity;
804
+ }
757
805
  function getRulesByCategory(category) {
758
806
  return allRuleIds.filter((id) => rulesRegistry[id].category === category);
759
807
  }
@@ -2135,6 +2183,205 @@ var contractsChecker = {
2135
2183
  }
2136
2184
  };
2137
2185
 
2186
+ // src/lint/checkers/vault/index.ts
2187
+ import { existsSync as existsSync5, readdirSync } from "fs";
2188
+ import { join as join3, dirname as dirname3, resolve as resolve2 } from "path";
2189
+ import { Project, SyntaxKind } from "ts-morph";
2190
+ var VAULT_PATHS = [
2191
+ "src/common/config/vault",
2192
+ "server/vault",
2193
+ "src/config/vault"
2194
+ ];
2195
+ var LEGACY_FILES = ["server/vault.ts", "src/vault.ts"];
2196
+ function findVaultLocation(cwd) {
2197
+ for (const path of VAULT_PATHS) {
2198
+ const fullPath = join3(cwd, path);
2199
+ if (existsSync5(fullPath)) {
2200
+ const files = readdirSync(fullPath).filter((f) => f.endsWith(".ts")).map((f) => join3(fullPath, f));
2201
+ if (files.length > 0) {
2202
+ return { basePath: fullPath, files };
2203
+ }
2204
+ }
2205
+ }
2206
+ for (const file of LEGACY_FILES) {
2207
+ const fullPath = join3(cwd, file);
2208
+ if (existsSync5(fullPath)) {
2209
+ return { basePath: dirname3(fullPath), files: [fullPath] };
2210
+ }
2211
+ }
2212
+ return null;
2213
+ }
2214
+ function isInsideZodObject(node) {
2215
+ if (!node) return false;
2216
+ let current = node;
2217
+ while (current) {
2218
+ if (current.getKind() === SyntaxKind.CallExpression) {
2219
+ const text = current.getText();
2220
+ if (text.startsWith("z.object(")) return true;
2221
+ }
2222
+ current = current.getParent();
2223
+ }
2224
+ return false;
2225
+ }
2226
+ function checkExternalImports(sourceFile, vault) {
2227
+ const results = [];
2228
+ const filePath = sourceFile.getFilePath();
2229
+ sourceFile.getImportDeclarations().forEach((imp) => {
2230
+ const moduleSpec = imp.getModuleSpecifierValue();
2231
+ const namedImports = imp.getNamedImports();
2232
+ if (moduleSpec === "zod") return;
2233
+ if (moduleSpec.startsWith(".")) {
2234
+ const resolvedPath = resolve2(dirname3(filePath), moduleSpec);
2235
+ if (!resolvedPath.startsWith(vault.basePath)) {
2236
+ const hasSchemaImport = namedImports.some(
2237
+ (ni) => ni.getName().toLowerCase().includes("schema")
2238
+ );
2239
+ if (hasSchemaImport) {
2240
+ results.push({
2241
+ ruleId: "VAULT-IMP-001",
2242
+ severity: getRuleSeverity("VAULT-IMP-001"),
2243
+ message: `External schema import from "${moduleSpec}"`,
2244
+ file: filePath,
2245
+ line: imp.getStartLineNumber(),
2246
+ suggestion: "Move schema to vault/ or duplicate with @sync-check"
2247
+ });
2248
+ }
2249
+ }
2250
+ }
2251
+ if (!moduleSpec.startsWith(".")) {
2252
+ const hasSchemaImport = namedImports.some(
2253
+ (ni) => ni.getName().toLowerCase().includes("schema")
2254
+ );
2255
+ if (hasSchemaImport) {
2256
+ results.push({
2257
+ ruleId: "VAULT-IMP-001",
2258
+ severity: getRuleSeverity("VAULT-IMP-001"),
2259
+ message: `External schema import from package "${moduleSpec}"`,
2260
+ file: filePath,
2261
+ line: imp.getStartLineNumber(),
2262
+ suggestion: "Define schema locally in vault/ instead"
2263
+ });
2264
+ }
2265
+ }
2266
+ });
2267
+ return results;
2268
+ }
2269
+ function checkAstPatterns(sourceFile) {
2270
+ const results = [];
2271
+ const filePath = sourceFile.getFilePath();
2272
+ sourceFile.forEachDescendant((node) => {
2273
+ if (node.getKind() === SyntaxKind.CallExpression) {
2274
+ const text = node.getText();
2275
+ if (text.startsWith("z.lazy(")) {
2276
+ results.push({
2277
+ ruleId: "VAULT-AST-001",
2278
+ severity: getRuleSeverity("VAULT-AST-001"),
2279
+ message: "z.lazy() cannot be parsed statically",
2280
+ file: filePath,
2281
+ line: node.getStartLineNumber()
2282
+ });
2283
+ }
2284
+ if (text.startsWith("z.enum(")) {
2285
+ const callExpr = node.asKind(SyntaxKind.CallExpression);
2286
+ const args = callExpr?.getArguments();
2287
+ if (args && args.length > 0) {
2288
+ const firstArg = args[0];
2289
+ if (firstArg?.getKind() === SyntaxKind.Identifier) {
2290
+ results.push({
2291
+ ruleId: "VAULT-AST-004",
2292
+ severity: getRuleSeverity("VAULT-AST-004"),
2293
+ message: "z.enum() must receive literal array, not variable",
2294
+ file: filePath,
2295
+ line: node.getStartLineNumber(),
2296
+ suggestion: "Use z.enum(['value1', 'value2']) with literals"
2297
+ });
2298
+ }
2299
+ }
2300
+ }
2301
+ if (text.startsWith("z.object(")) {
2302
+ const callExpr = node.asKind(SyntaxKind.CallExpression);
2303
+ const args = callExpr?.getArguments();
2304
+ if (args && args.length > 0) {
2305
+ const firstArg = args[0];
2306
+ if (firstArg?.getKind() === SyntaxKind.Identifier) {
2307
+ results.push({
2308
+ ruleId: "VAULT-AST-006",
2309
+ severity: getRuleSeverity("VAULT-AST-006"),
2310
+ message: "z.object() must receive object literal, not variable",
2311
+ file: filePath,
2312
+ line: node.getStartLineNumber()
2313
+ });
2314
+ }
2315
+ }
2316
+ }
2317
+ }
2318
+ if (node.getKind() === SyntaxKind.SpreadAssignment) {
2319
+ if (isInsideZodObject(node)) {
2320
+ const spreadExpr = node.asKind(SyntaxKind.SpreadAssignment);
2321
+ const expr = spreadExpr?.getExpression();
2322
+ if (expr?.getKind() === SyntaxKind.ConditionalExpression || expr?.getKind() === SyntaxKind.Identifier) {
2323
+ results.push({
2324
+ ruleId: "VAULT-AST-002",
2325
+ severity: getRuleSeverity("VAULT-AST-002"),
2326
+ message: "Conditional/dynamic spreads not parsable in z.object()",
2327
+ file: filePath,
2328
+ line: node.getStartLineNumber()
2329
+ });
2330
+ }
2331
+ }
2332
+ }
2333
+ if (node.getKind() === SyntaxKind.FunctionDeclaration || node.getKind() === SyntaxKind.ArrowFunction) {
2334
+ const funcNode = node.asKind(SyntaxKind.FunctionDeclaration) || node.asKind(SyntaxKind.ArrowFunction);
2335
+ if (funcNode) {
2336
+ const body = funcNode.getBody();
2337
+ if (body) {
2338
+ const bodyText = body.getText();
2339
+ if (bodyText.includes("z.object(") || bodyText.includes("return z.")) {
2340
+ results.push({
2341
+ ruleId: "VAULT-AST-003",
2342
+ severity: getRuleSeverity("VAULT-AST-003"),
2343
+ message: "Functions returning Zod schemas are not parsable",
2344
+ file: filePath,
2345
+ line: node.getStartLineNumber(),
2346
+ suggestion: "Define schemas as const, not functions"
2347
+ });
2348
+ }
2349
+ }
2350
+ }
2351
+ }
2352
+ });
2353
+ return results;
2354
+ }
2355
+ var vaultChecker = {
2356
+ name: "vault",
2357
+ rules: getRulesByCategory("vault"),
2358
+ async check(ctx) {
2359
+ const results = [];
2360
+ const vault = findVaultLocation(ctx.cwd);
2361
+ if (!vault) {
2362
+ return results;
2363
+ }
2364
+ const project = new Project({
2365
+ skipAddingFilesFromTsConfig: true,
2366
+ skipFileDependencyResolution: true
2367
+ });
2368
+ const sourceFiles = [];
2369
+ for (const filePath of vault.files) {
2370
+ try {
2371
+ const sf = project.addSourceFileAtPath(filePath);
2372
+ sourceFiles.push(sf);
2373
+ } catch {
2374
+ continue;
2375
+ }
2376
+ }
2377
+ for (const sourceFile of sourceFiles) {
2378
+ results.push(...checkExternalImports(sourceFile, vault));
2379
+ results.push(...checkAstPatterns(sourceFile));
2380
+ }
2381
+ return results;
2382
+ }
2383
+ };
2384
+
2138
2385
  // src/lint/reporters/console.ts
2139
2386
  import pc from "picocolors";
2140
2387
  var severityColors = {
@@ -2195,6 +2442,9 @@ function formatSummary(results) {
2195
2442
 
2196
2443
  // src/commands/lint.ts
2197
2444
  var contractsLibCheckers = [contractsChecker];
2445
+ var standaloneCheckers = {
2446
+ VAULT: vaultChecker
2447
+ };
2198
2448
  function detectProjectType(cwd) {
2199
2449
  const configResult = loadConfig(cwd);
2200
2450
  if (!configResult.success) {
@@ -2253,7 +2503,23 @@ function getChangedFiles(cwd) {
2253
2503
  }
2254
2504
  }
2255
2505
  async function lintCore(options) {
2256
- const cwd = resolve2(options.path || ".");
2506
+ const cwd = resolve3(options.path || ".");
2507
+ if (options.rules) {
2508
+ const standaloneKey = options.rules.toUpperCase();
2509
+ if (standaloneCheckers[standaloneKey]) {
2510
+ const checker = standaloneCheckers[standaloneKey];
2511
+ const results = await checker.check({ cwd, files: [] });
2512
+ const filteredResults2 = options.quiet ? results.filter((r) => r.severity === "error") : results;
2513
+ const summary2 = formatSummary(results);
2514
+ return {
2515
+ success: summary2.errors === 0,
2516
+ command: "lint",
2517
+ projectType: null,
2518
+ results: filteredResults2,
2519
+ summary: summary2
2520
+ };
2521
+ }
2522
+ }
2257
2523
  const projectConfig = detectProjectType(cwd);
2258
2524
  if (!projectConfig.supported) {
2259
2525
  return {
@@ -2317,8 +2583,8 @@ async function lintCommand(path = ".", options) {
2317
2583
  const startTime = performance.now();
2318
2584
  const targetPath = options.cwd || path;
2319
2585
  if (options.debug) {
2320
- console.log(`Linting: ${resolve2(targetPath)}`);
2321
- const projectConfig = detectProjectType(resolve2(targetPath));
2586
+ console.log(`Linting: ${resolve3(targetPath)}`);
2587
+ const projectConfig = detectProjectType(resolve3(targetPath));
2322
2588
  console.log(`Project type: ${projectConfig.type ?? "not configured"}`);
2323
2589
  console.log(
2324
2590
  `Checkers: ${projectConfig.checkers.map((c) => c.name).join(", ") || "none"}`
@@ -2350,7 +2616,7 @@ Completed in ${elapsed}ms`);
2350
2616
  // src/commands/analyze.ts
2351
2617
  import { basename as basename4 } from "path";
2352
2618
  import { execSync as execSync2 } from "child_process";
2353
- import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
2619
+ import { readFileSync as readFileSync4, existsSync as existsSync6 } from "fs";
2354
2620
  import fg8 from "fast-glob";
2355
2621
  import matter from "gray-matter";
2356
2622
  import { minimatch } from "minimatch";
@@ -2377,7 +2643,7 @@ function getChangedFiles2(cwd) {
2377
2643
  }
2378
2644
  function loadKnowledgeMappings(knowledgePath) {
2379
2645
  const expandedPath = expandPath(knowledgePath);
2380
- if (!existsSync5(expandedPath)) {
2646
+ if (!existsSync6(expandedPath)) {
2381
2647
  return [];
2382
2648
  }
2383
2649
  const knowledgeFiles = fg8.sync("**/*.knowledge.md", {
@@ -2466,8 +2732,8 @@ async function analyzeCommand(files = [], options) {
2466
2732
  }
2467
2733
 
2468
2734
  // src/commands/scaffold.ts
2469
- import { resolve as resolve3, dirname as dirname3, basename as basename5, join as join3 } from "path";
2470
- import { mkdirSync, writeFileSync, existsSync as existsSync6 } from "fs";
2735
+ import { resolve as resolve4, dirname as dirname4, basename as basename5, join as join4 } from "path";
2736
+ import { mkdirSync, writeFileSync, existsSync as existsSync7 } from "fs";
2471
2737
  function toPascalCase(str) {
2472
2738
  return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
2473
2739
  }
@@ -2765,7 +3031,7 @@ function generateNestJsBffFeature(outputPath, contractPath, endpointName, contra
2765
3031
  featureName: endpointName
2766
3032
  });
2767
3033
  const files = result.files.map((f) => ({
2768
- path: join3(outputPath, f.path),
3034
+ path: join4(outputPath, f.path),
2769
3035
  content: f.content,
2770
3036
  knowledge: f.isComplete ? void 0 : "nestjs-bff-skeleton"
2771
3037
  }));
@@ -2787,8 +3053,8 @@ async function scaffoldCore(options) {
2787
3053
  };
2788
3054
  }
2789
3055
  const contractsLib = options.contractsLib ?? "@adeo/ahs-operator-execution-contracts";
2790
- const absolutePath2 = resolve3(options.path);
2791
- const absoluteContractPath = resolve3(options.contractPath);
3056
+ const absolutePath2 = resolve4(options.path);
3057
+ const absoluteContractPath = resolve4(options.contractPath);
2792
3058
  const bffResult = generateNestJsBffFeature(
2793
3059
  absolutePath2,
2794
3060
  absoluteContractPath,
@@ -2819,8 +3085,8 @@ async function scaffoldCore(options) {
2819
3085
  const errors2 = [];
2820
3086
  for (const file of bffResult.files) {
2821
3087
  try {
2822
- const dir = dirname3(file.path);
2823
- if (!existsSync6(dir)) {
3088
+ const dir = dirname4(file.path);
3089
+ if (!existsSync7(dir)) {
2824
3090
  mkdirSync(dir, { recursive: true });
2825
3091
  }
2826
3092
  writeFileSync(file.path, file.content, "utf-8");
@@ -2850,7 +3116,7 @@ async function scaffoldCore(options) {
2850
3116
  availableTypes: [...Object.keys(generators), "nestjs-bff-feature"]
2851
3117
  };
2852
3118
  }
2853
- const absolutePath = resolve3(options.path);
3119
+ const absolutePath = resolve4(options.path);
2854
3120
  const files = generator(absolutePath);
2855
3121
  if (options.dryRun) {
2856
3122
  return {
@@ -2870,8 +3136,8 @@ async function scaffoldCore(options) {
2870
3136
  const errors = [];
2871
3137
  for (const file of files) {
2872
3138
  try {
2873
- const dir = dirname3(file.path);
2874
- if (!existsSync6(dir)) {
3139
+ const dir = dirname4(file.path);
3140
+ if (!existsSync7(dir)) {
2875
3141
  mkdirSync(dir, { recursive: true });
2876
3142
  }
2877
3143
  writeFileSync(file.path, file.content, "utf-8");
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  analyzeCommand,
4
4
  lintCommand,
5
5
  scaffoldCommand
6
- } from "./chunk-FV47ZLIM.js";
6
+ } from "./chunk-UNAQG5HI.js";
7
7
 
8
8
  // src/cli.ts
9
9
  import { program } from "commander";
@@ -257,322 +257,6 @@ function padEnd(str, length) {
257
257
  return str.length >= length ? str.slice(0, length - 1) + " " : str + " ".repeat(length - str.length);
258
258
  }
259
259
 
260
- // src/commands/migrate-contracts.ts
261
- import { readFileSync as readFileSync3 } from "fs";
262
- import { resolve as resolve2 } from "path";
263
-
264
- // src/lib/contracts/codemod/migrate-contracts.ts
265
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
266
- import fg2 from "fast-glob";
267
- async function migrateContracts(contractsPath, migrationMap, options = {}) {
268
- const results = [];
269
- const contractFiles = await fg2("**/*.contract.ts", {
270
- cwd: contractsPath,
271
- ignore: ["**/node_modules/**", "**/dist/**"],
272
- absolute: true
273
- });
274
- for (const filePath of contractFiles) {
275
- const fileResults = migrateContractFile(filePath, migrationMap, options);
276
- results.push(...fileResults);
277
- }
278
- return results;
279
- }
280
- function migrateContractFile(filePath, migrationMap, options) {
281
- const results = [];
282
- let content = readFileSync2(filePath, "utf-8");
283
- let modified = false;
284
- const changes = [];
285
- const hasContractMetadataImport = content.includes("contractMetadata");
286
- const hasPermissionImport = content.includes("from '../../../common/permissions'") || content.includes("from '../../common/permissions'") || content.includes('from "../../../common/permissions"') || content.includes('from "../../common/permissions"');
287
- const endpointPattern = /(\w+):\s*\{[^}]*?method:\s*['"](\w+)['"][^}]*?metadata:\s*(\{[^}]+\}|\w+\([^)]+\))/gs;
288
- const simpleEndpointPattern = /(\w+):\s*\{[^}]*?method:\s*['"](\w+)['"][^}]*?\}/gs;
289
- for (const [endpointName, mapping] of Object.entries(migrationMap)) {
290
- const endpointRegex = new RegExp(`(${endpointName}):\\s*\\{`, "g");
291
- if (!endpointRegex.test(content)) {
292
- continue;
293
- }
294
- const metadataInfo = analyzeEndpointMetadata(content, endpointName);
295
- if (!metadataInfo.found) {
296
- results.push({
297
- file: filePath,
298
- endpoint: endpointName,
299
- status: "skipped",
300
- message: "Endpoint structure not found or complex"
301
- });
302
- continue;
303
- }
304
- if (metadataInfo.hasNewFormat) {
305
- results.push({
306
- file: filePath,
307
- endpoint: endpointName,
308
- status: "skipped",
309
- message: "Already using new metadata format"
310
- });
311
- continue;
312
- }
313
- const newMetadata = buildNewMetadata(mapping, metadataInfo);
314
- if (metadataInfo.hasMetadata) {
315
- content = replaceMetadata(
316
- content,
317
- endpointName,
318
- metadataInfo,
319
- newMetadata
320
- );
321
- changes.push(`Replaced metadata for ${endpointName}`);
322
- } else {
323
- content = addMetadata(content, endpointName, newMetadata);
324
- changes.push(`Added metadata for ${endpointName}`);
325
- }
326
- modified = true;
327
- results.push({
328
- file: filePath,
329
- endpoint: endpointName,
330
- status: "migrated",
331
- message: `Migrated to new format: permission=${mapping.requiredPermission}, apiServices=[${mapping.apiServices.join(", ")}]`,
332
- changes
333
- });
334
- }
335
- if (modified) {
336
- if (!hasContractMetadataImport) {
337
- content = addContractMetadataImport(content);
338
- changes.push("Added contractMetadata import");
339
- }
340
- if (!hasPermissionImport) {
341
- content = addPermissionImport(content);
342
- changes.push("Added Permission import");
343
- }
344
- if (!options.dryRun) {
345
- writeFileSync2(filePath, content, "utf-8");
346
- }
347
- }
348
- return results;
349
- }
350
- function analyzeEndpointMetadata(content, endpointName) {
351
- const info = {
352
- found: false,
353
- hasMetadata: false,
354
- hasNewFormat: false,
355
- hasLegacyOpenapi: false,
356
- hasRequiresAuth: false,
357
- existingTags: [],
358
- startIndex: -1,
359
- endIndex: -1,
360
- fullMatch: ""
361
- };
362
- const endpointStart = content.indexOf(`${endpointName}: {`);
363
- if (endpointStart === -1) {
364
- const quotedStart = content.indexOf(`'${endpointName}': {`) !== -1 ? content.indexOf(`'${endpointName}': {`) : content.indexOf(`"${endpointName}": {`);
365
- if (quotedStart === -1) return info;
366
- }
367
- info.found = true;
368
- if (content.includes(`metadata: contractMetadata(`)) {
369
- const newFormatRegex = new RegExp(
370
- `${endpointName}:[\\s\\S]*?metadata:\\s*contractMetadata\\(`,
371
- "m"
372
- );
373
- if (newFormatRegex.test(content)) {
374
- info.hasNewFormat = true;
375
- info.hasMetadata = true;
376
- return info;
377
- }
378
- }
379
- const legacyPattern = new RegExp(
380
- `(${endpointName}:[\\s\\S]*?metadata:\\s*\\{[\\s\\S]*?openapi:\\s*\\{[\\s\\S]*?tags:\\s*\\[([^\\]]+)\\])`,
381
- "m"
382
- );
383
- const legacyMatch = content.match(legacyPattern);
384
- if (legacyMatch?.[2]) {
385
- info.hasMetadata = true;
386
- info.hasLegacyOpenapi = true;
387
- const tagsStr = legacyMatch[2];
388
- info.existingTags = tagsStr.split(",").map((t) => t.trim().replace(/['"]/g, "")).filter(Boolean);
389
- }
390
- const requiresAuthPattern = new RegExp(
391
- `${endpointName}:[\\s\\S]*?requiresAuth:\\s*(true|false)`,
392
- "m"
393
- );
394
- if (requiresAuthPattern.test(content)) {
395
- info.hasRequiresAuth = true;
396
- info.hasMetadata = true;
397
- }
398
- const simpleMetadataPattern = new RegExp(
399
- `${endpointName}:[\\s\\S]*?metadata:\\s*\\{`,
400
- "m"
401
- );
402
- if (simpleMetadataPattern.test(content)) {
403
- info.hasMetadata = true;
404
- }
405
- return info;
406
- }
407
- function buildNewMetadata(mapping, metadataInfo) {
408
- const tags = metadataInfo.existingTags.length > 0 ? metadataInfo.existingTags : ["TODO: Add tags"];
409
- const tagsArray = tags.map((t) => `'${t}'`).join(", ");
410
- let metadata = `metadata: contractMetadata({
411
- openApiTags: [${tagsArray}],
412
- requiredPermission: '${mapping.requiredPermission}',`;
413
- if (mapping.apiServices.length > 0) {
414
- const servicesArray = mapping.apiServices.map((s) => `'${s}'`).join(", ");
415
- metadata += `
416
- bff: {
417
- apiServices: [${servicesArray}],
418
- },`;
419
- }
420
- metadata += `
421
- }),`;
422
- return metadata;
423
- }
424
- function replaceMetadata(content, endpointName, metadataInfo, newMetadata) {
425
- const metadataPattern = new RegExp(
426
- `(${endpointName}:[\\s\\S]*?)(metadata:\\s*\\{[\\s\\S]*?\\}(?:\\s*as\\s*const)?,?)`,
427
- "m"
428
- );
429
- return content.replace(metadataPattern, `$1${newMetadata}`);
430
- }
431
- function addMetadata(content, endpointName, newMetadata) {
432
- const strictPattern = new RegExp(
433
- `(${endpointName}:[\\s\\S]*?)(strictStatusCodes:\\s*true,?)`,
434
- "m"
435
- );
436
- if (strictPattern.test(content)) {
437
- return content.replace(strictPattern, `$1${newMetadata}
438
- $2`);
439
- }
440
- const endpointStart = content.indexOf(`${endpointName}: {`);
441
- if (endpointStart === -1) {
442
- return content;
443
- }
444
- const blockStart = content.indexOf("{", endpointStart);
445
- if (blockStart === -1) return content;
446
- let braceCount = 1;
447
- let pos = blockStart + 1;
448
- while (pos < content.length && braceCount > 0) {
449
- const char = content[pos];
450
- if (char === "{") braceCount++;
451
- else if (char === "}") braceCount--;
452
- pos++;
453
- }
454
- if (braceCount !== 0) return content;
455
- const closingBracePos = pos - 1;
456
- const responsesMatch = content.slice(endpointStart, closingBracePos).match(/\n(\s+)responses:/);
457
- const indent = responsesMatch?.[1] ?? " ";
458
- const before = content.slice(0, closingBracePos);
459
- const after = content.slice(closingBracePos);
460
- const lastNonWhitespace = before.trimEnd().slice(-1);
461
- const needsComma = lastNonWhitespace !== ",";
462
- return before.trimEnd() + (needsComma ? "," : "") + `
463
-
464
- ${indent}${newMetadata}
465
- ${indent.slice(2)}` + after;
466
- }
467
- function addContractMetadataImport(content) {
468
- const metadataImportPattern = /import\s*\{[^}]*\}\s*from\s*['"]\.\.\/.*common\/metadata\/contract-metadata['"]/;
469
- if (metadataImportPattern.test(content)) {
470
- return content.replace(
471
- /(import\s*\{)([^}]*)(}\s*from\s*['"]\.\.\/.*common\/metadata\/contract-metadata['"])/,
472
- "$1$2, contractMetadata$3"
473
- );
474
- }
475
- const lastImportMatch = content.match(/import[^;]+;/g);
476
- if (lastImportMatch && lastImportMatch.length > 0) {
477
- const lastImport = lastImportMatch[lastImportMatch.length - 1];
478
- const insertPos = content.indexOf(lastImport) + lastImport.length;
479
- const depth = (content.match(/from ['"](\.\.\/)*/)?.[0]?.match(/\.\.\//g) ?? []).length;
480
- const relativePath = "../".repeat(Math.max(3, depth)) + "common/metadata/contract-metadata";
481
- return content.slice(0, insertPos) + `
482
- import { contractMetadata } from '${relativePath}';` + content.slice(insertPos);
483
- }
484
- return content;
485
- }
486
- function addPermissionImport(content) {
487
- const permissionImportPattern = /import\s*\{[^}]*Permission[^}]*\}\s*from\s*['"].*permissions['"]/;
488
- if (permissionImportPattern.test(content)) {
489
- return content;
490
- }
491
- const lastImportMatch = content.match(/import[^;]+;/g);
492
- if (lastImportMatch && lastImportMatch.length > 0) {
493
- const lastImport = lastImportMatch[lastImportMatch.length - 1];
494
- const insertPos = content.indexOf(lastImport) + lastImport.length;
495
- const depth = (content.match(/from ['"](\.\.\/)*/)?.[0]?.match(/\.\.\//g) ?? []).length;
496
- const relativePath = "../".repeat(Math.max(3, depth)) + "common/permissions";
497
- return content.slice(0, insertPos) + `
498
- import { Permission } from '${relativePath}';` + content.slice(insertPos);
499
- }
500
- return content;
501
- }
502
-
503
- // src/commands/migrate-contracts.ts
504
- async function migrateContractsCommand(contractsPath, options) {
505
- const absoluteContractsPath = resolve2(contractsPath);
506
- const absoluteMapPath = resolve2(options.migrationMap);
507
- console.log(`Migrating contracts at: ${absoluteContractsPath}`);
508
- console.log(`Using migration map: ${absoluteMapPath}`);
509
- if (options.dryRun) {
510
- console.log("DRY RUN - no files will be modified\n");
511
- } else {
512
- console.log("");
513
- }
514
- let mapData;
515
- try {
516
- const mapContent = readFileSync3(absoluteMapPath, "utf-8");
517
- mapData = JSON.parse(mapContent);
518
- } catch (err) {
519
- console.error(
520
- `Failed to load migration map: ${err instanceof Error ? err.message : String(err)}`
521
- );
522
- process.exit(1);
523
- }
524
- if (!mapData.migrationMap) {
525
- console.error("Migration map file must contain a 'migrationMap' property");
526
- process.exit(1);
527
- }
528
- const results = await migrateContracts(
529
- absoluteContractsPath,
530
- mapData.migrationMap,
531
- { dryRun: options.dryRun }
532
- );
533
- const migrated = results.filter((r) => r.status === "migrated");
534
- const skipped = results.filter((r) => r.status === "skipped");
535
- const errors = results.filter((r) => r.status === "error");
536
- if (migrated.length > 0) {
537
- console.log(`\u2705 Migrated (${migrated.length}):`);
538
- for (const r of migrated) {
539
- const shortPath = r.file.split("/").slice(-3).join("/");
540
- console.log(` ${r.endpoint} in ${shortPath}`);
541
- console.log(` \u2192 ${r.message}`);
542
- }
543
- console.log("");
544
- }
545
- if (skipped.length > 0) {
546
- console.log(`\u23ED\uFE0F Skipped (${skipped.length}):`);
547
- const byReason = /* @__PURE__ */ new Map();
548
- for (const r of skipped) {
549
- const existing = byReason.get(r.message) ?? [];
550
- existing.push(r.endpoint);
551
- byReason.set(r.message, existing);
552
- }
553
- for (const [reason, endpoints] of byReason) {
554
- console.log(
555
- ` ${reason}: ${endpoints.slice(0, 5).join(", ")}${endpoints.length > 5 ? ` (+${endpoints.length - 5} more)` : ""}`
556
- );
557
- }
558
- console.log("");
559
- }
560
- if (errors.length > 0) {
561
- console.log(`\u274C Errors (${errors.length}):`);
562
- for (const r of errors) {
563
- console.log(` ${r.endpoint}: ${r.message}`);
564
- }
565
- console.log("");
566
- }
567
- console.log("\u2500".repeat(50));
568
- console.log(
569
- `Total: ${results.length} | Migrated: ${migrated.length} | Skipped: ${skipped.length} | Errors: ${errors.length}`
570
- );
571
- if (options.dryRun && migrated.length > 0) {
572
- console.log("\nRun without --dry-run to apply changes");
573
- }
574
- }
575
-
576
260
  // src/cli.ts
577
261
  var require2 = createRequire(import.meta.url);
578
262
  var { version: VERSION } = require2("../package.json");
@@ -602,8 +286,4 @@ program.command("scaffold <type> <path>").description("Generate a colocated feat
602
286
  program.command("analyze-bff <bff-path>").description(
603
287
  "Analyze BFF to extract endpoint \u2192 permission \u2192 apiService mappings"
604
288
  ).option("-o, --output <file>", "Output JSON file path").option("--format <type>", "Output format: json, table", "json").action(analyzeBffCommand);
605
- program.command("migrate-contracts <contracts-path>").description("Migrate contracts to new metadata format using migration map").requiredOption(
606
- "-m, --migration-map <file>",
607
- "Path to migration map JSON file (from analyze-bff)"
608
- ).option("--dry-run", "Show what would be changed without writing files").action(migrateContractsCommand);
609
289
  program.parse();
@@ -3,7 +3,7 @@ import {
3
3
  analyzeCore,
4
4
  lintCore,
5
5
  scaffoldCore
6
- } from "./chunk-FV47ZLIM.js";
6
+ } from "./chunk-UNAQG5HI.js";
7
7
 
8
8
  // src/mcp-server.ts
9
9
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyphaene/hexa-ts-kit",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "description": "TypeScript dev kit for Claude Code agents: architecture linting, scaffolding, knowledge analysis",
5
5
  "type": "module",
6
6
  "bin": {
@@ -53,6 +53,7 @@
53
53
  "gray-matter": "^4.0.3",
54
54
  "minimatch": "^10.0.1",
55
55
  "picocolors": "^1.1.1",
56
+ "ts-morph": "^27.0.2",
56
57
  "vue-eslint-parser": "^10.2.0",
57
58
  "yaml": "^2.8.2",
58
59
  "zod": "^4.3.4"