@hyphaene/hexa-ts-kit 1.2.4 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -3,17 +3,583 @@ import {
3
3
  analyzeCommand,
4
4
  lintCommand,
5
5
  scaffoldCommand
6
- } from "./chunk-56VIQG3N.js";
6
+ } from "./chunk-FV47ZLIM.js";
7
7
 
8
8
  // src/cli.ts
9
9
  import { program } from "commander";
10
10
  import { createRequire } from "module";
11
+
12
+ // src/commands/analyze-bff.ts
13
+ import { writeFileSync } from "fs";
14
+ import { resolve } from "path";
15
+
16
+ // src/lib/contracts/codemod/analyze-bff.ts
17
+ import { readFileSync } from "fs";
18
+ import fg from "fast-glob";
19
+ import { parse } from "@typescript-eslint/typescript-estree";
20
+ var MODULE_TO_SERVICE = {
21
+ SeeCoreModule: "seeCore",
22
+ LocusModule: "locus",
23
+ SeeBudgetModule: "seeBudget",
24
+ SmdModule: "smd",
25
+ SeeProviderModule: "seeProvider"
26
+ };
27
+ var PERMISSION_CONST_TO_VALUE = {
28
+ CAN_MANAGE_CONTRACT: "canManageContract",
29
+ CAN_MANAGE_BUDGET_FLOW: "canManageBudgetFlow",
30
+ CAN_UPLOAD_BUDGET: "canUploadBudget",
31
+ CAN_DELETE_BUDGET: "canDeleteBudget",
32
+ CAN_MANAGE_NOTES: "canManageNotes",
33
+ CAN_MANAGE_EXTRA_COSTS: "canManageExtraCosts",
34
+ CAN_MANAGE_CUSTOMER_INFORMATION: "canManageCustomerInformation",
35
+ CAN_ASSIGN_PROVIDER: "canAssignProvider",
36
+ CAN_ASSIGN_WORKER: "canAssignWorker",
37
+ CAN_UPLOAD_DOCUMENTS: "canUploadDocuments",
38
+ CAN_DOWNLOAD_DOCUMENTS: "canDownloadDocuments",
39
+ CAN_DELETE_DOCUMENTS: "canDeleteDocuments",
40
+ CAN_SELECT_UNAVAILABLE_PROVIDER_WORKER: "canSelectUnavailableProviderAndWorker",
41
+ CAN_RESCHEDULE_SERVICE_EXECUTION: "canRescheduleServiceExecution",
42
+ CAN_MANAGE_REWORK: "canManageRework",
43
+ CAN_CANCEL_SERVICE_EXECUTION: "canCancelServiceExecution",
44
+ CAN_MANAGE_WORKING_CLOSED_FORM: "canManageWorkingClosedForm",
45
+ CAN_SKIP_CONTRACT: "canSkipContract"
46
+ };
47
+ async function analyzeBff(bffPath) {
48
+ const mappings = [];
49
+ const errors = [];
50
+ const controllerFiles = await fg("**/src/domains/**/*.controller.ts", {
51
+ cwd: bffPath,
52
+ ignore: [
53
+ "**/node_modules/**",
54
+ "**/dist/**",
55
+ "**/*.spec.ts",
56
+ "**/*.test.ts"
57
+ ],
58
+ absolute: true
59
+ });
60
+ const moduleFiles = await fg("**/src/domains/**/*.module.ts", {
61
+ cwd: bffPath,
62
+ ignore: ["**/node_modules/**", "**/dist/**"],
63
+ absolute: true
64
+ });
65
+ const featureApiServices = /* @__PURE__ */ new Map();
66
+ for (const modulePath of moduleFiles) {
67
+ const featureName = extractFeatureName(modulePath);
68
+ const apiServices = extractApiServicesFromModule(modulePath);
69
+ if (featureName && apiServices.length > 0) {
70
+ featureApiServices.set(featureName, apiServices);
71
+ }
72
+ }
73
+ for (const controllerPath of controllerFiles) {
74
+ try {
75
+ const featureName = extractFeatureName(controllerPath);
76
+ const apiServices = featureName ? featureApiServices.get(featureName) ?? [] : [];
77
+ const controllerMappings = analyzeController(controllerPath, apiServices);
78
+ mappings.push(...controllerMappings);
79
+ } catch (err) {
80
+ errors.push(
81
+ `Error analyzing ${controllerPath}: ${err instanceof Error ? err.message : String(err)}`
82
+ );
83
+ }
84
+ }
85
+ return { mappings, errors };
86
+ }
87
+ function extractFeatureName(filePath) {
88
+ const match = filePath.match(/domains\/([^/]+)\//);
89
+ return match?.[1] ?? null;
90
+ }
91
+ function extractApiServicesFromModule(modulePath) {
92
+ const content = readFileSync(modulePath, "utf-8");
93
+ const services = [];
94
+ for (const [moduleName, serviceName] of Object.entries(MODULE_TO_SERVICE)) {
95
+ if (content.includes(moduleName)) {
96
+ services.push(serviceName);
97
+ }
98
+ }
99
+ return services;
100
+ }
101
+ function analyzeController(controllerPath, apiServices) {
102
+ const content = readFileSync(controllerPath, "utf-8");
103
+ const mappings = [];
104
+ let ast;
105
+ try {
106
+ ast = parse(content, { loc: true, range: true, comment: true });
107
+ } catch {
108
+ return mappings;
109
+ }
110
+ for (const node of ast.body) {
111
+ if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "ClassDeclaration") {
112
+ const classNode = node.declaration;
113
+ analyzeClassMethods(
114
+ classNode,
115
+ controllerPath,
116
+ apiServices,
117
+ content,
118
+ mappings
119
+ );
120
+ }
121
+ if (node.type === "ClassDeclaration") {
122
+ analyzeClassMethods(node, controllerPath, apiServices, content, mappings);
123
+ }
124
+ }
125
+ return mappings;
126
+ }
127
+ function analyzeClassMethods(classNode, controllerPath, apiServices, content, mappings) {
128
+ for (const member of classNode.body.body) {
129
+ if (member.type !== "MethodDefinition") continue;
130
+ if (member.key.type !== "Identifier") continue;
131
+ const methodName = member.key.name;
132
+ if (methodName === "constructor" || methodName.startsWith("_")) continue;
133
+ const decorators = member.decorators ?? [];
134
+ let permission = null;
135
+ let method = null;
136
+ let path = null;
137
+ let contractEndpointName = null;
138
+ for (const decorator of decorators) {
139
+ if (decorator.expression.type === "CallExpression") {
140
+ const callee = decorator.expression.callee;
141
+ if (callee.type === "Identifier" && callee.name === "TsRestHandler") {
142
+ const arg = decorator.expression.arguments[0];
143
+ if (arg?.type === "MemberExpression" && arg.property.type === "Identifier") {
144
+ contractEndpointName = arg.property.name;
145
+ }
146
+ }
147
+ if (callee.type === "Identifier" && callee.name === "Authorize") {
148
+ const arg = decorator.expression.arguments[0];
149
+ if (arg?.type === "MemberExpression" && arg.property.type === "Identifier") {
150
+ permission = arg.property.name;
151
+ }
152
+ }
153
+ if (callee.type === "Identifier" && ["Get", "Post", "Put", "Patch", "Delete"].includes(callee.name)) {
154
+ method = callee.name.toUpperCase();
155
+ const arg = decorator.expression.arguments[0];
156
+ if (arg?.type === "Literal" && typeof arg.value === "string") {
157
+ path = arg.value;
158
+ }
159
+ }
160
+ }
161
+ }
162
+ const endpointName = contractEndpointName ?? methodName;
163
+ const permissionValue = permission ? PERMISSION_CONST_TO_VALUE[permission] ?? null : null;
164
+ const apiModules = Object.entries(MODULE_TO_SERVICE).filter(([_, service]) => apiServices.includes(service)).map(([module]) => module);
165
+ mappings.push({
166
+ endpointName,
167
+ controllerPath,
168
+ permission,
169
+ permissionValue,
170
+ apiModules,
171
+ apiServices,
172
+ method,
173
+ path
174
+ });
175
+ }
176
+ }
177
+ function generateMigrationMap(result) {
178
+ const map = {};
179
+ for (const mapping of result.mappings) {
180
+ const existing = map[mapping.endpointName];
181
+ const hasMoreInfo = !existing || mapping.permissionValue && !existing.requiredPermission || mapping.apiServices.length > 0 && existing.apiServices.length === 0 || mapping.permissionValue && mapping.apiServices.length > 0;
182
+ if (hasMoreInfo) {
183
+ map[mapping.endpointName] = {
184
+ requiredPermission: mapping.permissionValue ?? "NO_PERMISSION",
185
+ apiServices: mapping.apiServices
186
+ };
187
+ }
188
+ }
189
+ return map;
190
+ }
191
+
192
+ // src/commands/analyze-bff.ts
193
+ async function analyzeBffCommand(bffPath, options) {
194
+ const absolutePath = resolve(bffPath);
195
+ console.log(`Analyzing BFF at: ${absolutePath}
196
+ `);
197
+ const result = await analyzeBff(absolutePath);
198
+ if (result.errors.length > 0) {
199
+ console.error("Errors encountered:");
200
+ for (const error of result.errors) {
201
+ console.error(` - ${error}`);
202
+ }
203
+ console.log();
204
+ }
205
+ if (options.format === "table") {
206
+ printTable(result.mappings);
207
+ } else {
208
+ const migrationMap = generateMigrationMap(result);
209
+ const output = JSON.stringify(
210
+ {
211
+ mappings: result.mappings,
212
+ migrationMap,
213
+ summary: {
214
+ total: result.mappings.length,
215
+ withPermission: result.mappings.filter((m) => m.permission).length,
216
+ withApiServices: result.mappings.filter(
217
+ (m) => m.apiServices.length > 0
218
+ ).length,
219
+ errors: result.errors.length
220
+ }
221
+ },
222
+ null,
223
+ 2
224
+ );
225
+ if (options.output) {
226
+ writeFileSync(options.output, output, "utf-8");
227
+ console.log(`Output written to: ${options.output}`);
228
+ } else {
229
+ console.log(output);
230
+ }
231
+ }
232
+ }
233
+ function printTable(mappings) {
234
+ console.log("Endpoint Mappings:");
235
+ console.log("\u2500".repeat(100));
236
+ console.log(
237
+ padEnd("Endpoint", 30) + padEnd("Permission", 25) + padEnd("API Services", 20) + "Controller"
238
+ );
239
+ console.log("\u2500".repeat(100));
240
+ for (const m of mappings) {
241
+ const endpoint = padEnd(m.endpointName, 30);
242
+ const permission = padEnd(m.permissionValue ?? "(none)", 25);
243
+ const services = padEnd(m.apiServices.join(", ") || "(none)", 20);
244
+ const controller = m.controllerPath.split("/").slice(-3).join("/");
245
+ console.log(`${endpoint}${permission}${services}${controller}`);
246
+ }
247
+ console.log("\u2500".repeat(100));
248
+ console.log(`Total: ${mappings.length} endpoints`);
249
+ console.log(
250
+ `With permission: ${mappings.filter((m) => m.permission).length}`
251
+ );
252
+ console.log(
253
+ `With API services: ${mappings.filter((m) => m.apiServices.length > 0).length}`
254
+ );
255
+ }
256
+ function padEnd(str, length) {
257
+ return str.length >= length ? str.slice(0, length - 1) + " " : str + " ".repeat(length - str.length);
258
+ }
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
+ // src/cli.ts
11
577
  var require2 = createRequire(import.meta.url);
