@nxuss/lemma 0.9.2 → 0.9.3
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/README.md +62 -10
- package/dist/cjs/mcp/tools.d.ts.map +1 -1
- package/dist/cjs/mcp/tools.js +619 -0
- package/dist/cjs/mcp/tools.js.map +1 -1
- package/dist/esm/mcp/tools.d.ts.map +1 -1
- package/dist/esm/mcp/tools.js +619 -0
- package/dist/esm/mcp/tools.js.map +1 -1
- package/package.json +1 -1
package/dist/cjs/mcp/tools.js
CHANGED
|
@@ -530,6 +530,39 @@ const toolDefinitions = [
|
|
|
530
530
|
required: ["platform"],
|
|
531
531
|
},
|
|
532
532
|
},
|
|
533
|
+
{
|
|
534
|
+
name: "depgraph",
|
|
535
|
+
description: "Builds a real-time dependency graph for any file in the workspace using the TypeScript Compiler API. Shows what a file imports, what imports it (reverse deps), and what it exports. Zero LLM calls. Essential before any refactor to understand blast radius.",
|
|
536
|
+
inputSchema: {
|
|
537
|
+
type: "object",
|
|
538
|
+
properties: {
|
|
539
|
+
filePath: { type: "string", description: "Target file path relative to workspace root" },
|
|
540
|
+
reverse: { type: "boolean", description: "Include reverse dependencies (who imports this file)", default: true },
|
|
541
|
+
depth: { type: "number", description: "Graph traversal depth 1-5 (default: 1)", default: 1 },
|
|
542
|
+
format: { type: "string", enum: ["text", "mermaid", "json"], description: "Output format: text tree, Mermaid flowchart, or JSON", default: "text" },
|
|
543
|
+
external: { type: "boolean", description: "Include node_modules dependencies (default: false)", default: false },
|
|
544
|
+
circular: { type: "boolean", description: "Detect and highlight circular dependencies", default: false },
|
|
545
|
+
},
|
|
546
|
+
required: ["filePath"],
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
name: "refactor",
|
|
551
|
+
description: "Declarative multi-file codemod engine. Rename symbols across the entire workspace or move files updating all imports. Uses TypeScript Compiler API — zero LLM calls. Supports dry-run diff preview and post-refactor tsc verification.",
|
|
552
|
+
inputSchema: {
|
|
553
|
+
type: "object",
|
|
554
|
+
properties: {
|
|
555
|
+
operation: { type: "string", enum: ["rename", "move"], description: "Refactor operation: 'rename' a symbol everywhere, or 'move' a file updating all importers" },
|
|
556
|
+
from: { type: "string", description: "Symbol name to rename (rename op) OR source file path relative to workspace root (move op)" },
|
|
557
|
+
to: { type: "string", description: "New symbol name (rename op) OR destination file path relative to workspace root (move op)" },
|
|
558
|
+
file: { type: "string", description: "File where the symbol is declared — required for 'rename' operation" },
|
|
559
|
+
dryRun: { type: "boolean", description: "Preview changes as a unified diff without applying them", default: false },
|
|
560
|
+
tscCheck: { type: "boolean", description: "Run tsc --noEmit after applying to guarantee zero TypeScript errors", default: false },
|
|
561
|
+
backup: { type: "boolean", description: "Create .bak backup of each modified file before changing", default: false },
|
|
562
|
+
},
|
|
563
|
+
required: ["operation", "from", "to"],
|
|
564
|
+
},
|
|
565
|
+
},
|
|
533
566
|
];
|
|
534
567
|
const toolHandlers = {
|
|
535
568
|
scrub_privacy: handleScrubPrivacy,
|
|
@@ -568,6 +601,8 @@ const toolHandlers = {
|
|
|
568
601
|
review_pr: handleReviewPR,
|
|
569
602
|
pr_status: handlePRStatus,
|
|
570
603
|
generate_pr_workflow: handleGeneratePRWorkflow,
|
|
604
|
+
depgraph: handleDepgraph,
|
|
605
|
+
refactor: handleRefactor,
|
|
571
606
|
};
|
|
572
607
|
function setupToolsHandlers(server, onToolCall) {
|
|
573
608
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
@@ -2660,4 +2695,588 @@ function generateAzurePipelineYaml(threshold) {
|
|
|
2660
2695
|
` AZURE_DEVOPS_TOKEN: \$(AZURE_DEVOPS_TOKEN)`,
|
|
2661
2696
|
].join("\n");
|
|
2662
2697
|
}
|
|
2698
|
+
function buildDepGraphMap(workspaceRoot) {
|
|
2699
|
+
const map = new Map();
|
|
2700
|
+
const IGNORE_DG = new Set(["node_modules", ".git", "dist", "chroma_data", ".lemma", "build", "coverage", ".next"]);
|
|
2701
|
+
function walkDG(dir) {
|
|
2702
|
+
let entries = [];
|
|
2703
|
+
try {
|
|
2704
|
+
entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
2705
|
+
}
|
|
2706
|
+
catch {
|
|
2707
|
+
return;
|
|
2708
|
+
}
|
|
2709
|
+
for (const e of entries) {
|
|
2710
|
+
if (IGNORE_DG.has(e.name))
|
|
2711
|
+
continue;
|
|
2712
|
+
const abs = path_1.default.join(dir, e.name);
|
|
2713
|
+
if (e.isDirectory()) {
|
|
2714
|
+
walkDG(abs);
|
|
2715
|
+
}
|
|
2716
|
+
else if (/\.(ts|tsx|js|jsx)$/.test(e.name) && !e.name.endsWith(".d.ts")) {
|
|
2717
|
+
const rel = path_1.default.relative(workspaceRoot, abs).replace(/\\/g, "/");
|
|
2718
|
+
map.set(rel, { rel, abs, imports: [], importedBy: [], externalImports: [], exports: [] });
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
walkDG(workspaceRoot);
|
|
2723
|
+
for (const [, node] of map) {
|
|
2724
|
+
try {
|
|
2725
|
+
const src = fs_1.default.readFileSync(node.abs, "utf8");
|
|
2726
|
+
const sf = ts.createSourceFile(node.abs, src, ts.ScriptTarget.Latest, true);
|
|
2727
|
+
ts.forEachChild(sf, (n) => {
|
|
2728
|
+
if (ts.isImportDeclaration(n) && ts.isStringLiteral(n.moduleSpecifier)) {
|
|
2729
|
+
const spec = n.moduleSpecifier.text;
|
|
2730
|
+
if (spec.startsWith(".")) {
|
|
2731
|
+
const dir = path_1.default.dirname(node.abs);
|
|
2732
|
+
let resolved = path_1.default.resolve(dir, spec);
|
|
2733
|
+
for (const ext of [".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"]) {
|
|
2734
|
+
if (fs_1.default.existsSync(resolved + ext)) {
|
|
2735
|
+
resolved += ext;
|
|
2736
|
+
break;
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
const rel = path_1.default.relative(workspaceRoot, resolved).replace(/\\/g, "/");
|
|
2740
|
+
if (!node.imports.includes(rel))
|
|
2741
|
+
node.imports.push(rel);
|
|
2742
|
+
}
|
|
2743
|
+
else {
|
|
2744
|
+
const pkg = spec.split("/")[0];
|
|
2745
|
+
if (!node.externalImports.includes(pkg))
|
|
2746
|
+
node.externalImports.push(pkg);
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
const hasExportMod = (nd) => ts.canHaveModifiers(nd) &&
|
|
2750
|
+
(ts.getModifiers(nd) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
2751
|
+
if (hasExportMod(n)) {
|
|
2752
|
+
if (ts.isFunctionDeclaration(n) && n.name)
|
|
2753
|
+
node.exports.push(n.name.text);
|
|
2754
|
+
else if (ts.isClassDeclaration(n) && n.name)
|
|
2755
|
+
node.exports.push(n.name.text);
|
|
2756
|
+
else if (ts.isVariableStatement(n)) {
|
|
2757
|
+
for (const decl of n.declarationList.declarations) {
|
|
2758
|
+
if (ts.isIdentifier(decl.name))
|
|
2759
|
+
node.exports.push(decl.name.text);
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
else if (ts.isInterfaceDeclaration(n))
|
|
2763
|
+
node.exports.push(n.name.text);
|
|
2764
|
+
else if (ts.isTypeAliasDeclaration(n))
|
|
2765
|
+
node.exports.push(n.name.text);
|
|
2766
|
+
else if (ts.isEnumDeclaration(n))
|
|
2767
|
+
node.exports.push(n.name.text);
|
|
2768
|
+
}
|
|
2769
|
+
if (ts.isExportDeclaration(n) && n.exportClause && ts.isNamedExports(n.exportClause)) {
|
|
2770
|
+
for (const el of n.exportClause.elements)
|
|
2771
|
+
node.exports.push(el.name.text);
|
|
2772
|
+
}
|
|
2773
|
+
});
|
|
2774
|
+
}
|
|
2775
|
+
catch { /* skip unparseable */ }
|
|
2776
|
+
}
|
|
2777
|
+
for (const [, node] of map) {
|
|
2778
|
+
for (const imp of node.imports) {
|
|
2779
|
+
const target = map.get(imp);
|
|
2780
|
+
if (target && !target.importedBy.includes(node.rel))
|
|
2781
|
+
target.importedBy.push(node.rel);
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
return map;
|
|
2785
|
+
}
|
|
2786
|
+
function bfsDepGraph(map, startRel, direction, maxDepth) {
|
|
2787
|
+
const visited = new Map();
|
|
2788
|
+
const queue = [{ rel: startRel, depth: 0 }];
|
|
2789
|
+
while (queue.length > 0) {
|
|
2790
|
+
const item = queue.shift();
|
|
2791
|
+
if (visited.has(item.rel) || item.depth > maxDepth)
|
|
2792
|
+
continue;
|
|
2793
|
+
const node = map.get(item.rel);
|
|
2794
|
+
if (!node)
|
|
2795
|
+
continue;
|
|
2796
|
+
visited.set(item.rel, { node, depth: item.depth });
|
|
2797
|
+
if (item.depth < maxDepth) {
|
|
2798
|
+
const neighbors = direction === "forward" ? node.imports : node.importedBy;
|
|
2799
|
+
for (const n of neighbors)
|
|
2800
|
+
queue.push({ rel: n, depth: item.depth + 1 });
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
return visited;
|
|
2804
|
+
}
|
|
2805
|
+
function detectDepCycles(map, subset) {
|
|
2806
|
+
const cycles = [];
|
|
2807
|
+
const visited = new Set();
|
|
2808
|
+
const stack = new Set();
|
|
2809
|
+
function dfs(rel, currentPath) {
|
|
2810
|
+
if (stack.has(rel)) {
|
|
2811
|
+
const idx = currentPath.indexOf(rel);
|
|
2812
|
+
if (idx >= 0)
|
|
2813
|
+
cycles.push([...currentPath.slice(idx), rel]);
|
|
2814
|
+
return;
|
|
2815
|
+
}
|
|
2816
|
+
if (visited.has(rel))
|
|
2817
|
+
return;
|
|
2818
|
+
visited.add(rel);
|
|
2819
|
+
stack.add(rel);
|
|
2820
|
+
const node = map.get(rel);
|
|
2821
|
+
if (node) {
|
|
2822
|
+
for (const imp of node.imports) {
|
|
2823
|
+
if (!subset || subset.has(imp))
|
|
2824
|
+
dfs(imp, [...currentPath, rel]);
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
stack.delete(rel);
|
|
2828
|
+
}
|
|
2829
|
+
const keys = subset ? [...subset] : [...map.keys()];
|
|
2830
|
+
for (const k of keys)
|
|
2831
|
+
dfs(k, []);
|
|
2832
|
+
return cycles;
|
|
2833
|
+
}
|
|
2834
|
+
function formatDGText(targetNode, forwardVisited, backwardVisited, cycles, includeExternal, showReverse) {
|
|
2835
|
+
const lines = [`📦 ${targetNode.rel}`];
|
|
2836
|
+
const directImports = targetNode.imports;
|
|
2837
|
+
const externalImps = includeExternal ? targetNode.externalImports : [];
|
|
2838
|
+
const allImports = [...directImports, ...externalImps];
|
|
2839
|
+
if (allImports.length > 0) {
|
|
2840
|
+
lines.push("├── importa:");
|
|
2841
|
+
allImports.forEach((imp, i) => {
|
|
2842
|
+
const isLast = i === allImports.length - 1;
|
|
2843
|
+
const prefix = isLast ? "│ └──" : "│ ├──";
|
|
2844
|
+
const isExt = externalImps.includes(imp);
|
|
2845
|
+
if (isExt) {
|
|
2846
|
+
lines.push(`${prefix} ${imp} (external)`);
|
|
2847
|
+
}
|
|
2848
|
+
else {
|
|
2849
|
+
const impNode = forwardVisited.get(imp)?.node;
|
|
2850
|
+
const expSuffix = impNode && impNode.exports.length > 0
|
|
2851
|
+
? ` → ${impNode.exports.slice(0, 3).join(", ")}${impNode.exports.length > 3 ? "…" : ""}`
|
|
2852
|
+
: "";
|
|
2853
|
+
lines.push(`${prefix} ${imp}${expSuffix}`);
|
|
2854
|
+
if (impNode && impNode.imports.length > 0) {
|
|
2855
|
+
impNode.imports.forEach((subImp, si) => {
|
|
2856
|
+
const subPrefix = si === impNode.imports.length - 1 ? "│ └──" : "│ ├──";
|
|
2857
|
+
if (subImp !== targetNode.rel)
|
|
2858
|
+
lines.push(`${subPrefix} ${subImp}`);
|
|
2859
|
+
});
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
});
|
|
2863
|
+
}
|
|
2864
|
+
else {
|
|
2865
|
+
lines.push("├── importa: (ninguno)");
|
|
2866
|
+
}
|
|
2867
|
+
if (showReverse) {
|
|
2868
|
+
const revDeps = targetNode.importedBy;
|
|
2869
|
+
if (revDeps.length > 0) {
|
|
2870
|
+
lines.push("├── es importado por:");
|
|
2871
|
+
revDeps.forEach((dep, i) => {
|
|
2872
|
+
const prefix = i === revDeps.length - 1 ? "│ └──" : "│ ├──";
|
|
2873
|
+
lines.push(`${prefix} ${dep}`);
|
|
2874
|
+
const depNode = backwardVisited.get(dep)?.node;
|
|
2875
|
+
if (depNode && depNode.importedBy.length > 0) {
|
|
2876
|
+
depNode.importedBy.forEach((subDep, si) => {
|
|
2877
|
+
const subPrefix = si === depNode.importedBy.length - 1 ? "│ └──" : "│ ├──";
|
|
2878
|
+
if (subDep !== targetNode.rel)
|
|
2879
|
+
lines.push(`${subPrefix} ${subDep}`);
|
|
2880
|
+
});
|
|
2881
|
+
}
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2884
|
+
else {
|
|
2885
|
+
lines.push("├── es importado por: (nadie — posible dead export)");
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
if (targetNode.exports.length > 0) {
|
|
2889
|
+
lines.push("└── exporta:");
|
|
2890
|
+
targetNode.exports.forEach((exp, i) => {
|
|
2891
|
+
const prefix = i === targetNode.exports.length - 1 ? " └──" : " ├──";
|
|
2892
|
+
lines.push(`${prefix} ${exp}`);
|
|
2893
|
+
});
|
|
2894
|
+
}
|
|
2895
|
+
else {
|
|
2896
|
+
lines.push("└── exporta: (ningún export detectado)");
|
|
2897
|
+
}
|
|
2898
|
+
if (cycles.length > 0) {
|
|
2899
|
+
lines.push("");
|
|
2900
|
+
lines.push("⚠️ DEPENDENCIAS CIRCULARES DETECTADAS:");
|
|
2901
|
+
cycles.slice(0, 5).forEach((c) => lines.push(` 🔄 ${c.join(" → ")}`));
|
|
2902
|
+
}
|
|
2903
|
+
return lines.join("\n");
|
|
2904
|
+
}
|
|
2905
|
+
function formatDGMermaid(targetNode, forwardVisited, backwardVisited, cycles, includeExternal, showReverse) {
|
|
2906
|
+
const lines = ["flowchart LR"];
|
|
2907
|
+
const addedEdges = new Set();
|
|
2908
|
+
const safeId = (s) => s.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
2909
|
+
const tid = safeId(targetNode.rel);
|
|
2910
|
+
lines.push(` ${tid}["📦 ${path_1.default.basename(targetNode.rel)}"]`);
|
|
2911
|
+
lines.push(` style ${tid} fill:#6366f1,color:#fff,stroke:#4f46e5`);
|
|
2912
|
+
for (const imp of targetNode.imports) {
|
|
2913
|
+
const eid = `${tid}-->${safeId(imp)}`;
|
|
2914
|
+
if (!addedEdges.has(eid)) {
|
|
2915
|
+
addedEdges.add(eid);
|
|
2916
|
+
lines.push(` ${tid} --> ${safeId(imp)}["${path_1.default.basename(imp)}"]`);
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
if (includeExternal) {
|
|
2920
|
+
for (const ext of targetNode.externalImports) {
|
|
2921
|
+
const extId = safeId(`ext_${ext}`);
|
|
2922
|
+
lines.push(` ${tid} --> ${extId}["📦 ${ext}"]`);
|
|
2923
|
+
lines.push(` style ${extId} fill:#374151,color:#9ca3af,stroke:#374151`);
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
for (const [rel, { node }] of forwardVisited) {
|
|
2927
|
+
if (rel === targetNode.rel)
|
|
2928
|
+
continue;
|
|
2929
|
+
for (const imp of node.imports) {
|
|
2930
|
+
const eid = `${safeId(rel)}-->${safeId(imp)}`;
|
|
2931
|
+
if (!addedEdges.has(eid)) {
|
|
2932
|
+
addedEdges.add(eid);
|
|
2933
|
+
lines.push(` ${safeId(rel)} --> ${safeId(imp)}["${path_1.default.basename(imp)}"]`);
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
if (showReverse) {
|
|
2938
|
+
for (const dep of targetNode.importedBy) {
|
|
2939
|
+
const eid = `${safeId(dep)}-->${tid}`;
|
|
2940
|
+
if (!addedEdges.has(eid)) {
|
|
2941
|
+
addedEdges.add(eid);
|
|
2942
|
+
lines.push(` ${safeId(dep)}["${path_1.default.basename(dep)}"] --> ${tid}`);
|
|
2943
|
+
lines.push(` style ${safeId(dep)} fill:#065f46,color:#fff,stroke:#047857`);
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
for (const [rel, { node }] of backwardVisited) {
|
|
2947
|
+
if (rel === targetNode.rel)
|
|
2948
|
+
continue;
|
|
2949
|
+
for (const dep of node.importedBy) {
|
|
2950
|
+
const eid = `${safeId(dep)}-->${safeId(rel)}`;
|
|
2951
|
+
if (!addedEdges.has(eid)) {
|
|
2952
|
+
addedEdges.add(eid);
|
|
2953
|
+
lines.push(` ${safeId(dep)}["${path_1.default.basename(dep)}"] --> ${safeId(rel)}["${path_1.default.basename(rel)}"]`);
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
if (cycles.length > 0) {
|
|
2959
|
+
lines.push(` %% ⚠️ Cycles: ${cycles.length}`);
|
|
2960
|
+
cycles.slice(0, 3).forEach(c => lines.push(` %% ${c.join(" → ")}`));
|
|
2961
|
+
}
|
|
2962
|
+
return lines.join("\n");
|
|
2963
|
+
}
|
|
2964
|
+
async function handleDepgraph(args) {
|
|
2965
|
+
const filePath = args?.filePath;
|
|
2966
|
+
const showReverse = args?.reverse !== false;
|
|
2967
|
+
const depth = Math.min(Math.max(typeof args?.depth === "number" ? args.depth : 1, 1), 5);
|
|
2968
|
+
const format = args?.format || "text";
|
|
2969
|
+
const includeExternal = !!(args?.external);
|
|
2970
|
+
const detectCircular = !!(args?.circular);
|
|
2971
|
+
const workspaceRoot = process.cwd();
|
|
2972
|
+
if (!filePath)
|
|
2973
|
+
throw new Error("filePath is required");
|
|
2974
|
+
try {
|
|
2975
|
+
const { resolved } = (0, utils_1.safeResolvePath)(workspaceRoot, filePath);
|
|
2976
|
+
const relTarget = path_1.default.relative(workspaceRoot, resolved).replace(/\\/g, "/");
|
|
2977
|
+
const map = buildDepGraphMap(workspaceRoot);
|
|
2978
|
+
const targetNode = map.get(relTarget);
|
|
2979
|
+
if (!targetNode) {
|
|
2980
|
+
return { content: [{ type: "text", text: `File not found in workspace graph: ${relTarget}\nCheck path is correct and has .ts/.tsx/.js/.jsx extension.` }] };
|
|
2981
|
+
}
|
|
2982
|
+
const forwardVisited = bfsDepGraph(map, relTarget, "forward", depth);
|
|
2983
|
+
const backwardVisited = showReverse ? bfsDepGraph(map, relTarget, "backward", depth) : new Map();
|
|
2984
|
+
const allRelInScope = new Set([...forwardVisited.keys(), ...(showReverse ? backwardVisited.keys() : [])]);
|
|
2985
|
+
const cycles = detectCircular ? detectDepCycles(map, allRelInScope) : [];
|
|
2986
|
+
let output;
|
|
2987
|
+
if (format === "json") {
|
|
2988
|
+
const jsonResult = {
|
|
2989
|
+
target: relTarget,
|
|
2990
|
+
imports: targetNode.imports,
|
|
2991
|
+
importedBy: showReverse ? targetNode.importedBy : undefined,
|
|
2992
|
+
externalImports: includeExternal ? targetNode.externalImports : undefined,
|
|
2993
|
+
exports: targetNode.exports,
|
|
2994
|
+
cycles: cycles.length > 0 ? cycles : undefined,
|
|
2995
|
+
subGraph: {
|
|
2996
|
+
forward: Object.fromEntries([...forwardVisited.entries()].map(([k, v]) => [k, { depth: v.depth, imports: v.node.imports }])),
|
|
2997
|
+
backward: showReverse ? Object.fromEntries([...backwardVisited.entries()].map(([k, v]) => [k, { depth: v.depth, importedBy: v.node.importedBy }])) : undefined,
|
|
2998
|
+
},
|
|
2999
|
+
};
|
|
3000
|
+
output = JSON.stringify(jsonResult, null, 2);
|
|
3001
|
+
}
|
|
3002
|
+
else if (format === "mermaid") {
|
|
3003
|
+
output = formatDGMermaid(targetNode, forwardVisited, backwardVisited, cycles, includeExternal, showReverse);
|
|
3004
|
+
}
|
|
3005
|
+
else {
|
|
3006
|
+
output = formatDGText(targetNode, forwardVisited, backwardVisited, cycles, includeExternal, showReverse);
|
|
3007
|
+
}
|
|
3008
|
+
const stats = `\n\n📊 Stats: ${forwardVisited.size} nodos forward, ${backwardVisited.size} nodos reverse, depth=${depth}`;
|
|
3009
|
+
return { content: [{ type: "text", text: output + (format === "json" ? "" : stats) }] };
|
|
3010
|
+
}
|
|
3011
|
+
catch (err) {
|
|
3012
|
+
(0, utils_1.logError)("depgraph", err);
|
|
3013
|
+
return { content: [{ type: "text", text: `depgraph failed: ${err.message}` }] };
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
3017
|
+
// ─── refactor ─────────────────────────────────────────────────────────────────
|
|
3018
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
3019
|
+
function escapeRegexStr(s) {
|
|
3020
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3021
|
+
}
|
|
3022
|
+
function findSymbolRefs(workspaceRoot, symbolName) {
|
|
3023
|
+
const results = [];
|
|
3024
|
+
const IGNORE_RF = new Set(["node_modules", ".git", "dist", "chroma_data", ".lemma", "build", "coverage", ".next"]);
|
|
3025
|
+
function walkRF(dir) {
|
|
3026
|
+
let entries = [];
|
|
3027
|
+
try {
|
|
3028
|
+
entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
3029
|
+
}
|
|
3030
|
+
catch {
|
|
3031
|
+
return;
|
|
3032
|
+
}
|
|
3033
|
+
for (const e of entries) {
|
|
3034
|
+
if (IGNORE_RF.has(e.name))
|
|
3035
|
+
continue;
|
|
3036
|
+
const abs = path_1.default.join(dir, e.name);
|
|
3037
|
+
if (e.isDirectory()) {
|
|
3038
|
+
walkRF(abs);
|
|
3039
|
+
}
|
|
3040
|
+
else if (/\.(ts|tsx|js|jsx)$/.test(e.name) && !e.name.endsWith(".d.ts")) {
|
|
3041
|
+
const rel = path_1.default.relative(workspaceRoot, abs).replace(/\\/g, "/");
|
|
3042
|
+
try {
|
|
3043
|
+
const content = fs_1.default.readFileSync(abs, "utf8");
|
|
3044
|
+
if (!content.includes(symbolName))
|
|
3045
|
+
continue;
|
|
3046
|
+
const sf = ts.createSourceFile(abs, content, ts.ScriptTarget.Latest, true);
|
|
3047
|
+
const contentLines = content.split("\n");
|
|
3048
|
+
const identifierLines = new Set();
|
|
3049
|
+
function collectIdents(node) {
|
|
3050
|
+
if (ts.isIdentifier(node) && node.text === symbolName) {
|
|
3051
|
+
const { line } = sf.getLineAndCharacterOfPosition(node.getStart());
|
|
3052
|
+
identifierLines.add(line);
|
|
3053
|
+
}
|
|
3054
|
+
ts.forEachChild(node, collectIdents);
|
|
3055
|
+
}
|
|
3056
|
+
collectIdents(sf);
|
|
3057
|
+
const matchingLines = [...identifierLines].map(li => ({ lineIndex: li, lineContent: contentLines[li] ?? "" }));
|
|
3058
|
+
if (matchingLines.length > 0)
|
|
3059
|
+
results.push({ abs, rel, lines: matchingLines });
|
|
3060
|
+
}
|
|
3061
|
+
catch { /* skip */ }
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
walkRF(workspaceRoot);
|
|
3066
|
+
return results;
|
|
3067
|
+
}
|
|
3068
|
+
function applySymbolRenameInContent(content, fromName, toName) {
|
|
3069
|
+
const regex = new RegExp(`(?<![a-zA-Z0-9_$])${escapeRegexStr(fromName)}(?![a-zA-Z0-9_$])`, "g");
|
|
3070
|
+
return content.replace(regex, toName);
|
|
3071
|
+
}
|
|
3072
|
+
function buildUnifiedDiff(filePath, before, after) {
|
|
3073
|
+
if (before === after)
|
|
3074
|
+
return "";
|
|
3075
|
+
const bl = before.split("\n");
|
|
3076
|
+
const al = after.split("\n");
|
|
3077
|
+
const diffLines = [`--- a/${filePath}`, `+++ b/${filePath}`];
|
|
3078
|
+
const changed = new Set();
|
|
3079
|
+
const maxLen = Math.max(bl.length, al.length);
|
|
3080
|
+
for (let i = 0; i < maxLen; i++) {
|
|
3081
|
+
if (bl[i] !== al[i])
|
|
3082
|
+
changed.add(i);
|
|
3083
|
+
}
|
|
3084
|
+
let i = 0;
|
|
3085
|
+
while (i < maxLen) {
|
|
3086
|
+
if (!changed.has(i)) {
|
|
3087
|
+
i++;
|
|
3088
|
+
continue;
|
|
3089
|
+
}
|
|
3090
|
+
const hunkStart = Math.max(0, i - 2);
|
|
3091
|
+
let hunkEnd = i + 1;
|
|
3092
|
+
while (hunkEnd < maxLen && changed.has(hunkEnd))
|
|
3093
|
+
hunkEnd++;
|
|
3094
|
+
hunkEnd = Math.min(maxLen, hunkEnd + 2);
|
|
3095
|
+
const bc = Math.min(hunkEnd, bl.length) - hunkStart;
|
|
3096
|
+
const ac = Math.min(hunkEnd, al.length) - hunkStart;
|
|
3097
|
+
diffLines.push(`@@ -${hunkStart + 1},${bc} +${hunkStart + 1},${ac} @@`);
|
|
3098
|
+
for (let j = hunkStart; j < hunkEnd; j++) {
|
|
3099
|
+
if (j >= maxLen)
|
|
3100
|
+
break;
|
|
3101
|
+
if (changed.has(j)) {
|
|
3102
|
+
if (j < bl.length)
|
|
3103
|
+
diffLines.push(`-${bl[j]}`);
|
|
3104
|
+
if (j < al.length)
|
|
3105
|
+
diffLines.push(`+${al[j]}`);
|
|
3106
|
+
}
|
|
3107
|
+
else {
|
|
3108
|
+
if (j < bl.length)
|
|
3109
|
+
diffLines.push(` ${bl[j]}`);
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
i = hunkEnd;
|
|
3113
|
+
}
|
|
3114
|
+
return diffLines.join("\n");
|
|
3115
|
+
}
|
|
3116
|
+
async function handleRefactorRename(args, workspaceRoot) {
|
|
3117
|
+
const fromName = args.from;
|
|
3118
|
+
const toName = args.to;
|
|
3119
|
+
const dryRun = !!(args.dryRun);
|
|
3120
|
+
const tscCheck = !!(args.tscCheck);
|
|
3121
|
+
const backup = !!(args.backup);
|
|
3122
|
+
if (!fromName || !toName)
|
|
3123
|
+
throw new Error("'from' and 'to' are required for rename");
|
|
3124
|
+
const refs = findSymbolRefs(workspaceRoot, fromName);
|
|
3125
|
+
if (refs.length === 0) {
|
|
3126
|
+
return { content: [{ type: "text", text: `No references found to symbol "${fromName}" in the workspace.` }] };
|
|
3127
|
+
}
|
|
3128
|
+
let filesModified = 0;
|
|
3129
|
+
let totalRefs = 0;
|
|
3130
|
+
const report = [];
|
|
3131
|
+
const diffs = [];
|
|
3132
|
+
for (const ref of refs) {
|
|
3133
|
+
const before = fs_1.default.readFileSync(ref.abs, "utf8");
|
|
3134
|
+
const after = applySymbolRenameInContent(before, fromName, toName);
|
|
3135
|
+
if (before === after)
|
|
3136
|
+
continue;
|
|
3137
|
+
const count = (before.match(new RegExp(`(?<![a-zA-Z0-9_$])${escapeRegexStr(fromName)}(?![a-zA-Z0-9_$])`, "g")) || []).length;
|
|
3138
|
+
totalRefs += count;
|
|
3139
|
+
filesModified++;
|
|
3140
|
+
if (dryRun) {
|
|
3141
|
+
const d = buildUnifiedDiff(ref.rel, before, after);
|
|
3142
|
+
if (d)
|
|
3143
|
+
diffs.push(d);
|
|
3144
|
+
}
|
|
3145
|
+
else {
|
|
3146
|
+
if (backup)
|
|
3147
|
+
fs_1.default.writeFileSync(ref.abs + ".bak", before, "utf8");
|
|
3148
|
+
fs_1.default.writeFileSync(ref.abs, after, "utf8");
|
|
3149
|
+
report.push(` ✅ ${ref.rel} (${count} refs)`);
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
if (dryRun) {
|
|
3153
|
+
return { content: [{ type: "text", text: [`🔍 DRY RUN — Rename "${fromName}" → "${toName}"`, ``, `Archivos que cambiarían: ${filesModified} | Referencias: ${totalRefs}`, ``, `=== DIFF PREVIEW ===`, ...diffs].join("\n") }] };
|
|
3154
|
+
}
|
|
3155
|
+
let tscOutput = "";
|
|
3156
|
+
if (tscCheck && filesModified > 0) {
|
|
3157
|
+
try {
|
|
3158
|
+
const out = (0, child_process_1.execSync)("npx tsc --noEmit 2>&1 | head -30 || true", { cwd: workspaceRoot, encoding: "utf8", timeout: 30000 });
|
|
3159
|
+
const errors = out.split("\n").filter((l) => l.includes("error TS"));
|
|
3160
|
+
tscOutput = errors.length === 0 ? "\n\n✅ tsc --noEmit: 0 errores" : `\n\n⚠️ tsc: ${errors.length} error(es):\n${errors.join("\n")}`;
|
|
3161
|
+
}
|
|
3162
|
+
catch (e) {
|
|
3163
|
+
tscOutput = `\n\n⚠️ tsc check falló: ${e.message}`;
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
return { content: [{ type: "text", text: [`✅ Rename: "${fromName}" → "${toName}"`, ``, `📁 ${filesModified} archivos | 🔗 ${totalRefs} refs`, ``, ...report].join("\n") + tscOutput }] };
|
|
3167
|
+
}
|
|
3168
|
+
async function handleRefactorMove(args, workspaceRoot) {
|
|
3169
|
+
const fromRel = args.from;
|
|
3170
|
+
const toRel = args.to;
|
|
3171
|
+
const dryRun = !!(args.dryRun);
|
|
3172
|
+
const tscCheck = !!(args.tscCheck);
|
|
3173
|
+
const backup = !!(args.backup);
|
|
3174
|
+
const { resolved: fromAbs } = (0, utils_1.safeResolvePath)(workspaceRoot, fromRel);
|
|
3175
|
+
const toAbs = path_1.default.resolve(workspaceRoot, toRel);
|
|
3176
|
+
const fromRelNorm = path_1.default.relative(workspaceRoot, fromAbs).replace(/\\/g, "/");
|
|
3177
|
+
const toRelNorm = path_1.default.relative(workspaceRoot, toAbs).replace(/\\/g, "/");
|
|
3178
|
+
if (!fs_1.default.existsSync(fromAbs)) {
|
|
3179
|
+
return { content: [{ type: "text", text: `Source file not found: ${fromRelNorm}` }] };
|
|
3180
|
+
}
|
|
3181
|
+
const IGNORE_MV = new Set(["node_modules", ".git", "dist", "chroma_data", ".lemma", "build", "coverage", ".next"]);
|
|
3182
|
+
const importers = [];
|
|
3183
|
+
function walkMV(dir) {
|
|
3184
|
+
let entries = [];
|
|
3185
|
+
try {
|
|
3186
|
+
entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
3187
|
+
}
|
|
3188
|
+
catch {
|
|
3189
|
+
return;
|
|
3190
|
+
}
|
|
3191
|
+
for (const e of entries) {
|
|
3192
|
+
if (IGNORE_MV.has(e.name))
|
|
3193
|
+
continue;
|
|
3194
|
+
const abs = path_1.default.join(dir, e.name);
|
|
3195
|
+
if (e.isDirectory()) {
|
|
3196
|
+
walkMV(abs);
|
|
3197
|
+
}
|
|
3198
|
+
else if (/\.(ts|tsx|js|jsx)$/.test(e.name) && !e.name.endsWith(".d.ts")) {
|
|
3199
|
+
const rel = path_1.default.relative(workspaceRoot, abs).replace(/\\/g, "/");
|
|
3200
|
+
if (rel === fromRelNorm)
|
|
3201
|
+
return;
|
|
3202
|
+
try {
|
|
3203
|
+
const content = fs_1.default.readFileSync(abs, "utf8");
|
|
3204
|
+
const sf = ts.createSourceFile(abs, content, ts.ScriptTarget.Latest, true);
|
|
3205
|
+
let found = false;
|
|
3206
|
+
ts.forEachChild(sf, (n) => {
|
|
3207
|
+
if (!found && ts.isImportDeclaration(n) && ts.isStringLiteral(n.moduleSpecifier)) {
|
|
3208
|
+
const spec = n.moduleSpecifier.text;
|
|
3209
|
+
if (spec.startsWith(".")) {
|
|
3210
|
+
const res = path_1.default.resolve(path_1.default.dirname(abs), spec).replace(/\\/g, "/");
|
|
3211
|
+
const fromN = fromAbs.replace(/\\/g, "/");
|
|
3212
|
+
const fromNoExt = fromN.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
3213
|
+
if (res === fromN || res === fromNoExt)
|
|
3214
|
+
found = true;
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
});
|
|
3218
|
+
if (found)
|
|
3219
|
+
importers.push({ abs, rel, content });
|
|
3220
|
+
}
|
|
3221
|
+
catch { /* skip */ }
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
walkMV(workspaceRoot);
|
|
3226
|
+
const addDot = (p) => p.startsWith(".") ? p : `./${p}`;
|
|
3227
|
+
const patches = [];
|
|
3228
|
+
for (const imp of importers) {
|
|
3229
|
+
const oldRelImport = path_1.default.relative(path_1.default.dirname(imp.abs), fromAbs).replace(/\\/g, "/");
|
|
3230
|
+
const newRelImport = path_1.default.relative(path_1.default.dirname(imp.abs), toAbs).replace(/\\/g, "/");
|
|
3231
|
+
const oldSpec = addDot(oldRelImport.replace(/\.(ts|tsx|js|jsx)$/, ""));
|
|
3232
|
+
const newSpec = addDot(newRelImport.replace(/\.(ts|tsx|js|jsx)$/, ""));
|
|
3233
|
+
const after = imp.content.replace(new RegExp(`(['"])${escapeRegexStr(oldSpec)}(['"])`, "g"), `$1${newSpec}$2`);
|
|
3234
|
+
if (after !== imp.content)
|
|
3235
|
+
patches.push({ rel: imp.rel, abs: imp.abs, before: imp.content, after });
|
|
3236
|
+
}
|
|
3237
|
+
if (dryRun) {
|
|
3238
|
+
const diffs = patches.map(p => buildUnifiedDiff(p.rel, p.before, p.after)).filter(Boolean);
|
|
3239
|
+
return { content: [{ type: "text", text: [`🔍 DRY RUN — Move "${fromRelNorm}" → "${toRelNorm}"`, ``, `Importadores que se actualizarían: ${patches.length}`, ``, `=== DIFF PREVIEW ===`, ...diffs].join("\n") }] };
|
|
3240
|
+
}
|
|
3241
|
+
fs_1.default.mkdirSync(path_1.default.dirname(toAbs), { recursive: true });
|
|
3242
|
+
if (backup)
|
|
3243
|
+
fs_1.default.copyFileSync(fromAbs, fromAbs + ".bak");
|
|
3244
|
+
fs_1.default.copyFileSync(fromAbs, toAbs);
|
|
3245
|
+
fs_1.default.unlinkSync(fromAbs);
|
|
3246
|
+
for (const p of patches) {
|
|
3247
|
+
if (backup)
|
|
3248
|
+
fs_1.default.writeFileSync(p.abs + ".bak", p.before, "utf8");
|
|
3249
|
+
fs_1.default.writeFileSync(p.abs, p.after, "utf8");
|
|
3250
|
+
}
|
|
3251
|
+
let tscOutput = "";
|
|
3252
|
+
if (tscCheck) {
|
|
3253
|
+
try {
|
|
3254
|
+
const out = (0, child_process_1.execSync)("npx tsc --noEmit 2>&1 | head -30 || true", { cwd: workspaceRoot, encoding: "utf8", timeout: 30000 });
|
|
3255
|
+
const errors = out.split("\n").filter((l) => l.includes("error TS"));
|
|
3256
|
+
tscOutput = errors.length === 0 ? "\n\n✅ tsc --noEmit: 0 errores" : `\n\n⚠️ tsc: ${errors.length} error(es):\n${errors.join("\n")}`;
|
|
3257
|
+
}
|
|
3258
|
+
catch (e) {
|
|
3259
|
+
tscOutput = `\n\n⚠️ tsc check falló: ${e.message}`;
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
return { content: [{ type: "text", text: [`✅ Move: "${fromRelNorm}" → "${toRelNorm}"`, ``, `📁 ${patches.length} importadores actualizados:`, ...patches.map(p => ` ✅ ${p.rel}`)].join("\n") + tscOutput }] };
|
|
3263
|
+
}
|
|
3264
|
+
async function handleRefactor(args) {
|
|
3265
|
+
const operation = args?.operation;
|
|
3266
|
+
const workspaceRoot = process.cwd();
|
|
3267
|
+
if (!operation)
|
|
3268
|
+
throw new Error("'operation' is required (rename | move)");
|
|
3269
|
+
try {
|
|
3270
|
+
switch (operation) {
|
|
3271
|
+
case "rename": return await handleRefactorRename(args, workspaceRoot);
|
|
3272
|
+
case "move": return await handleRefactorMove(args, workspaceRoot);
|
|
3273
|
+
default:
|
|
3274
|
+
return { content: [{ type: "text", text: `Unknown operation: "${operation}". Supported: rename, move` }] };
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
catch (err) {
|
|
3278
|
+
(0, utils_1.logError)("refactor", err);
|
|
3279
|
+
return { content: [{ type: "text", text: `refactor failed: ${err.message}` }] };
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
2663
3282
|
//# sourceMappingURL=tools.js.map
|