@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/bin/hexa-ts-mcp.js +0 -0
- package/dist/chunk-FV47ZLIM.js +2922 -0
- package/dist/cli.js +586 -3
- package/dist/mcp-server.js +5 -4
- package/package.json +3 -2
- package/dist/chunk-56VIQG3N.js +0 -1434
package/dist/cli.js
CHANGED
|
@@ -3,17 +3,583 @@ import {
|
|
|
3
3
|
analyzeCommand,
|
|
4
4
|
lintCommand,
|
|
5
5
|
scaffoldCommand
|
|
6
|
-
} from "./chunk-
|
|
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").
|
|
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();
|
package/dist/mcp-server.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
analyzeCore,
|
|
4
4
|
lintCore,
|
|
5
5
|
scaffoldCore
|
|
6
|
-
} from "./chunk-
|
|
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
|
-
|
|
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.
|
|
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": "^
|
|
65
|
+
"semantic-release": "^25.0.2",
|
|
65
66
|
"tsup": "^8.3.5",
|
|
66
67
|
"typescript": "^5.7.2",
|
|
67
68
|
"vitest": "^3.1.4"
|