12
578
  var { version: VERSION } = require2("../package.json");
13
579
  program.name("hexa-ts").description(
14
580
  "TypeScript dev kit: architecture linting, scaffolding, knowledge analysis"
15
581
  ).version(VERSION);
16
- program.command("lint [path]").description("Lint files for colocation architecture rules").option("-f, --format <type>", "Output format: console, json", "console").option("--changed", "Only lint files changed in git").option(
582
+ program.command("lint [path]").description("Lint files for colocation architecture rules").option("-f, --format <type>", "Output format: console, json", "console").option("--cwd <path>", "Working directory to lint (alias for path argument)").option("--changed", "Only lint files changed in git").option(
17
583
  "--rules <rules>",
18
584
  "Comma-separated list of rule prefixes to run (COL,NAM,DOM,VUE,etc)"
19
585
  ).option("--quiet", "Only show errors, hide warnings and info").option("--debug", "Show debug information").action(lintCommand);
@@ -22,5 +588,22 @@ program.command("analyze [files...]").description("Map files to their associated
22
588
  "Path to knowledge files",
23
589
  "~/.claude/marketplace/shared/knowledge"
24
590
  ).action(analyzeCommand);
25
- program.command("scaffold <type> <path>").description("Generate a colocated feature structure").option("--dry-run", "Show what would be created without writing files").action(scaffoldCommand);
591
+ program.command("scaffold <type> <path>").description("Generate a colocated feature structure").option("--dry-run", "Show what would be created without writing files").option(
592
+ "--contract-path <path>",
593
+ "Path to contract file (for nestjs-bff-feature)"
594
+ ).option(
595
+ "--endpoint-name <name>",
596
+ "Endpoint name from contract (for nestjs-bff-feature)"
597
+ ).option(
598
+ "--contracts-lib <import>",
599
+ "Import path for contracts lib",
600
+ "@adeo/ahs-operator-execution-contracts"
601
+ ).action(scaffoldCommand);
602
+ program.command("analyze-bff <bff-path>").description(
603
+ "Analyze BFF to extract endpoint \u2192 permission \u2192 apiService mappings"
604
+ ).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);
26
609
  program.parse();
