@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.
- package/dist/{chunk-FV47ZLIM.js → chunk-UNAQG5HI.js} +282 -16
- package/dist/cli.js +1 -321
- package/dist/mcp-server.js +1 -1
- package/package.json +2 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/commands/lint.ts
|
|
2
|
-
import { resolve as
|
|
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 =
|
|
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: ${
|
|
2321
|
-
const projectConfig = detectProjectType(
|
|
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
|
|
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 (!
|
|
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
|
|
2470
|
-
import { mkdirSync, writeFileSync, existsSync as
|
|
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:
|
|
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 =
|
|
2791
|
-
const absoluteContractPath =
|
|
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 =
|
|
2823
|
-
if (!
|
|
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 =
|
|
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 =
|
|
2874
|
-
if (!
|
|
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-
|
|
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();
|
package/dist/mcp-server.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyphaene/hexa-ts-kit",
|
|
3
|
-
"version": "1.
|
|
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"
|