closer-code 1.0.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/.env.example +83 -0
- package/API_GUIDE.md +1411 -0
- package/AUTO_MKDIR_IMPROVEMENT.md +354 -0
- package/CLAUDE.md +55 -0
- package/CTRL_C_EXPERIMENT.md +90 -0
- package/PROJECT_CLEANUP_SUMMARY.md +121 -0
- package/README.md +686 -0
- package/cloco.md +51 -0
- package/config.example.json +116 -0
- package/dist/bash-runner.js +128 -0
- package/dist/batch-cli.js +20736 -0
- package/dist/closer-cli.js +21190 -0
- package/dist/index.js +31228 -0
- package/docs/EXPORT_COMMAND.md +152 -0
- package/docs/FILE_NAMING_IMPROVEMENT.md +168 -0
- package/docs/GLOBAL_CONFIG.md +128 -0
- package/docs/LONG_MESSAGE_DISPLAY_FIX.md +202 -0
- package/docs/PROJECT_HISTORY_ISOLATION.md +315 -0
- package/docs/QUICK_START_HISTORY.md +207 -0
- package/docs/TASK_PROGRESS_FEATURE.md +190 -0
- package/docs/THINKING_CONTENT_RESEARCH.md +267 -0
- package/docs/THINKING_FEATURE.md +187 -0
- package/docs/THINKING_IMPROVEMENT_COMPARISON.md +193 -0
- package/docs/THINKING_OPTIMIZATION_SUMMARY.md +242 -0
- package/docs/UI_IMPROVEMENTS_2025-01-18.md +256 -0
- package/docs/WHY_THINKING_SHORT.md +201 -0
- package/package.json +49 -0
- package/scenarios/README.md +234 -0
- package/scenarios/run-all-scenarios.js +342 -0
- package/scenarios/scenario1-batch-converter.js +247 -0
- package/scenarios/scenario2-code-analyzer.js +375 -0
- package/scenarios/scenario3-doc-generator.js +371 -0
- package/scenarios/scenario4-log-analyzer.js +496 -0
- package/scenarios/scenario5-tdd-helper.js +681 -0
- package/src/ai-client-legacy.js +171 -0
- package/src/ai-client.js +221 -0
- package/src/bash-runner.js +148 -0
- package/src/batch-cli.js +327 -0
- package/src/cli.jsx +166 -0
- package/src/closer-cli.jsx +1103 -0
- package/src/closer-cli.jsx.backup +948 -0
- package/src/commands/batch.js +62 -0
- package/src/commands/chat.js +10 -0
- package/src/commands/config.js +154 -0
- package/src/commands/help.js +76 -0
- package/src/commands/history.js +192 -0
- package/src/commands/setup.js +17 -0
- package/src/commands/upgrade.js +101 -0
- package/src/commands/workflow-tests.js +125 -0
- package/src/config.js +343 -0
- package/src/conversation.js +962 -0
- package/src/git-helper.js +349 -0
- package/src/index.js +88 -0
- package/src/logger.js +347 -0
- package/src/plan.js +193 -0
- package/src/planner.js +397 -0
- package/src/search.js +195 -0
- package/src/setup.js +147 -0
- package/src/shortcuts.js +269 -0
- package/src/snippets.js +430 -0
- package/src/test-modules.js +118 -0
- package/src/tools.js +398 -0
- package/src/utils/cli.js +124 -0
- package/src/utils/validator.js +184 -0
- package/src/utils/version.js +33 -0
- package/src/utils/workflow-test.js +271 -0
- package/src/utils/workflow.js +268 -0
- package/test/demo-file-naming.js +92 -0
- package/test/demo-thinking.js +124 -0
- package/test/final-verification-report.md +303 -0
- package/test/research-thinking.js +130 -0
- package/test/test-auto-mkdir.js +123 -0
- package/test/test-e2e-empty-dir.md +108 -0
- package/test/test-export-logic.js +119 -0
- package/test/test-global-cloco.js +126 -0
- package/test/test-history-isolation.js +291 -0
- package/test/test-improved-thinking.js +43 -0
- package/test/test-long-message.js +65 -0
- package/test/test-plan-functionality.js +95 -0
- package/test/test-real-scenario.js +216 -0
- package/test/test-thinking-display.js +65 -0
- package/test/ui-verification-test.js +203 -0
- package/test/verify-history-isolation.sh +71 -0
- package/test/verify-thinking.js +339 -0
- package/test/workflows/empty-dir-creation.md +51 -0
- package/test/workflows/inventor/ascii-teacup.js +199 -0
- package/test/workflows/inventor/ascii-teacup.mjs +199 -0
- package/test/workflows/inventor/ascii_apple.hs +84 -0
- package/test/workflows/inventor/ascii_apple.py +91 -0
- package/test/workflows/inventor/cloco.md +3 -0
- package/test/workflows/longtalk/cloco.md +19 -0
- package/test/workflows/longtalk/emoji_500.txt +63 -0
- package/test/workflows/longtalk/emoji_list.txt +20 -0
- package/test/workflows/programmer/adder.md +33 -0
- package/test/workflows/programmer/expect.md +2 -0
- package/test/workflows/programmer/prompt.md +3 -0
- package/test/workflows/test-empty-dir-creation.js +113 -0
- package/test-ctrl-c.jsx +126 -0
- package/test-manual-file-creation.js +151 -0
- package/winfix.md +3 -0
package/src/planner.js
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 任务规划和执行引擎
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createAIClient } from './ai-client.js';
|
|
6
|
+
import { ToolExecutor } from './tools.js';
|
|
7
|
+
import { loadMemory, saveMemory } from './config.js';
|
|
8
|
+
|
|
9
|
+
// 任务状态
|
|
10
|
+
export const TaskStatus = {
|
|
11
|
+
PENDING: 'pending',
|
|
12
|
+
IN_PROGRESS: 'in_progress',
|
|
13
|
+
COMPLETED: 'completed',
|
|
14
|
+
FAILED: 'failed',
|
|
15
|
+
BLOCKED: 'blocked'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// 步骤状态
|
|
19
|
+
export const StepStatus = {
|
|
20
|
+
PENDING: 'pending',
|
|
21
|
+
IN_PROGRESS: 'in_progress',
|
|
22
|
+
COMPLETED: 'completed',
|
|
23
|
+
FAILED: 'failed',
|
|
24
|
+
SKIPPED: 'skipped'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 任务计划
|
|
29
|
+
*/
|
|
30
|
+
export class TaskPlan {
|
|
31
|
+
constructor(description, steps = []) {
|
|
32
|
+
this.id = Date.now().toString(36) + Math.random().toString(36).substr(2);
|
|
33
|
+
this.description = description;
|
|
34
|
+
this.steps = steps;
|
|
35
|
+
this.status = TaskStatus.PENDING;
|
|
36
|
+
this.currentStep = 0;
|
|
37
|
+
this.createdAt = new Date().toISOString();
|
|
38
|
+
this.startedAt = null;
|
|
39
|
+
this.completedAt = null;
|
|
40
|
+
this.error = null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
start() {
|
|
44
|
+
this.status = TaskStatus.IN_PROGRESS;
|
|
45
|
+
this.startedAt = new Date().toISOString();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
complete() {
|
|
49
|
+
this.status = TaskStatus.COMPLETED;
|
|
50
|
+
this.completedAt = new Date().toISOString();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fail(error) {
|
|
54
|
+
this.status = TaskStatus.FAILED;
|
|
55
|
+
this.error = error;
|
|
56
|
+
this.completedAt = new Date().toISOString();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getCurrentStep() {
|
|
60
|
+
return this.steps[this.currentStep];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
advanceStep() {
|
|
64
|
+
this.currentStep++;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getProgress() {
|
|
68
|
+
const completed = this.steps.filter(s => s.status === StepStatus.COMPLETED).length;
|
|
69
|
+
return {
|
|
70
|
+
completed,
|
|
71
|
+
total: this.steps.length,
|
|
72
|
+
percentage: this.steps.length > 0 ? (completed / this.steps.length) * 100 : 0
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 任务规划器
|
|
79
|
+
*/
|
|
80
|
+
export class TaskPlanner {
|
|
81
|
+
constructor(config) {
|
|
82
|
+
this.config = config;
|
|
83
|
+
this.aiClient = createAIClient(config);
|
|
84
|
+
this.toolExecutor = new ToolExecutor(config);
|
|
85
|
+
this.memory = loadMemory();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 为用户请求创建任务计划
|
|
90
|
+
*/
|
|
91
|
+
async planTask(userRequest, context = {}) {
|
|
92
|
+
const systemPrompt = `You are an expert software developer and task planner.
|
|
93
|
+
When given a user request, break it down into clear, actionable steps.
|
|
94
|
+
|
|
95
|
+
Guidelines:
|
|
96
|
+
- Break complex tasks into smaller, verifiable steps
|
|
97
|
+
- Each step should have a clear description and expected outcome
|
|
98
|
+
- Consider dependencies between steps
|
|
99
|
+
- Include validation steps to verify success
|
|
100
|
+
- Estimate which steps might need human confirmation
|
|
101
|
+
|
|
102
|
+
Output format: Return a JSON object with:
|
|
103
|
+
{
|
|
104
|
+
"description": "Brief description of the overall task",
|
|
105
|
+
"steps": [
|
|
106
|
+
{
|
|
107
|
+
"description": "What this step does",
|
|
108
|
+
"action": "tool_name",
|
|
109
|
+
"input": { ...tool_input },
|
|
110
|
+
"expectedResult": "What success looks like",
|
|
111
|
+
"requiresConfirmation": false,
|
|
112
|
+
"dependencies": []
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
}`;
|
|
116
|
+
|
|
117
|
+
const messages = [
|
|
118
|
+
{
|
|
119
|
+
role: 'user',
|
|
120
|
+
content: `Create a task plan for: ${userRequest}
|
|
121
|
+
|
|
122
|
+
${context ? `Additional context:\n${context}` : ''}
|
|
123
|
+
|
|
124
|
+
Consider the available tools: bash, readFile, writeFile, editFile, searchFiles, searchCode, listFiles, runTests
|
|
125
|
+
|
|
126
|
+
Respond with only the JSON plan, no additional text.`
|
|
127
|
+
}
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const response = await this.aiClient.chat(messages, { systemPrompt });
|
|
132
|
+
const text = response.content.find(c => c.type === 'text')?.text || '{}';
|
|
133
|
+
const planData = JSON.parse(text);
|
|
134
|
+
|
|
135
|
+
return new TaskPlan(planData.description, planData.steps);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error('Failed to create plan:', error.message);
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 执行任务计划
|
|
144
|
+
*/
|
|
145
|
+
async executePlan(plan, onProgress = null) {
|
|
146
|
+
plan.start();
|
|
147
|
+
|
|
148
|
+
if (onProgress) {
|
|
149
|
+
onProgress({ type: 'plan_started', plan });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (let i = 0; i < plan.steps.length; i++) {
|
|
153
|
+
const step = plan.steps[i];
|
|
154
|
+
|
|
155
|
+
// 检查依赖
|
|
156
|
+
if (step.dependencies) {
|
|
157
|
+
for (const depIndex of step.dependencies) {
|
|
158
|
+
const depStep = plan.steps[depIndex];
|
|
159
|
+
if (depStep.status !== StepStatus.COMPLETED) {
|
|
160
|
+
step.status = StepStatus.BLOCKED;
|
|
161
|
+
plan.status = TaskStatus.BLOCKED;
|
|
162
|
+
if (onProgress) {
|
|
163
|
+
onProgress({ type: 'step_blocked', step, dependency: depIndex });
|
|
164
|
+
}
|
|
165
|
+
return plan;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 执行步骤
|
|
171
|
+
step.status = StepStatus.IN_PROGRESS;
|
|
172
|
+
if (onProgress) {
|
|
173
|
+
onProgress({ type: 'step_started', step, progress: plan.getProgress() });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const result = await this.executeStep(step);
|
|
178
|
+
|
|
179
|
+
if (result.success) {
|
|
180
|
+
step.status = StepStatus.COMPLETED;
|
|
181
|
+
step.result = result.data;
|
|
182
|
+
|
|
183
|
+
if (onProgress) {
|
|
184
|
+
onProgress({ type: 'step_completed', step, result, progress: plan.getProgress() });
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
step.status = StepStatus.FAILED;
|
|
188
|
+
step.error = result.error;
|
|
189
|
+
|
|
190
|
+
if (onProgress) {
|
|
191
|
+
onProgress({ type: 'step_failed', step, error: result.error });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 根据配置决定是否继续
|
|
195
|
+
if (this.config.behavior.maxRetries > 0) {
|
|
196
|
+
// 可以在这里添加重试逻辑
|
|
197
|
+
}
|
|
198
|
+
plan.fail(result.error);
|
|
199
|
+
return plan;
|
|
200
|
+
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
step.status = StepStatus.FAILED;
|
|
203
|
+
step.error = error.message;
|
|
204
|
+
|
|
205
|
+
if (onProgress) {
|
|
206
|
+
onProgress({ type: 'step_failed', step, error: error.message });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
plan.fail(error.message);
|
|
210
|
+
return plan;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
plan.advanceStep();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
plan.complete();
|
|
217
|
+
|
|
218
|
+
if (onProgress) {
|
|
219
|
+
onProgress({ type: 'plan_completed', plan });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 保存到记忆
|
|
223
|
+
this.saveToMemory(plan);
|
|
224
|
+
|
|
225
|
+
return plan;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 执行单个步骤
|
|
230
|
+
*/
|
|
231
|
+
async executeStep(step) {
|
|
232
|
+
if (step.action === 'plan') {
|
|
233
|
+
// 子计划,递归执行
|
|
234
|
+
const subPlan = await this.planTask(step.description, step.input?.context);
|
|
235
|
+
return { success: true, data: { plan: subPlan } };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!this.toolExecutor.isToolEnabled(step.action)) {
|
|
239
|
+
return { success: false, error: `Tool '${step.action}' is not enabled` };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return await this.toolExecutor.execute(step.action, step.input || {});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 保存计划到记忆
|
|
247
|
+
*/
|
|
248
|
+
saveToMemory(plan) {
|
|
249
|
+
if (!this.memory.projects) {
|
|
250
|
+
this.memory.projects = {};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const projectKey = this.config.behavior.workingDir || 'default';
|
|
254
|
+
if (!this.memory.projects[projectKey]) {
|
|
255
|
+
this.memory.projects[projectKey] = {
|
|
256
|
+
plans: [],
|
|
257
|
+
patterns: {},
|
|
258
|
+
lessons: []
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
this.memory.projects[projectKey].plans.push({
|
|
263
|
+
id: plan.id,
|
|
264
|
+
description: plan.description,
|
|
265
|
+
status: plan.status,
|
|
266
|
+
stepCount: plan.steps.length,
|
|
267
|
+
createdAt: plan.createdAt
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
saveMemory(this.memory);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 从项目中学习模式
|
|
275
|
+
*/
|
|
276
|
+
async learnFromProject() {
|
|
277
|
+
const systemPrompt = `Analyze the codebase and identify patterns, conventions, and best practices.
|
|
278
|
+
|
|
279
|
+
Focus on:
|
|
280
|
+
- Project structure and organization
|
|
281
|
+
- Naming conventions
|
|
282
|
+
- Common patterns used
|
|
283
|
+
- Dependencies and their purposes
|
|
284
|
+
- Testing approach
|
|
285
|
+
- Build and deployment setup
|
|
286
|
+
|
|
287
|
+
Respond with a JSON object describing the patterns found.`;
|
|
288
|
+
|
|
289
|
+
// 读取关键文件
|
|
290
|
+
const files = ['package.json', 'README.md', '.gitignore', 'tsconfig.json'];
|
|
291
|
+
|
|
292
|
+
const context = [];
|
|
293
|
+
for (const file of files) {
|
|
294
|
+
try {
|
|
295
|
+
const result = await this.toolExecutor.readFile({ filePath: file });
|
|
296
|
+
if (result.success) {
|
|
297
|
+
context.push(`=== ${file} ===\n${result.data.content}`);
|
|
298
|
+
}
|
|
299
|
+
} catch (error) {
|
|
300
|
+
// 文件可能不存在
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const messages = [
|
|
305
|
+
{
|
|
306
|
+
role: 'user',
|
|
307
|
+
content: `Analyze this project:\n\n${context.join('\n\n')}`
|
|
308
|
+
}
|
|
309
|
+
];
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const response = await this.aiClient.chat(messages, { systemPrompt });
|
|
313
|
+
const text = response.content.find(c => c.type === 'text')?.text || '{}';
|
|
314
|
+
const patterns = JSON.parse(text);
|
|
315
|
+
|
|
316
|
+
const projectKey = this.config.behavior.workingDir || 'default';
|
|
317
|
+
if (!this.memory.projects[projectKey]) {
|
|
318
|
+
this.memory.projects[projectKey] = { plans: [], patterns: {}, lessons: [] };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
this.memory.projects[projectKey].patterns = patterns;
|
|
322
|
+
saveMemory(this.memory);
|
|
323
|
+
|
|
324
|
+
return patterns;
|
|
325
|
+
} catch (error) {
|
|
326
|
+
console.error('Failed to learn from project:', error.message);
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* 问题诊断器
|
|
334
|
+
*/
|
|
335
|
+
export class ProblemDiagnoser {
|
|
336
|
+
constructor(config) {
|
|
337
|
+
this.config = config;
|
|
338
|
+
this.aiClient = createAIClient(config);
|
|
339
|
+
this.toolExecutor = new ToolExecutor(config);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* 诊断错误
|
|
344
|
+
*/
|
|
345
|
+
async diagnose(error, context = {}) {
|
|
346
|
+
const systemPrompt = `You are an expert debugger. Analyze errors and provide:
|
|
347
|
+
1. Root cause analysis
|
|
348
|
+
2. Specific solutions
|
|
349
|
+
3. Prevention strategies
|
|
350
|
+
4. Related code to check
|
|
351
|
+
|
|
352
|
+
Be concise and actionable.`;
|
|
353
|
+
|
|
354
|
+
// 收集上下文信息
|
|
355
|
+
const contextInfo = await this.gatherContext(context);
|
|
356
|
+
|
|
357
|
+
const messages = [
|
|
358
|
+
{
|
|
359
|
+
role: 'user',
|
|
360
|
+
content: `Diagnose this error:
|
|
361
|
+
|
|
362
|
+
${error}
|
|
363
|
+
|
|
364
|
+
${contextInfo ? `\nContext:\n${contextInfo}` : ''}`
|
|
365
|
+
}
|
|
366
|
+
];
|
|
367
|
+
|
|
368
|
+
const response = await this.aiClient.chat(messages, { systemPrompt });
|
|
369
|
+
return response.content.find(c => c.type === 'text')?.text;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* 收集诊断上下文
|
|
374
|
+
*/
|
|
375
|
+
async gatherContext(context) {
|
|
376
|
+
const info = [];
|
|
377
|
+
|
|
378
|
+
// 添加环境信息
|
|
379
|
+
info.push(`Node.js: ${process.version}`);
|
|
380
|
+
info.push(`Platform: ${process.platform}`);
|
|
381
|
+
info.push(`CWD: ${process.cwd()}`);
|
|
382
|
+
|
|
383
|
+
// 添加项目信息
|
|
384
|
+
try {
|
|
385
|
+
const packageResult = await this.toolExecutor.readFile({ filePath: 'package.json' });
|
|
386
|
+
if (packageResult.success) {
|
|
387
|
+
const pkg = JSON.parse(packageResult.data.content);
|
|
388
|
+
info.push(`Project: ${pkg.name} v${pkg.version}`);
|
|
389
|
+
info.push(`Dependencies: ${Object.keys(pkg.dependencies || {}).length}`);
|
|
390
|
+
}
|
|
391
|
+
} catch (error) {
|
|
392
|
+
// 忽略
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return info.join('\n');
|
|
396
|
+
}
|
|
397
|
+
}
|
package/src/search.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 代码搜索模块
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readdir } from 'fs/promises';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { glob } from 'glob';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 搜索代码中的文本模式
|
|
11
|
+
*/
|
|
12
|
+
export async function searchCode(pattern, options = {}) {
|
|
13
|
+
const {
|
|
14
|
+
searchPath = '.',
|
|
15
|
+
type = null,
|
|
16
|
+
caseInsensitive = false,
|
|
17
|
+
maxResults = 100
|
|
18
|
+
} = options;
|
|
19
|
+
|
|
20
|
+
const flags = caseInsensitive ? 'gi' : 'g';
|
|
21
|
+
const regex = new RegExp(pattern, flags);
|
|
22
|
+
const results = [];
|
|
23
|
+
|
|
24
|
+
// 获取要搜索的文件
|
|
25
|
+
let globPattern = '**/*';
|
|
26
|
+
if (type) {
|
|
27
|
+
globPattern = `**/*.${type}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const files = await glob(globPattern, {
|
|
31
|
+
cwd: searchPath,
|
|
32
|
+
ignore: ['**/node_modules/**', '**/dist/**', '**/.git/**', '**/coverage/**']
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
for (const file of files.slice(0, maxResults)) {
|
|
36
|
+
const fullPath = join(searchPath, file);
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const { readFile } = await import('fs/promises');
|
|
40
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
41
|
+
const lines = content.split('\n');
|
|
42
|
+
|
|
43
|
+
let fileHasMatch = false;
|
|
44
|
+
const fileResults = [];
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < lines.length; i++) {
|
|
47
|
+
const line = lines[i];
|
|
48
|
+
const match = regex.exec(line);
|
|
49
|
+
if (match) {
|
|
50
|
+
fileHasMatch = true;
|
|
51
|
+
fileResults.push({
|
|
52
|
+
lineNumber: i + 1,
|
|
53
|
+
line: line.trim(),
|
|
54
|
+
match: match[0],
|
|
55
|
+
index: match.index
|
|
56
|
+
});
|
|
57
|
+
regex.lastIndex = 0; // 重置以查找同一行中的其他匹配
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (fileHasMatch) {
|
|
62
|
+
results.push({
|
|
63
|
+
file,
|
|
64
|
+
path: fullPath,
|
|
65
|
+
matches: fileResults
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
// 忽略无法读取的文件(二进制文件等)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
pattern,
|
|
75
|
+
results,
|
|
76
|
+
totalFiles: results.length,
|
|
77
|
+
totalMatches: results.reduce((sum, r) => sum + r.matches.length, 0)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 搜索函数定义
|
|
83
|
+
*/
|
|
84
|
+
export async function searchFunction(pattern, options = {}) {
|
|
85
|
+
const { searchPath = '.', type = 'js' } = options;
|
|
86
|
+
|
|
87
|
+
// 函数定义的正则模式
|
|
88
|
+
const functionPatterns = [
|
|
89
|
+
// JavaScript/TypeScript
|
|
90
|
+
/function\s+(\w+)\s*\(/g,
|
|
91
|
+
/(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)\s*=>|\s*function\s*\()/g,
|
|
92
|
+
/(\w+)\s*:\s*function\s*\(/g,
|
|
93
|
+
/class\s+(\w+)/g,
|
|
94
|
+
// Python
|
|
95
|
+
/def\s+(\w+)\s*\(/g,
|
|
96
|
+
/class\s+(\w+)/g,
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
const results = [];
|
|
100
|
+
|
|
101
|
+
const files = await glob(`**/*.${type}`, {
|
|
102
|
+
cwd: searchPath,
|
|
103
|
+
ignore: ['**/node_modules/**', '**/dist/**']
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
for (const file of files) {
|
|
107
|
+
const fullPath = join(searchPath, file);
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const { readFile } = await import('fs/promises');
|
|
111
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
112
|
+
const lines = content.split('\n');
|
|
113
|
+
|
|
114
|
+
for (const funcPattern of functionPatterns) {
|
|
115
|
+
for (let i = 0; i < lines.length; i++) {
|
|
116
|
+
const line = lines[i];
|
|
117
|
+
funcPattern.lastIndex = 0;
|
|
118
|
+
const match = funcPattern.exec(line);
|
|
119
|
+
|
|
120
|
+
if (match) {
|
|
121
|
+
const funcName = match[1];
|
|
122
|
+
|
|
123
|
+
// 检查是否匹配搜索模式
|
|
124
|
+
if (pattern && !funcName.toLowerCase().includes(pattern.toLowerCase())) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
results.push({
|
|
129
|
+
file,
|
|
130
|
+
lineNumber: i + 1,
|
|
131
|
+
name: funcName,
|
|
132
|
+
line: line.trim()
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
// 忽略错误
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return results;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 搜索导入/导出
|
|
147
|
+
*/
|
|
148
|
+
export async function searchImports(searchPath = '.') {
|
|
149
|
+
const results = [];
|
|
150
|
+
const importPatterns = [
|
|
151
|
+
// ES6 imports
|
|
152
|
+
/^import\s+.*?from\s+['"]([^'"]+)['"]/gm,
|
|
153
|
+
// CommonJS require
|
|
154
|
+
/require\(['"]([^'"]+)['"]\)/g,
|
|
155
|
+
// TypeScript exports
|
|
156
|
+
/^export\s+(?:default\s+)?(?:class|function|const|let|var)\s+(\w+)/gm
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
const files = await glob('**/*.{js,jsx,ts,tsx}', {
|
|
160
|
+
cwd: searchPath,
|
|
161
|
+
ignore: ['**/node_modules/**', '**/dist/**']
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
for (const file of files) {
|
|
165
|
+
const fullPath = join(searchPath, file);
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const { readFile } = await import('fs/promises');
|
|
169
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
170
|
+
|
|
171
|
+
const fileImports = {
|
|
172
|
+
file,
|
|
173
|
+
imports: [],
|
|
174
|
+
exports: []
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
for (const pattern of importPatterns) {
|
|
178
|
+
let match;
|
|
179
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
180
|
+
if (match[1]) {
|
|
181
|
+
fileImports.imports.push(match[1]);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (fileImports.imports.length > 0) {
|
|
187
|
+
results.push(fileImports);
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
// 忽略错误
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return results;
|
|
195
|
+
}
|