@@ -3,7 +3,7 @@ import {
3
3
  analyzeCore,
4
4
  lintCore,
5
5
  scaffoldCore
6
- } from "./chunk-56VIQG3N.js";
6
+ } from "./chunk-FV47ZLIM.js";
7
7
 
8
8
  // src/mcp-server.ts
9
9
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -20,16 +20,17 @@ server.tool(
20
20
  "lint",
21
21
  "Lint files for colocation architecture rules. Checks 71 rules across structure (COL, NAM, DOM) and AST (VUE, RUL, TSP) categories. Use --changed for incremental mode on git-modified files only.",
22
22
  {
23
- path: z.string().optional().describe("Path to lint (default: current working directory)"),
23
+ cwd: z.string().optional().describe("Working directory to lint (default: current directory)"),
24
+ path: z.string().optional().describe("Alias for cwd - path to lint"),
24
25
  changed: z.boolean().optional().describe("Only lint git-changed files (staged and unstaged)"),
25
26
  rules: z.string().optional().describe(
26
- "Filter by rule prefix, comma-separated (COL, NAM, DOM, VUE, RUL, TSP)"
27
+ "Filter by rule prefix, comma-separated (COL, NAM, DOM, VUE, RUL, TSP, CTR)"
27
28
  ),
28
29
  quiet: z.boolean().optional().describe("Only show errors, hide warnings and info")
29
30
  },
30
31
  async (params) => {
31
32
  const result = await lintCore({
32
- path: params.path,
33
+ path: params.cwd || params.path,
33
34
  changed: params.changed,
34
35
  rules: params.rules,
35
36
  quiet: params.quiet
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyphaene/hexa-ts-kit",
3
- "version": "1.2.4",
3
+ "version": "1.4.0",
4
4
  "description": "TypeScript dev kit for Claude Code agents: architecture linting, scaffolding, knowledge analysis",
5
5
  "type": "module",
6
6
  "bin": {
@@ -54,6 +54,7 @@
54
54
  "minimatch": "^10.0.1",
55
55
  "picocolors": "^1.1.1",
56
56
  "vue-eslint-parser": "^10.2.0",
57
+ "yaml": "^2.8.2",
57
58
  "zod": "^4.3.4"
58
59
  },
59
60
  "devDependencies": {
@@ -61,7 +62,7 @@
61
62
  "@semantic-release/git": "^10.0.1",
62
63
  "@types/node": "^22.10.2",
63
64
  "@vitest/coverage-v8": "^3.1.4",
64
- "semantic-release": "^24.2.5",
65
+ "semantic-release": "^25.0.2",
65
66
  "tsup": "^8.3.5",
66
67
  "typescript": "^5.7.2",
67
68
  "vitest": "^3.1.4"