activo 0.2.2 β 0.3.1
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 +87 -3
- package/dist/core/llm/ollama.d.ts +2 -0
- package/dist/core/llm/ollama.d.ts.map +1 -1
- package/dist/core/llm/ollama.js +26 -0
- package/dist/core/llm/ollama.js.map +1 -1
- package/dist/core/tools/ast.d.ts +81 -0
- package/dist/core/tools/ast.d.ts.map +1 -0
- package/dist/core/tools/ast.js +700 -0
- package/dist/core/tools/ast.js.map +1 -0
- package/dist/core/tools/cache.d.ts +19 -0
- package/dist/core/tools/cache.d.ts.map +1 -0
- package/dist/core/tools/cache.js +497 -0
- package/dist/core/tools/cache.js.map +1 -0
- package/dist/core/tools/cssAnalysis.d.ts +3 -0
- package/dist/core/tools/cssAnalysis.d.ts.map +1 -0
- package/dist/core/tools/cssAnalysis.js +270 -0
- package/dist/core/tools/cssAnalysis.js.map +1 -0
- package/dist/core/tools/dependencyAnalysis.d.ts +3 -0
- package/dist/core/tools/dependencyAnalysis.d.ts.map +1 -0
- package/dist/core/tools/dependencyAnalysis.js +295 -0
- package/dist/core/tools/dependencyAnalysis.js.map +1 -0
- package/dist/core/tools/embeddings.d.ts +8 -0
- package/dist/core/tools/embeddings.d.ts.map +1 -0
- package/dist/core/tools/embeddings.js +631 -0
- package/dist/core/tools/embeddings.js.map +1 -0
- package/dist/core/tools/frontendAst.d.ts +6 -0
- package/dist/core/tools/frontendAst.d.ts.map +1 -0
- package/dist/core/tools/frontendAst.js +680 -0
- package/dist/core/tools/frontendAst.js.map +1 -0
- package/dist/core/tools/htmlAnalysis.d.ts +3 -0
- package/dist/core/tools/htmlAnalysis.d.ts.map +1 -0
- package/dist/core/tools/htmlAnalysis.js +398 -0
- package/dist/core/tools/htmlAnalysis.js.map +1 -0
- package/dist/core/tools/index.d.ts +13 -0
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +27 -1
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/javaAst.d.ts +6 -0
- package/dist/core/tools/javaAst.d.ts.map +1 -0
- package/dist/core/tools/javaAst.js +678 -0
- package/dist/core/tools/javaAst.js.map +1 -0
- package/dist/core/tools/memory.d.ts +11 -0
- package/dist/core/tools/memory.d.ts.map +1 -0
- package/dist/core/tools/memory.js +551 -0
- package/dist/core/tools/memory.js.map +1 -0
- package/dist/core/tools/mybatisAnalysis.d.ts +3 -0
- package/dist/core/tools/mybatisAnalysis.d.ts.map +1 -0
- package/dist/core/tools/mybatisAnalysis.js +251 -0
- package/dist/core/tools/mybatisAnalysis.js.map +1 -0
- package/dist/core/tools/openapiAnalysis.d.ts +3 -0
- package/dist/core/tools/openapiAnalysis.d.ts.map +1 -0
- package/dist/core/tools/openapiAnalysis.js +356 -0
- package/dist/core/tools/openapiAnalysis.js.map +1 -0
- package/dist/core/tools/pythonAnalysis.d.ts +3 -0
- package/dist/core/tools/pythonAnalysis.d.ts.map +1 -0
- package/dist/core/tools/pythonAnalysis.js +387 -0
- package/dist/core/tools/pythonAnalysis.js.map +1 -0
- package/dist/core/tools/sqlAnalysis.d.ts +3 -0
- package/dist/core/tools/sqlAnalysis.d.ts.map +1 -0
- package/dist/core/tools/sqlAnalysis.js +250 -0
- package/dist/core/tools/sqlAnalysis.js.map +1 -0
- package/package.json +2 -1
- package/src/core/llm/ollama.ts +30 -0
- package/src/core/tools/ast.ts +826 -0
- package/src/core/tools/cache.ts +570 -0
- package/src/core/tools/cssAnalysis.ts +324 -0
- package/src/core/tools/dependencyAnalysis.ts +363 -0
- package/src/core/tools/embeddings.ts +746 -0
- package/src/core/tools/frontendAst.ts +802 -0
- package/src/core/tools/htmlAnalysis.ts +466 -0
- package/src/core/tools/index.ts +27 -1
- package/src/core/tools/javaAst.ts +812 -0
- package/src/core/tools/memory.ts +655 -0
- package/src/core/tools/mybatisAnalysis.ts +322 -0
- package/src/core/tools/openapiAnalysis.ts +431 -0
- package/src/core/tools/pythonAnalysis.ts +477 -0
- package/src/core/tools/sqlAnalysis.ts +298 -0
- package/FINAL_SIMPLIFIED_SPEC.md +0 -456
- package/TODO.md +0 -193
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
import { Tool, ToolResult } from "./types.js";
|
|
5
|
+
|
|
6
|
+
// React component info
|
|
7
|
+
interface ReactComponentInfo {
|
|
8
|
+
name: string;
|
|
9
|
+
type: "function" | "class" | "arrow";
|
|
10
|
+
filepath: string;
|
|
11
|
+
line: number;
|
|
12
|
+
props: string[];
|
|
13
|
+
hooks: string[];
|
|
14
|
+
stateVariables: string[];
|
|
15
|
+
effects: number;
|
|
16
|
+
memoized: boolean;
|
|
17
|
+
issues: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Vue component info
|
|
21
|
+
interface VueComponentInfo {
|
|
22
|
+
name: string;
|
|
23
|
+
filepath: string;
|
|
24
|
+
type: "options" | "composition" | "script-setup";
|
|
25
|
+
props: string[];
|
|
26
|
+
emits: string[];
|
|
27
|
+
data: string[];
|
|
28
|
+
computed: string[];
|
|
29
|
+
methods: string[];
|
|
30
|
+
watchers: string[];
|
|
31
|
+
lifecycle: string[];
|
|
32
|
+
composables: string[];
|
|
33
|
+
issues: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// jQuery usage info
|
|
37
|
+
interface JQueryUsageInfo {
|
|
38
|
+
filepath: string;
|
|
39
|
+
selectors: Array<{ selector: string; line: number }>;
|
|
40
|
+
events: Array<{ event: string; line: number }>;
|
|
41
|
+
ajax: Array<{ method: string; line: number }>;
|
|
42
|
+
deprecated: Array<{ method: string; line: number; suggestion: string }>;
|
|
43
|
+
domManipulations: number;
|
|
44
|
+
issues: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Deprecated jQuery methods
|
|
48
|
+
const DEPRECATED_JQUERY: Record<string, string> = {
|
|
49
|
+
".bind(": "Use .on() instead",
|
|
50
|
+
".unbind(": "Use .off() instead",
|
|
51
|
+
".delegate(": "Use .on() instead",
|
|
52
|
+
".undelegate(": "Use .off() instead",
|
|
53
|
+
".live(": "Use .on() instead",
|
|
54
|
+
".die(": "Use .off() instead",
|
|
55
|
+
".load(": "Use .on('load', ...) instead",
|
|
56
|
+
".unload(": "Use .on('unload', ...) instead",
|
|
57
|
+
".error(": "Use .on('error', ...) instead",
|
|
58
|
+
".size(": "Use .length instead",
|
|
59
|
+
".andSelf(": "Use .addBack() instead",
|
|
60
|
+
".toggle(": "Use .on('click', ...) with state instead",
|
|
61
|
+
"$.browser": "Use feature detection instead",
|
|
62
|
+
"$.sub(": "Removed in jQuery 1.9",
|
|
63
|
+
"$.isArray(": "Use Array.isArray() instead",
|
|
64
|
+
"$.parseJSON(": "Use JSON.parse() instead",
|
|
65
|
+
"$.trim(": "Use String.trim() instead",
|
|
66
|
+
"$.now()": "Use Date.now() instead",
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// React hooks list
|
|
70
|
+
const REACT_HOOKS = [
|
|
71
|
+
"useState",
|
|
72
|
+
"useEffect",
|
|
73
|
+
"useContext",
|
|
74
|
+
"useReducer",
|
|
75
|
+
"useCallback",
|
|
76
|
+
"useMemo",
|
|
77
|
+
"useRef",
|
|
78
|
+
"useImperativeHandle",
|
|
79
|
+
"useLayoutEffect",
|
|
80
|
+
"useDebugValue",
|
|
81
|
+
"useDeferredValue",
|
|
82
|
+
"useTransition",
|
|
83
|
+
"useId",
|
|
84
|
+
"useSyncExternalStore",
|
|
85
|
+
"useInsertionEffect",
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
// Analyze React component
|
|
89
|
+
function analyzeReactFile(content: string, filepath: string): ReactComponentInfo[] {
|
|
90
|
+
const components: ReactComponentInfo[] = [];
|
|
91
|
+
const lines = content.split("\n");
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const sourceFile = ts.createSourceFile(
|
|
95
|
+
filepath,
|
|
96
|
+
content,
|
|
97
|
+
ts.ScriptTarget.Latest,
|
|
98
|
+
true,
|
|
99
|
+
filepath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.JSX
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
function visit(node: ts.Node) {
|
|
103
|
+
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
104
|
+
|
|
105
|
+
// Function component
|
|
106
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
107
|
+
const name = node.name.text;
|
|
108
|
+
if (/^[A-Z]/.test(name)) {
|
|
109
|
+
const component = analyzeReactComponent(node, name, "function", line, content, filepath);
|
|
110
|
+
if (component) components.push(component);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Arrow function component
|
|
115
|
+
if (ts.isVariableStatement(node)) {
|
|
116
|
+
for (const decl of node.declarationList.declarations) {
|
|
117
|
+
if (ts.isIdentifier(decl.name) && decl.initializer) {
|
|
118
|
+
const name = decl.name.text;
|
|
119
|
+
if (/^[A-Z]/.test(name)) {
|
|
120
|
+
if (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer)) {
|
|
121
|
+
const component = analyzeReactComponent(decl.initializer, name, "arrow", line, content, filepath);
|
|
122
|
+
if (component) components.push(component);
|
|
123
|
+
}
|
|
124
|
+
// Check for React.memo, React.forwardRef
|
|
125
|
+
if (ts.isCallExpression(decl.initializer)) {
|
|
126
|
+
const expr = decl.initializer.expression;
|
|
127
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
128
|
+
const methodName = expr.name.text;
|
|
129
|
+
if (["memo", "forwardRef"].includes(methodName)) {
|
|
130
|
+
const arg = decl.initializer.arguments[0];
|
|
131
|
+
if (arg && (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg))) {
|
|
132
|
+
const component = analyzeReactComponent(arg, name, "arrow", line, content, filepath);
|
|
133
|
+
if (component) {
|
|
134
|
+
component.memoized = methodName === "memo";
|
|
135
|
+
components.push(component);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Class component
|
|
147
|
+
if (ts.isClassDeclaration(node) && node.name) {
|
|
148
|
+
const name = node.name.text;
|
|
149
|
+
// Check if extends React.Component or Component
|
|
150
|
+
if (node.heritageClauses) {
|
|
151
|
+
for (const clause of node.heritageClauses) {
|
|
152
|
+
const extendsText = clause.types.map((t) => t.expression.getText(sourceFile)).join(", ");
|
|
153
|
+
if (extendsText.includes("Component") || extendsText.includes("PureComponent")) {
|
|
154
|
+
const component: ReactComponentInfo = {
|
|
155
|
+
name,
|
|
156
|
+
type: "class",
|
|
157
|
+
filepath,
|
|
158
|
+
line,
|
|
159
|
+
props: [],
|
|
160
|
+
hooks: [],
|
|
161
|
+
stateVariables: [],
|
|
162
|
+
effects: 0,
|
|
163
|
+
memoized: extendsText.includes("PureComponent"),
|
|
164
|
+
issues: ["Class components are legacy - consider converting to function components"],
|
|
165
|
+
};
|
|
166
|
+
components.push(component);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
ts.forEachChild(node, visit);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
visit(sourceFile);
|
|
176
|
+
} catch {
|
|
177
|
+
// Fallback to regex
|
|
178
|
+
const componentRegex = /(?:function|const|let|var)\s+([A-Z]\w+)\s*[=:]\s*(?:\([^)]*\)|[^=])*=>/g;
|
|
179
|
+
let match;
|
|
180
|
+
while ((match = componentRegex.exec(content)) !== null) {
|
|
181
|
+
const line = content.substring(0, match.index).split("\n").length;
|
|
182
|
+
components.push({
|
|
183
|
+
name: match[1],
|
|
184
|
+
type: "arrow",
|
|
185
|
+
filepath,
|
|
186
|
+
line,
|
|
187
|
+
props: [],
|
|
188
|
+
hooks: [],
|
|
189
|
+
stateVariables: [],
|
|
190
|
+
effects: 0,
|
|
191
|
+
memoized: false,
|
|
192
|
+
issues: [],
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return components;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Analyze a React component node
|
|
201
|
+
function analyzeReactComponent(
|
|
202
|
+
node: ts.Node,
|
|
203
|
+
name: string,
|
|
204
|
+
type: "function" | "arrow",
|
|
205
|
+
line: number,
|
|
206
|
+
content: string,
|
|
207
|
+
filepath: string
|
|
208
|
+
): ReactComponentInfo | null {
|
|
209
|
+
const component: ReactComponentInfo = {
|
|
210
|
+
name,
|
|
211
|
+
type,
|
|
212
|
+
filepath,
|
|
213
|
+
line,
|
|
214
|
+
props: [],
|
|
215
|
+
hooks: [],
|
|
216
|
+
stateVariables: [],
|
|
217
|
+
effects: 0,
|
|
218
|
+
memoized: false,
|
|
219
|
+
issues: [],
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const nodeText = node.getText();
|
|
223
|
+
|
|
224
|
+
// Find hooks
|
|
225
|
+
for (const hook of REACT_HOOKS) {
|
|
226
|
+
const hookRegex = new RegExp(`\\b${hook}\\s*\\(`, "g");
|
|
227
|
+
const matches = nodeText.match(hookRegex);
|
|
228
|
+
if (matches) {
|
|
229
|
+
component.hooks.push(`${hook} (${matches.length})`);
|
|
230
|
+
if (hook === "useEffect" || hook === "useLayoutEffect") {
|
|
231
|
+
component.effects += matches.length;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Find useState variables
|
|
237
|
+
const useStateRegex = /const\s+\[(\w+),\s*set\w+\]\s*=\s*useState/g;
|
|
238
|
+
let match;
|
|
239
|
+
while ((match = useStateRegex.exec(nodeText)) !== null) {
|
|
240
|
+
component.stateVariables.push(match[1]);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Check for issues
|
|
244
|
+
// 1. Missing dependency array in useEffect
|
|
245
|
+
const effectWithoutDeps = /useEffect\s*\(\s*\([^)]*\)\s*=>\s*\{[^}]+\}\s*\)/g;
|
|
246
|
+
if (effectWithoutDeps.test(nodeText)) {
|
|
247
|
+
component.issues.push("useEffect without dependency array - may cause infinite loops");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 2. Too many state variables
|
|
251
|
+
if (component.stateVariables.length > 5) {
|
|
252
|
+
component.issues.push(`Too many useState (${component.stateVariables.length}) - consider useReducer`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 3. Too many effects
|
|
256
|
+
if (component.effects > 3) {
|
|
257
|
+
component.issues.push(`Too many useEffect (${component.effects}) - consider splitting component`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 4. Check for inline function in JSX (performance issue)
|
|
261
|
+
const inlineHandler = /on\w+\s*=\s*\{\s*\([^)]*\)\s*=>/g;
|
|
262
|
+
if (inlineHandler.test(nodeText)) {
|
|
263
|
+
component.issues.push("Inline arrow functions in JSX props - consider useCallback");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return component;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Analyze Vue file
|
|
270
|
+
function analyzeVueFile(content: string, filepath: string): VueComponentInfo {
|
|
271
|
+
const component: VueComponentInfo = {
|
|
272
|
+
name: path.basename(filepath, path.extname(filepath)),
|
|
273
|
+
filepath,
|
|
274
|
+
type: "options",
|
|
275
|
+
props: [],
|
|
276
|
+
emits: [],
|
|
277
|
+
data: [],
|
|
278
|
+
computed: [],
|
|
279
|
+
methods: [],
|
|
280
|
+
watchers: [],
|
|
281
|
+
lifecycle: [],
|
|
282
|
+
composables: [],
|
|
283
|
+
issues: [],
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// Check for script setup (Composition API with <script setup>)
|
|
287
|
+
if (/<script\s+setup/.test(content)) {
|
|
288
|
+
component.type = "script-setup";
|
|
289
|
+
|
|
290
|
+
// Find defineProps
|
|
291
|
+
const propsMatch = content.match(/defineProps\s*[<(]([^>)]+)[>)]/);
|
|
292
|
+
if (propsMatch) {
|
|
293
|
+
const propsContent = propsMatch[1];
|
|
294
|
+
const propNames = propsContent.match(/\w+(?=\s*[?:])/g);
|
|
295
|
+
if (propNames) component.props = propNames;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Find defineEmits
|
|
299
|
+
const emitsMatch = content.match(/defineEmits\s*[<(]([^>)]+)[>)]/);
|
|
300
|
+
if (emitsMatch) {
|
|
301
|
+
const emitsContent = emitsMatch[1];
|
|
302
|
+
const emitNames = emitsContent.match(/['"](\w+)['"]/g);
|
|
303
|
+
if (emitNames) component.emits = emitNames.map((e) => e.replace(/['"]/g, ""));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Find composables (useXxx)
|
|
307
|
+
const composableRegex = /\buse[A-Z]\w+\s*\(/g;
|
|
308
|
+
const composables = content.match(composableRegex);
|
|
309
|
+
if (composables) {
|
|
310
|
+
component.composables = [...new Set(composables.map((c) => c.replace("(", "")))];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Find refs and reactives
|
|
314
|
+
const refRegex = /(?:const|let)\s+(\w+)\s*=\s*(?:ref|reactive|computed)\s*\(/g;
|
|
315
|
+
let match;
|
|
316
|
+
while ((match = refRegex.exec(content)) !== null) {
|
|
317
|
+
component.data.push(match[1]);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// Check for Composition API (setup function)
|
|
321
|
+
else if (/setup\s*\(\s*(?:props|[^)]*)\s*\)\s*\{/.test(content)) {
|
|
322
|
+
component.type = "composition";
|
|
323
|
+
|
|
324
|
+
// Find return object properties
|
|
325
|
+
const returnMatch = content.match(/return\s*\{([^}]+)\}/);
|
|
326
|
+
if (returnMatch) {
|
|
327
|
+
const returnContent = returnMatch[1];
|
|
328
|
+
const names = returnContent.match(/\w+(?=\s*[,}])/g);
|
|
329
|
+
if (names) component.methods = names;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Options API
|
|
333
|
+
else {
|
|
334
|
+
component.type = "options";
|
|
335
|
+
|
|
336
|
+
// Props
|
|
337
|
+
const propsMatch = content.match(/props\s*:\s*\[([^\]]+)\]/);
|
|
338
|
+
if (propsMatch) {
|
|
339
|
+
component.props = propsMatch[1].match(/['"](\w+)['"]/g)?.map((p) => p.replace(/['"]/g, "")) || [];
|
|
340
|
+
}
|
|
341
|
+
const propsObjMatch = content.match(/props\s*:\s*\{([^}]+)\}/);
|
|
342
|
+
if (propsObjMatch) {
|
|
343
|
+
const propNames = propsObjMatch[1].match(/(\w+)\s*:/g);
|
|
344
|
+
if (propNames) component.props = propNames.map((p) => p.replace(":", "").trim());
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Data
|
|
348
|
+
const dataMatch = content.match(/data\s*\(\s*\)\s*\{[^}]*return\s*\{([^}]+)\}/);
|
|
349
|
+
if (dataMatch) {
|
|
350
|
+
const dataNames = dataMatch[1].match(/(\w+)\s*:/g);
|
|
351
|
+
if (dataNames) component.data = dataNames.map((d) => d.replace(":", "").trim());
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Computed
|
|
355
|
+
const computedMatch = content.match(/computed\s*:\s*\{([^}]+)\}/);
|
|
356
|
+
if (computedMatch) {
|
|
357
|
+
const computedNames = computedMatch[1].match(/(\w+)\s*[:(]/g);
|
|
358
|
+
if (computedNames) component.computed = computedNames.map((c) => c.replace(/[:(]/g, "").trim());
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Methods
|
|
362
|
+
const methodsMatch = content.match(/methods\s*:\s*\{([^}]+)\}/);
|
|
363
|
+
if (methodsMatch) {
|
|
364
|
+
const methodNames = methodsMatch[1].match(/(\w+)\s*\(/g);
|
|
365
|
+
if (methodNames) component.methods = methodNames.map((m) => m.replace("(", "").trim());
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Watch
|
|
369
|
+
const watchMatch = content.match(/watch\s*:\s*\{([^}]+)\}/);
|
|
370
|
+
if (watchMatch) {
|
|
371
|
+
const watchNames = watchMatch[1].match(/['"]?(\w+)['"]?\s*[:(]/g);
|
|
372
|
+
if (watchNames) component.watchers = watchNames.map((w) => w.replace(/['":()]/g, "").trim());
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Lifecycle hooks (both APIs)
|
|
377
|
+
const lifecycleHooks = [
|
|
378
|
+
"beforeCreate", "created", "beforeMount", "mounted",
|
|
379
|
+
"beforeUpdate", "updated", "beforeUnmount", "unmounted",
|
|
380
|
+
"onBeforeMount", "onMounted", "onBeforeUpdate", "onUpdated",
|
|
381
|
+
"onBeforeUnmount", "onUnmounted",
|
|
382
|
+
];
|
|
383
|
+
for (const hook of lifecycleHooks) {
|
|
384
|
+
if (content.includes(hook)) {
|
|
385
|
+
component.lifecycle.push(hook);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Issues
|
|
390
|
+
if (component.type === "options" && component.methods.length > 10) {
|
|
391
|
+
component.issues.push("Too many methods - consider splitting component");
|
|
392
|
+
}
|
|
393
|
+
if (component.data.length > 10) {
|
|
394
|
+
component.issues.push("Too many data properties - consider splitting component");
|
|
395
|
+
}
|
|
396
|
+
if (component.watchers.length > 5) {
|
|
397
|
+
component.issues.push("Too many watchers - may impact performance");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return component;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Analyze jQuery usage
|
|
404
|
+
function analyzeJQueryFile(content: string, filepath: string): JQueryUsageInfo {
|
|
405
|
+
const info: JQueryUsageInfo = {
|
|
406
|
+
filepath,
|
|
407
|
+
selectors: [],
|
|
408
|
+
events: [],
|
|
409
|
+
ajax: [],
|
|
410
|
+
deprecated: [],
|
|
411
|
+
domManipulations: 0,
|
|
412
|
+
issues: [],
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const lines = content.split("\n");
|
|
416
|
+
|
|
417
|
+
for (let i = 0; i < lines.length; i++) {
|
|
418
|
+
const line = lines[i];
|
|
419
|
+
const lineNum = i + 1;
|
|
420
|
+
|
|
421
|
+
// Find selectors
|
|
422
|
+
const selectorRegex = /\$\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
423
|
+
let match;
|
|
424
|
+
while ((match = selectorRegex.exec(line)) !== null) {
|
|
425
|
+
info.selectors.push({ selector: match[1], line: lineNum });
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Find events
|
|
429
|
+
const eventRegex = /\.(on|click|submit|change|focus|blur|keyup|keydown|mouseover|mouseout)\s*\(/g;
|
|
430
|
+
while ((match = eventRegex.exec(line)) !== null) {
|
|
431
|
+
info.events.push({ event: match[1], line: lineNum });
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Find AJAX
|
|
435
|
+
const ajaxRegex = /\$\.(ajax|get|post|getJSON)\s*\(/g;
|
|
436
|
+
while ((match = ajaxRegex.exec(line)) !== null) {
|
|
437
|
+
info.ajax.push({ method: match[1], line: lineNum });
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Find deprecated methods
|
|
441
|
+
for (const [deprecated, suggestion] of Object.entries(DEPRECATED_JQUERY)) {
|
|
442
|
+
if (line.includes(deprecated)) {
|
|
443
|
+
info.deprecated.push({ method: deprecated, line: lineNum, suggestion });
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Count DOM manipulations
|
|
448
|
+
const domMethods = [".html(", ".text(", ".append(", ".prepend(", ".after(", ".before(", ".remove(", ".empty(", ".attr(", ".css(", ".addClass(", ".removeClass("];
|
|
449
|
+
for (const method of domMethods) {
|
|
450
|
+
if (line.includes(method)) {
|
|
451
|
+
info.domManipulations++;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Issues
|
|
457
|
+
if (info.deprecated.length > 0) {
|
|
458
|
+
info.issues.push(`${info.deprecated.length} deprecated jQuery methods found`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Check for inefficient selectors
|
|
462
|
+
const inefficientSelectors = info.selectors.filter((s) =>
|
|
463
|
+
s.selector.startsWith("*") ||
|
|
464
|
+
s.selector.includes(" > * ") ||
|
|
465
|
+
/^\w+\s+\w+\s+\w+/.test(s.selector) // Deep nesting
|
|
466
|
+
);
|
|
467
|
+
if (inefficientSelectors.length > 0) {
|
|
468
|
+
info.issues.push(`${inefficientSelectors.length} potentially inefficient selectors`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Check for too many DOM manipulations
|
|
472
|
+
if (info.domManipulations > 20) {
|
|
473
|
+
info.issues.push("High DOM manipulation count - consider batching or virtual DOM");
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return info;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// React Check Tool
|
|
480
|
+
export const reactCheckTool: Tool = {
|
|
481
|
+
name: "react_check",
|
|
482
|
+
description: "Analyze React components (React λΆμ). Finds components, hooks usage, potential issues. Use when user asks: 'react check', 'react λΆμ', 'component λΆμ'.",
|
|
483
|
+
parameters: {
|
|
484
|
+
type: "object",
|
|
485
|
+
required: ["pattern"],
|
|
486
|
+
properties: {
|
|
487
|
+
pattern: {
|
|
488
|
+
type: "string",
|
|
489
|
+
description: "Glob pattern (e.g., src/**/*.{tsx,jsx})",
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
handler: async (args): Promise<ToolResult> => {
|
|
494
|
+
try {
|
|
495
|
+
const { glob } = await import("glob");
|
|
496
|
+
const pattern = args.pattern as string;
|
|
497
|
+
|
|
498
|
+
const files = await glob(pattern, {
|
|
499
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**"],
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
const allComponents: ReactComponentInfo[] = [];
|
|
503
|
+
|
|
504
|
+
for (const file of files) {
|
|
505
|
+
if (!/\.(tsx|jsx)$/.test(file)) continue;
|
|
506
|
+
try {
|
|
507
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
508
|
+
const components = analyzeReactFile(content, file);
|
|
509
|
+
allComponents.push(...components);
|
|
510
|
+
} catch {
|
|
511
|
+
// Skip unparseable files
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const lines: string[] = [];
|
|
516
|
+
lines.push("=== React μ»΄ν¬λνΈ λΆμ ===");
|
|
517
|
+
lines.push("");
|
|
518
|
+
lines.push(`π μ΄ ${allComponents.length}κ° μ»΄ν¬λνΈ λ°κ²¬`);
|
|
519
|
+
lines.push("");
|
|
520
|
+
|
|
521
|
+
// Group by type
|
|
522
|
+
const byType = {
|
|
523
|
+
function: allComponents.filter((c) => c.type === "function"),
|
|
524
|
+
arrow: allComponents.filter((c) => c.type === "arrow"),
|
|
525
|
+
class: allComponents.filter((c) => c.type === "class"),
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
lines.push(` Function: ${byType.function.length}κ°`);
|
|
529
|
+
lines.push(` Arrow: ${byType.arrow.length}κ°`);
|
|
530
|
+
lines.push(` Class: ${byType.class.length}κ° ${byType.class.length > 0 ? "(λ κ±°μ)" : ""}`);
|
|
531
|
+
lines.push("");
|
|
532
|
+
|
|
533
|
+
// List components with issues
|
|
534
|
+
const withIssues = allComponents.filter((c) => c.issues.length > 0);
|
|
535
|
+
if (withIssues.length > 0) {
|
|
536
|
+
lines.push("β οΈ μ΄μ λ°κ²¬:");
|
|
537
|
+
for (const comp of withIssues) {
|
|
538
|
+
lines.push(` ${comp.name} (${path.relative(process.cwd(), comp.filepath)}:${comp.line})`);
|
|
539
|
+
for (const issue of comp.issues) {
|
|
540
|
+
lines.push(` - ${issue}`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
lines.push("");
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Hooks usage summary
|
|
547
|
+
const hooksUsage: Record<string, number> = {};
|
|
548
|
+
for (const comp of allComponents) {
|
|
549
|
+
for (const hook of comp.hooks) {
|
|
550
|
+
const hookName = hook.split(" ")[0];
|
|
551
|
+
hooksUsage[hookName] = (hooksUsage[hookName] || 0) + 1;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (Object.keys(hooksUsage).length > 0) {
|
|
556
|
+
lines.push("πͺ Hooks μ¬μ©:");
|
|
557
|
+
for (const [hook, count] of Object.entries(hooksUsage).sort((a, b) => b[1] - a[1])) {
|
|
558
|
+
lines.push(` ${hook}: ${count}ν`);
|
|
559
|
+
}
|
|
560
|
+
lines.push("");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Memoized components
|
|
564
|
+
const memoized = allComponents.filter((c) => c.memoized);
|
|
565
|
+
if (memoized.length > 0) {
|
|
566
|
+
lines.push(`β
Memoized μ»΄ν¬λνΈ: ${memoized.length}κ°`);
|
|
567
|
+
for (const comp of memoized) {
|
|
568
|
+
lines.push(` ${comp.name}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return { success: true, content: lines.join("\n") };
|
|
573
|
+
} catch (error) {
|
|
574
|
+
return { success: false, content: "", error: String(error) };
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
// Vue Check Tool
|
|
580
|
+
export const vueCheckTool: Tool = {
|
|
581
|
+
name: "vue_check",
|
|
582
|
+
description: "Analyze Vue components (Vue λΆμ). Finds components, composition API usage, issues. Use when user asks: 'vue check', 'vue λΆμ', 'vue component'.",
|
|
583
|
+
parameters: {
|
|
584
|
+
type: "object",
|
|
585
|
+
required: ["pattern"],
|
|
586
|
+
properties: {
|
|
587
|
+
pattern: {
|
|
588
|
+
type: "string",
|
|
589
|
+
description: "Glob pattern (e.g., src/**/*.vue)",
|
|
590
|
+
},
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
handler: async (args): Promise<ToolResult> => {
|
|
594
|
+
try {
|
|
595
|
+
const { glob } = await import("glob");
|
|
596
|
+
const pattern = args.pattern as string;
|
|
597
|
+
|
|
598
|
+
const files = await glob(pattern, {
|
|
599
|
+
ignore: ["**/node_modules/**", "**/dist/**"],
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
const allComponents: VueComponentInfo[] = [];
|
|
603
|
+
|
|
604
|
+
for (const file of files) {
|
|
605
|
+
if (!file.endsWith(".vue")) continue;
|
|
606
|
+
try {
|
|
607
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
608
|
+
const component = analyzeVueFile(content, file);
|
|
609
|
+
allComponents.push(component);
|
|
610
|
+
} catch {
|
|
611
|
+
// Skip unparseable files
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const lines: string[] = [];
|
|
616
|
+
lines.push("=== Vue μ»΄ν¬λνΈ λΆμ ===");
|
|
617
|
+
lines.push("");
|
|
618
|
+
lines.push(`π μ΄ ${allComponents.length}κ° μ»΄ν¬λνΈ λ°κ²¬`);
|
|
619
|
+
lines.push("");
|
|
620
|
+
|
|
621
|
+
// Group by type
|
|
622
|
+
const byType = {
|
|
623
|
+
"script-setup": allComponents.filter((c) => c.type === "script-setup"),
|
|
624
|
+
composition: allComponents.filter((c) => c.type === "composition"),
|
|
625
|
+
options: allComponents.filter((c) => c.type === "options"),
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
lines.push(` Script Setup: ${byType["script-setup"].length}κ° (κΆμ₯)`);
|
|
629
|
+
lines.push(` Composition API: ${byType.composition.length}κ°`);
|
|
630
|
+
lines.push(` Options API: ${byType.options.length}κ°`);
|
|
631
|
+
lines.push("");
|
|
632
|
+
|
|
633
|
+
// List components with issues
|
|
634
|
+
const withIssues = allComponents.filter((c) => c.issues.length > 0);
|
|
635
|
+
if (withIssues.length > 0) {
|
|
636
|
+
lines.push("β οΈ μ΄μ λ°κ²¬:");
|
|
637
|
+
for (const comp of withIssues) {
|
|
638
|
+
lines.push(` ${comp.name}`);
|
|
639
|
+
for (const issue of comp.issues) {
|
|
640
|
+
lines.push(` - ${issue}`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
lines.push("");
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Composables usage
|
|
647
|
+
const composables: Record<string, number> = {};
|
|
648
|
+
for (const comp of allComponents) {
|
|
649
|
+
for (const c of comp.composables) {
|
|
650
|
+
composables[c] = (composables[c] || 0) + 1;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (Object.keys(composables).length > 0) {
|
|
655
|
+
lines.push("π§ Composables μ¬μ©:");
|
|
656
|
+
for (const [name, count] of Object.entries(composables).sort((a, b) => b[1] - a[1]).slice(0, 10)) {
|
|
657
|
+
lines.push(` ${name}: ${count}ν`);
|
|
658
|
+
}
|
|
659
|
+
lines.push("");
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Lifecycle hooks usage
|
|
663
|
+
const lifecycleCount: Record<string, number> = {};
|
|
664
|
+
for (const comp of allComponents) {
|
|
665
|
+
for (const hook of comp.lifecycle) {
|
|
666
|
+
lifecycleCount[hook] = (lifecycleCount[hook] || 0) + 1;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (Object.keys(lifecycleCount).length > 0) {
|
|
671
|
+
lines.push("π Lifecycle Hooks:");
|
|
672
|
+
for (const [hook, count] of Object.entries(lifecycleCount).sort((a, b) => b[1] - a[1])) {
|
|
673
|
+
lines.push(` ${hook}: ${count}ν`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return { success: true, content: lines.join("\n") };
|
|
678
|
+
} catch (error) {
|
|
679
|
+
return { success: false, content: "", error: String(error) };
|
|
680
|
+
}
|
|
681
|
+
},
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
// jQuery Check Tool
|
|
685
|
+
export const jqueryCheckTool: Tool = {
|
|
686
|
+
name: "jquery_check",
|
|
687
|
+
description: "Analyze jQuery usage (jQuery λΆμ). Finds selectors, events, deprecated methods. Use when user asks: 'jquery check', 'jquery λΆμ', 'jquery deprecated'.",
|
|
688
|
+
parameters: {
|
|
689
|
+
type: "object",
|
|
690
|
+
required: ["pattern"],
|
|
691
|
+
properties: {
|
|
692
|
+
pattern: {
|
|
693
|
+
type: "string",
|
|
694
|
+
description: "Glob pattern (e.g., src/**/*.js)",
|
|
695
|
+
},
|
|
696
|
+
},
|
|
697
|
+
},
|
|
698
|
+
handler: async (args): Promise<ToolResult> => {
|
|
699
|
+
try {
|
|
700
|
+
const { glob } = await import("glob");
|
|
701
|
+
const pattern = args.pattern as string;
|
|
702
|
+
|
|
703
|
+
const files = await glob(pattern, {
|
|
704
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/*.min.js"],
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
const allUsages: JQueryUsageInfo[] = [];
|
|
708
|
+
|
|
709
|
+
for (const file of files) {
|
|
710
|
+
if (!/\.js$/.test(file)) continue;
|
|
711
|
+
try {
|
|
712
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
713
|
+
// Check if file uses jQuery
|
|
714
|
+
if (content.includes("$(" ) || content.includes("jQuery(")) {
|
|
715
|
+
const usage = analyzeJQueryFile(content, file);
|
|
716
|
+
allUsages.push(usage);
|
|
717
|
+
}
|
|
718
|
+
} catch {
|
|
719
|
+
// Skip unreadable files
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (allUsages.length === 0) {
|
|
724
|
+
return { success: true, content: "jQuery μ¬μ©μ μ°Ύμ§ λͺ»νμ΅λλ€." };
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const lines: string[] = [];
|
|
728
|
+
lines.push("=== jQuery μ¬μ© λΆμ ===");
|
|
729
|
+
lines.push("");
|
|
730
|
+
lines.push(`π ${allUsages.length}κ° νμΌμμ jQuery μ¬μ© λ°κ²¬`);
|
|
731
|
+
lines.push("");
|
|
732
|
+
|
|
733
|
+
// Total stats
|
|
734
|
+
const totalSelectors = allUsages.reduce((sum, u) => sum + u.selectors.length, 0);
|
|
735
|
+
const totalEvents = allUsages.reduce((sum, u) => sum + u.events.length, 0);
|
|
736
|
+
const totalAjax = allUsages.reduce((sum, u) => sum + u.ajax.length, 0);
|
|
737
|
+
const totalDeprecated = allUsages.reduce((sum, u) => sum + u.deprecated.length, 0);
|
|
738
|
+
|
|
739
|
+
lines.push("π ν΅κ³:");
|
|
740
|
+
lines.push(` μ
λ ν°: ${totalSelectors}κ°`);
|
|
741
|
+
lines.push(` μ΄λ²€νΈ λ°μΈλ©: ${totalEvents}κ°`);
|
|
742
|
+
lines.push(` AJAX νΈμΆ: ${totalAjax}κ°`);
|
|
743
|
+
lines.push(` Deprecated λ©μλ: ${totalDeprecated}κ°`);
|
|
744
|
+
lines.push("");
|
|
745
|
+
|
|
746
|
+
// Deprecated methods
|
|
747
|
+
if (totalDeprecated > 0) {
|
|
748
|
+
lines.push("β οΈ Deprecated λ©μλ:");
|
|
749
|
+
for (const usage of allUsages) {
|
|
750
|
+
for (const dep of usage.deprecated) {
|
|
751
|
+
lines.push(` ${path.relative(process.cwd(), usage.filepath)}:${dep.line}`);
|
|
752
|
+
lines.push(` ${dep.method} β ${dep.suggestion}`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
lines.push("");
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Files with issues
|
|
759
|
+
const withIssues = allUsages.filter((u) => u.issues.length > 0);
|
|
760
|
+
if (withIssues.length > 0) {
|
|
761
|
+
lines.push("π νμΌλ³ μ΄μ:");
|
|
762
|
+
for (const usage of withIssues) {
|
|
763
|
+
lines.push(` ${path.relative(process.cwd(), usage.filepath)}`);
|
|
764
|
+
for (const issue of usage.issues) {
|
|
765
|
+
lines.push(` - ${issue}`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
lines.push("");
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Common selectors
|
|
772
|
+
const selectorCounts: Record<string, number> = {};
|
|
773
|
+
for (const usage of allUsages) {
|
|
774
|
+
for (const sel of usage.selectors) {
|
|
775
|
+
selectorCounts[sel.selector] = (selectorCounts[sel.selector] || 0) + 1;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const commonSelectors = Object.entries(selectorCounts)
|
|
780
|
+
.sort((a, b) => b[1] - a[1])
|
|
781
|
+
.slice(0, 10);
|
|
782
|
+
|
|
783
|
+
if (commonSelectors.length > 0) {
|
|
784
|
+
lines.push("π― μμ£Ό μ¬μ©λλ μ
λ ν°:");
|
|
785
|
+
for (const [selector, count] of commonSelectors) {
|
|
786
|
+
lines.push(` "${selector}": ${count}ν`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return { success: true, content: lines.join("\n") };
|
|
791
|
+
} catch (error) {
|
|
792
|
+
return { success: false, content: "", error: String(error) };
|
|
793
|
+
}
|
|
794
|
+
},
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
// Export all frontend tools
|
|
798
|
+
export const frontendTools: Tool[] = [
|
|
799
|
+
reactCheckTool,
|
|
800
|
+
vueCheckTool,
|
|
801
|
+
jqueryCheckTool,
|
|
802
|
+
];
|