dev-mcp-server 0.0.2 → 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 +23 -55
- package/README.md +609 -219
- package/cli.js +486 -160
- package/package.json +2 -2
- package/src/agents/BaseAgent.js +113 -0
- package/src/agents/dreamer.js +165 -0
- package/src/agents/improver.js +175 -0
- package/src/agents/specialists.js +202 -0
- package/src/agents/taskDecomposer.js +176 -0
- package/src/agents/teamCoordinator.js +153 -0
- package/src/api/routes/agents.js +172 -0
- package/src/api/routes/extras.js +115 -0
- package/src/api/routes/git.js +72 -0
- package/src/api/routes/ingest.js +60 -40
- package/src/api/routes/knowledge.js +59 -41
- package/src/api/routes/memory.js +41 -0
- package/src/api/routes/newRoutes.js +168 -0
- package/src/api/routes/pipelines.js +41 -0
- package/src/api/routes/planner.js +54 -0
- package/src/api/routes/query.js +24 -0
- package/src/api/routes/sessions.js +54 -0
- package/src/api/routes/tasks.js +67 -0
- package/src/api/routes/tools.js +85 -0
- package/src/api/routes/v5routes.js +196 -0
- package/src/api/server.js +133 -5
- package/src/context/compactor.js +151 -0
- package/src/context/contextEngineer.js +181 -0
- package/src/context/contextVisualizer.js +140 -0
- package/src/core/conversationEngine.js +231 -0
- package/src/core/indexer.js +169 -143
- package/src/core/ingester.js +141 -126
- package/src/core/queryEngine.js +286 -236
- package/src/cron/cronScheduler.js +260 -0
- package/src/dashboard/index.html +1181 -0
- package/src/lsp/symbolNavigator.js +220 -0
- package/src/memory/memoryManager.js +186 -0
- package/src/memory/teamMemory.js +111 -0
- package/src/messaging/messageBus.js +177 -0
- package/src/monitor/proactiveMonitor.js +337 -0
- package/src/pipelines/pipelineEngine.js +230 -0
- package/src/planner/plannerEngine.js +202 -0
- package/src/plugins/builtin/stats-plugin.js +29 -0
- package/src/plugins/pluginManager.js +144 -0
- package/src/prompts/promptEngineer.js +289 -0
- package/src/sessions/sessionManager.js +166 -0
- package/src/skills/skillsManager.js +263 -0
- package/src/storage/store.js +127 -105
- package/src/tasks/taskManager.js +151 -0
- package/src/tools/BashTool.js +154 -0
- package/src/tools/FileEditTool.js +280 -0
- package/src/tools/GitTool.js +212 -0
- package/src/tools/GrepTool.js +199 -0
- package/src/tools/registry.js +1380 -0
- package/src/utils/costTracker.js +69 -0
- package/src/utils/fileParser.js +176 -153
- package/src/utils/llmClient.js +355 -206
- package/src/watcher/fileWatcher.js +137 -0
- package/src/worktrees/worktreeManager.js +176 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full git workflow integration: status, diff, commit, branch, log, review.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const BashTool = require('./BashTool');
|
|
6
|
+
const llm = require('../utils/llmClient');
|
|
7
|
+
const logger = require('../utils/logger');
|
|
8
|
+
const costTracker = require('../utils/costTracker');
|
|
9
|
+
|
|
10
|
+
class GitTool {
|
|
11
|
+
/**
|
|
12
|
+
* Check if a directory is inside a git repository
|
|
13
|
+
*/
|
|
14
|
+
async isGitRepo(cwd = process.cwd()) {
|
|
15
|
+
const result = await BashTool.execute('git rev-parse --is-inside-work-tree', { cwd, approved: true });
|
|
16
|
+
return result.exitCode === 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get full git status
|
|
21
|
+
*/
|
|
22
|
+
async status(cwd = process.cwd()) {
|
|
23
|
+
const isRepo = await this.isGitRepo(cwd);
|
|
24
|
+
if (!isRepo) return { branch: '(not a git repo)', ahead: 0, behind: 0, staged: [], unstaged: [], untracked: [], recentCommits: [], error: 'Not a git repository' };
|
|
25
|
+
|
|
26
|
+
const [statusResult, branchResult] = await Promise.all([
|
|
27
|
+
BashTool.execute('git status --porcelain=v2 --branch', { cwd, approved: true }),
|
|
28
|
+
BashTool.execute('git log --oneline -5', { cwd, approved: true }),
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
return this._parseStatus(statusResult.stdout || '', branchResult.stdout || '');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_parseStatus(statusOutput, logOutput) {
|
|
35
|
+
const lines = statusOutput.split('\n').filter(Boolean);
|
|
36
|
+
const result = { branch: '', ahead: 0, behind: 0, staged: [], unstaged: [], untracked: [], recentCommits: [] };
|
|
37
|
+
|
|
38
|
+
for (const line of lines) {
|
|
39
|
+
if (line.startsWith('# branch.oid')) continue;
|
|
40
|
+
if (line.startsWith('# branch.head')) result.branch = line.split(' ')[2];
|
|
41
|
+
if (line.startsWith('# branch.ab')) {
|
|
42
|
+
const m = line.match(/\+(\d+)\s+-(\d+)/);
|
|
43
|
+
if (m) { result.ahead = parseInt(m[1]); result.behind = parseInt(m[2]); }
|
|
44
|
+
}
|
|
45
|
+
if (line.startsWith('1 ') || line.startsWith('2 ')) {
|
|
46
|
+
const xy = line.slice(2, 4);
|
|
47
|
+
const file = line.split('\t').pop();
|
|
48
|
+
if (xy[0] !== '.' && xy[0] !== '?') result.staged.push({ status: xy[0], file });
|
|
49
|
+
if (xy[1] !== '.' && xy[1] !== '?') result.unstaged.push({ status: xy[1], file });
|
|
50
|
+
}
|
|
51
|
+
if (line.startsWith('? ')) result.untracked.push(line.slice(2));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
result.recentCommits = logOutput.split('\n').filter(Boolean).slice(0, 5);
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get diff (staged, unstaged, or specific file)
|
|
60
|
+
*/
|
|
61
|
+
async diff(options = {}) {
|
|
62
|
+
const { cwd = process.cwd(), staged = false, file, stat = false } = options;
|
|
63
|
+
const isRepo = await this.isGitRepo(cwd);
|
|
64
|
+
if (!isRepo) return { diff: '', hasChanges: false, staged, error: 'Not a git repository' };
|
|
65
|
+
|
|
66
|
+
const flags = [
|
|
67
|
+
staged ? '--staged' : '',
|
|
68
|
+
stat ? '--stat' : '',
|
|
69
|
+
'--no-color',
|
|
70
|
+
file ? `-- ${JSON.stringify(file)}` : '',
|
|
71
|
+
].filter(Boolean).join(' ');
|
|
72
|
+
|
|
73
|
+
const result = await BashTool.execute(`git diff ${flags}`, { cwd, approved: true });
|
|
74
|
+
return {
|
|
75
|
+
diff: result.stdout || '',
|
|
76
|
+
hasChanges: (result.stdout || '').trim().length > 0,
|
|
77
|
+
staged,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Stage files and create a commit with AI-generated message
|
|
83
|
+
*/
|
|
84
|
+
async commit(options = {}) {
|
|
85
|
+
const { cwd = process.cwd(), files = ['.'], message, autoMessage = false } = options;
|
|
86
|
+
const isRepo = await this.isGitRepo(cwd);
|
|
87
|
+
if (!isRepo) return { success: false, message: 'Not a git repository' };
|
|
88
|
+
|
|
89
|
+
// Stage files
|
|
90
|
+
const stageCmd = `git add ${files.map(f => JSON.stringify(f)).join(' ')}`;
|
|
91
|
+
const stageResult = await BashTool.execute(stageCmd, { cwd, approved: true });
|
|
92
|
+
if (stageResult.exitCode !== 0) return { success: false, message: stageResult.stderr || 'git add failed' };
|
|
93
|
+
|
|
94
|
+
// Get staged diff for context
|
|
95
|
+
const diffResult = await this.diff({ cwd, staged: true });
|
|
96
|
+
if (!diffResult.hasChanges) return { success: false, message: 'Nothing staged to commit' };
|
|
97
|
+
|
|
98
|
+
let commitMessage = message;
|
|
99
|
+
if (!commitMessage || autoMessage) {
|
|
100
|
+
commitMessage = await this._generateCommitMessage(diffResult.diff);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const result = await BashTool.execute(`git commit -m ${JSON.stringify(commitMessage)}`, { cwd, approved: true });
|
|
104
|
+
if (result.exitCode !== 0) return { success: false, message: result.stderr || 'commit failed' };
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
success: true,
|
|
108
|
+
message: commitMessage,
|
|
109
|
+
output: result.stdout,
|
|
110
|
+
autoGenerated: !message || autoMessage,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async _generateCommitMessage(diff) {
|
|
115
|
+
if (!diff || diff.trim().length === 0) return 'chore: update files';
|
|
116
|
+
const truncatedDiff = diff.slice(0, 4000);
|
|
117
|
+
try {
|
|
118
|
+
const response = await llm.chat({
|
|
119
|
+
model: llm.model('fast'),
|
|
120
|
+
max_tokens: 150,
|
|
121
|
+
messages: [{
|
|
122
|
+
role: 'user',
|
|
123
|
+
content: `Generate a concise git commit message in conventional commits format (type: description) for this diff. Reply ONLY with the commit message, no explanation.\n\nDiff:\n${truncatedDiff}`,
|
|
124
|
+
}],
|
|
125
|
+
});
|
|
126
|
+
costTracker.record({
|
|
127
|
+
model: llm.model('fast'),
|
|
128
|
+
inputTokens: response.usage.input_tokens,
|
|
129
|
+
outputTokens: response.usage.output_tokens,
|
|
130
|
+
queryType: 'git-commit',
|
|
131
|
+
});
|
|
132
|
+
return response.content[0].text.trim().replace(/^["']|["']$/g, '');
|
|
133
|
+
} catch {
|
|
134
|
+
return 'chore: update files';
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* AI-powered code review of staged changes or a file
|
|
140
|
+
*/
|
|
141
|
+
async review(options = {}) {
|
|
142
|
+
const { cwd = process.cwd(), staged = false, file, focus = '' } = options;
|
|
143
|
+
const diffResult = await this.diff({ cwd, staged, file });
|
|
144
|
+
if (!diffResult.hasChanges) return { review: 'No changes to review.', hasIssues: false };
|
|
145
|
+
|
|
146
|
+
const diff = diffResult.diff.slice(0, 6000);
|
|
147
|
+
const focusInstruction = focus
|
|
148
|
+
? `Focus especially on: ${focus}`
|
|
149
|
+
: 'Focus on: bugs, security issues, performance problems, code style, and maintainability.';
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const response = await llm.chat({
|
|
153
|
+
model: llm.model('smart'),
|
|
154
|
+
max_tokens: 1500,
|
|
155
|
+
system: `You are a senior software engineer conducting a code review. Be direct and specific.
|
|
156
|
+
Format your review as:
|
|
157
|
+
## Summary
|
|
158
|
+
(1-2 sentences)
|
|
159
|
+
|
|
160
|
+
## Issues Found
|
|
161
|
+
(list any bugs, security problems, or critical issues — be specific with line references)
|
|
162
|
+
|
|
163
|
+
## Suggestions
|
|
164
|
+
(improvements, not blockers)
|
|
165
|
+
|
|
166
|
+
## Verdict
|
|
167
|
+
LGTM | Needs Changes | Blocking Issues`,
|
|
168
|
+
messages: [{ role: 'user', content: `Review this diff:\n${focusInstruction}\n\n\`\`\`diff\n${diff}\n\`\`\`` }],
|
|
169
|
+
});
|
|
170
|
+
costTracker.record({ model: llm.model('smart'), inputTokens: response.usage.input_tokens, outputTokens: response.usage.output_tokens, queryType: 'git-review' });
|
|
171
|
+
const reviewText = response.content[0].text;
|
|
172
|
+
return { review: reviewText, hasIssues: /blocking issues|needs changes/i.test(reviewText), diff: diffResult.diff };
|
|
173
|
+
} catch (e) {
|
|
174
|
+
return { review: `Review failed: ${e.message}`, hasIssues: false };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get commit log
|
|
180
|
+
*/
|
|
181
|
+
async log(options = {}) {
|
|
182
|
+
const { cwd = process.cwd(), limit = 10, file, oneline = false } = options;
|
|
183
|
+
const isRepo = await this.isGitRepo(cwd);
|
|
184
|
+
if (!isRepo) return oneline ? [] : [];
|
|
185
|
+
|
|
186
|
+
const format = oneline ? '--oneline' : '--pretty=format:"%H|%an|%ae|%ad|%s" --date=short';
|
|
187
|
+
const fileFlag = file ? `-- ${JSON.stringify(file)}` : '';
|
|
188
|
+
const result = await BashTool.execute(`git log -${limit} ${format} ${fileFlag}`, { cwd, approved: true });
|
|
189
|
+
if (result.exitCode !== 0) return [];
|
|
190
|
+
if (oneline) return result.stdout.split('\n').filter(Boolean);
|
|
191
|
+
return result.stdout.split('\n').filter(Boolean).map(line => {
|
|
192
|
+
const [hash, author, email, date, ...subjectParts] = line.replace(/^"|"$/g, '').split('|');
|
|
193
|
+
return { hash: hash?.slice(0, 8), author, email, date, subject: subjectParts.join('|') };
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* List branches
|
|
199
|
+
*/
|
|
200
|
+
async branches(cwd = process.cwd()) {
|
|
201
|
+
const isRepo = await this.isGitRepo(cwd);
|
|
202
|
+
if (!isRepo) return [];
|
|
203
|
+
const result = await BashTool.execute('git branch -a --sort=-committerdate', { cwd, approved: true });
|
|
204
|
+
if (result.exitCode !== 0) return [];
|
|
205
|
+
return result.stdout.split('\n').filter(Boolean).map(b => ({
|
|
206
|
+
name: b.replace(/^\*?\s+/, '').trim(),
|
|
207
|
+
current: b.startsWith('*'),
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = new GitTool();
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GrepTool — ripgrep-based content search with fallback.
|
|
3
|
+
* Searches for patterns across the codebase quickly without needing to ingest first.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { exec } = require('child_process');
|
|
7
|
+
const { promisify } = require('util');
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const logger = require('../utils/logger');
|
|
11
|
+
|
|
12
|
+
// Check if ripgrep is available
|
|
13
|
+
let RG_AVAILABLE = null;
|
|
14
|
+
async function checkRg() {
|
|
15
|
+
if (RG_AVAILABLE !== null) return RG_AVAILABLE;
|
|
16
|
+
try {
|
|
17
|
+
await execAsync('rg --version');
|
|
18
|
+
RG_AVAILABLE = true;
|
|
19
|
+
} catch {
|
|
20
|
+
RG_AVAILABLE = false;
|
|
21
|
+
}
|
|
22
|
+
return RG_AVAILABLE;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class GrepTool {
|
|
26
|
+
/**
|
|
27
|
+
* Search for a pattern in files
|
|
28
|
+
* @param {string} pattern - regex or literal string
|
|
29
|
+
* @param {object} opts
|
|
30
|
+
* cwd - directory to search in
|
|
31
|
+
* glob - file glob filter (e.g. '*.js')
|
|
32
|
+
* ignoreCase - case-insensitive
|
|
33
|
+
* maxResults - limit results
|
|
34
|
+
* contextLines - lines of context around each match
|
|
35
|
+
* literal - treat pattern as literal string (no regex)
|
|
36
|
+
*/
|
|
37
|
+
async search(pattern, opts = {}) {
|
|
38
|
+
const {
|
|
39
|
+
cwd = process.cwd(),
|
|
40
|
+
glob,
|
|
41
|
+
ignoreCase = false,
|
|
42
|
+
maxResults = 50,
|
|
43
|
+
contextLines = 2,
|
|
44
|
+
literal = false,
|
|
45
|
+
} = opts;
|
|
46
|
+
|
|
47
|
+
const useRg = await checkRg();
|
|
48
|
+
|
|
49
|
+
if (useRg) {
|
|
50
|
+
return this._rgSearch(pattern, { cwd, glob, ignoreCase, maxResults, contextLines, literal });
|
|
51
|
+
} else {
|
|
52
|
+
return this._nativeSearch(pattern, { cwd, glob, ignoreCase, maxResults });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async _rgSearch(pattern, opts) {
|
|
57
|
+
const { cwd, glob, ignoreCase, maxResults, contextLines, literal } = opts;
|
|
58
|
+
|
|
59
|
+
const flags = [
|
|
60
|
+
'--json',
|
|
61
|
+
`--max-count=${maxResults}`,
|
|
62
|
+
`--context=${contextLines}`,
|
|
63
|
+
ignoreCase ? '--ignore-case' : '',
|
|
64
|
+
literal ? '--fixed-strings' : '',
|
|
65
|
+
glob ? `--glob '${glob}'` : '',
|
|
66
|
+
'--hidden',
|
|
67
|
+
'--no-follow',
|
|
68
|
+
// Standard ignores
|
|
69
|
+
'--glob !node_modules',
|
|
70
|
+
'--glob !.git',
|
|
71
|
+
'--glob !dist',
|
|
72
|
+
'--glob !build',
|
|
73
|
+
'--glob !*.min.js',
|
|
74
|
+
].filter(Boolean).join(' ');
|
|
75
|
+
|
|
76
|
+
const cmd = `rg ${flags} ${JSON.stringify(pattern)}`;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const { stdout } = await execAsync(cmd, { cwd, maxBuffer: 5 * 1024 * 1024 });
|
|
80
|
+
return this._parseRgJson(stdout, maxResults);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (err.code === 1) return { matches: [], total: 0, tool: 'ripgrep', note: 'No matches found' };
|
|
83
|
+
throw new Error(`ripgrep error: ${err.message}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_parseRgJson(output, maxResults) {
|
|
88
|
+
const lines = output.trim().split('\n').filter(Boolean);
|
|
89
|
+
const matches = [];
|
|
90
|
+
let total = 0;
|
|
91
|
+
|
|
92
|
+
for (const line of lines) {
|
|
93
|
+
try {
|
|
94
|
+
const obj = JSON.parse(line);
|
|
95
|
+
if (obj.type === 'match') {
|
|
96
|
+
total++;
|
|
97
|
+
if (matches.length < maxResults) {
|
|
98
|
+
matches.push({
|
|
99
|
+
file: obj.data.path.text,
|
|
100
|
+
lineNumber: obj.data.line_number,
|
|
101
|
+
line: obj.data.lines.text.trimEnd(),
|
|
102
|
+
submatches: obj.data.submatches?.map(s => s.match?.text) || [],
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch { }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { matches, total, tool: 'ripgrep' };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async _nativeSearch(pattern, opts) {
|
|
113
|
+
const { cwd, ignoreCase, maxResults } = opts;
|
|
114
|
+
const { glob: globModule } = require('glob');
|
|
115
|
+
const fs = require('fs');
|
|
116
|
+
|
|
117
|
+
const files = await globModule('**/*', {
|
|
118
|
+
cwd,
|
|
119
|
+
absolute: true,
|
|
120
|
+
nodir: true,
|
|
121
|
+
ignore: ['**/node_modules/**', '**/.git/**', '**/dist/**'],
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const regex = new RegExp(pattern, ignoreCase ? 'gi' : 'g');
|
|
125
|
+
const matches = [];
|
|
126
|
+
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
if (matches.length >= maxResults) break;
|
|
129
|
+
try {
|
|
130
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
131
|
+
const lines = content.split('\n');
|
|
132
|
+
lines.forEach((line, idx) => {
|
|
133
|
+
if (matches.length < maxResults && regex.test(line)) {
|
|
134
|
+
matches.push({
|
|
135
|
+
file: path.relative(cwd, file),
|
|
136
|
+
lineNumber: idx + 1,
|
|
137
|
+
line: line.trimEnd(),
|
|
138
|
+
submatches: [],
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
regex.lastIndex = 0;
|
|
142
|
+
});
|
|
143
|
+
} catch { }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { matches, total: matches.length, tool: 'native-grep' };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Quick search: find all definitions of a symbol (function, class, const)
|
|
151
|
+
*/
|
|
152
|
+
async findDefinitions(symbol, cwd = process.cwd()) {
|
|
153
|
+
const patterns = [
|
|
154
|
+
`function ${symbol}`,
|
|
155
|
+
`class ${symbol}`,
|
|
156
|
+
`const ${symbol}\\s*=`,
|
|
157
|
+
`let ${symbol}\\s*=`,
|
|
158
|
+
`var ${symbol}\\s*=`,
|
|
159
|
+
`${symbol}\\s*\\(`, // method definition
|
|
160
|
+
`exports\\.${symbol}`,
|
|
161
|
+
`module\\.exports.*${symbol}`,
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
const allMatches = [];
|
|
165
|
+
for (const p of patterns) {
|
|
166
|
+
try {
|
|
167
|
+
const result = await this.search(p, { cwd, maxResults: 10 });
|
|
168
|
+
allMatches.push(...result.matches);
|
|
169
|
+
} catch { }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Deduplicate by file+line
|
|
173
|
+
const seen = new Set();
|
|
174
|
+
return allMatches.filter(m => {
|
|
175
|
+
const key = `${m.file}:${m.lineNumber}`;
|
|
176
|
+
if (seen.has(key)) return false;
|
|
177
|
+
seen.add(key);
|
|
178
|
+
return true;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Find all imports/requires of a module
|
|
184
|
+
*/
|
|
185
|
+
async findImports(moduleName, cwd = process.cwd()) {
|
|
186
|
+
const pattern = `(import|require).*['"]${moduleName}['"]`;
|
|
187
|
+
return this.search(pattern, { cwd, maxResults: 30 });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Find all TODO/FIXME/HACK/BUG comments
|
|
192
|
+
*/
|
|
193
|
+
async findTodos(cwd = process.cwd()) {
|
|
194
|
+
const pattern = '(TODO|FIXME|HACK|BUG|XXX|NOTE)\\s*[:\\-]?';
|
|
195
|
+
return this.search(pattern, { cwd, maxResults: 100, ignoreCase: true });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports = new GrepTool();
|