@stan-chen/simple-cli 0.2.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 +287 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +259 -0
- package/dist/commands/add.d.ts +9 -0
- package/dist/commands/add.js +50 -0
- package/dist/commands/git/commit.d.ts +12 -0
- package/dist/commands/git/commit.js +97 -0
- package/dist/commands/git/status.d.ts +6 -0
- package/dist/commands/git/status.js +42 -0
- package/dist/commands/index.d.ts +16 -0
- package/dist/commands/index.js +376 -0
- package/dist/commands/mcp/status.d.ts +6 -0
- package/dist/commands/mcp/status.js +31 -0
- package/dist/commands/swarm.d.ts +36 -0
- package/dist/commands/swarm.js +236 -0
- package/dist/commands.d.ts +32 -0
- package/dist/commands.js +427 -0
- package/dist/context.d.ts +116 -0
- package/dist/context.js +327 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +109 -0
- package/dist/lib/agent.d.ts +98 -0
- package/dist/lib/agent.js +281 -0
- package/dist/lib/editor.d.ts +74 -0
- package/dist/lib/editor.js +441 -0
- package/dist/lib/git.d.ts +164 -0
- package/dist/lib/git.js +351 -0
- package/dist/lib/ui.d.ts +159 -0
- package/dist/lib/ui.js +252 -0
- package/dist/mcp/client.d.ts +22 -0
- package/dist/mcp/client.js +81 -0
- package/dist/mcp/manager.d.ts +186 -0
- package/dist/mcp/manager.js +442 -0
- package/dist/prompts/provider.d.ts +22 -0
- package/dist/prompts/provider.js +78 -0
- package/dist/providers/index.d.ts +15 -0
- package/dist/providers/index.js +82 -0
- package/dist/providers/multi.d.ts +11 -0
- package/dist/providers/multi.js +28 -0
- package/dist/registry.d.ts +24 -0
- package/dist/registry.js +379 -0
- package/dist/repoMap.d.ts +5 -0
- package/dist/repoMap.js +79 -0
- package/dist/router.d.ts +41 -0
- package/dist/router.js +108 -0
- package/dist/skills.d.ts +25 -0
- package/dist/skills.js +288 -0
- package/dist/swarm/coordinator.d.ts +86 -0
- package/dist/swarm/coordinator.js +257 -0
- package/dist/swarm/index.d.ts +28 -0
- package/dist/swarm/index.js +29 -0
- package/dist/swarm/task.d.ts +104 -0
- package/dist/swarm/task.js +221 -0
- package/dist/swarm/types.d.ts +132 -0
- package/dist/swarm/types.js +37 -0
- package/dist/swarm/worker.d.ts +107 -0
- package/dist/swarm/worker.js +299 -0
- package/dist/tools/analyzeFile.d.ts +16 -0
- package/dist/tools/analyzeFile.js +43 -0
- package/dist/tools/git.d.ts +40 -0
- package/dist/tools/git.js +236 -0
- package/dist/tools/glob.d.ts +34 -0
- package/dist/tools/glob.js +165 -0
- package/dist/tools/grep.d.ts +53 -0
- package/dist/tools/grep.js +296 -0
- package/dist/tools/linter.d.ts +35 -0
- package/dist/tools/linter.js +349 -0
- package/dist/tools/listDir.d.ts +29 -0
- package/dist/tools/listDir.js +50 -0
- package/dist/tools/memory.d.ts +34 -0
- package/dist/tools/memory.js +215 -0
- package/dist/tools/readFiles.d.ts +25 -0
- package/dist/tools/readFiles.js +31 -0
- package/dist/tools/reloadTools.d.ts +11 -0
- package/dist/tools/reloadTools.js +22 -0
- package/dist/tools/runCommand.d.ts +32 -0
- package/dist/tools/runCommand.js +79 -0
- package/dist/tools/scraper.d.ts +31 -0
- package/dist/tools/scraper.js +211 -0
- package/dist/tools/writeFiles.d.ts +63 -0
- package/dist/tools/writeFiles.js +87 -0
- package/dist/ui/server.d.ts +5 -0
- package/dist/ui/server.js +74 -0
- package/dist/watcher.d.ts +35 -0
- package/dist/watcher.js +164 -0
- package/docs/assets/logo.jpeg +0 -0
- package/package.json +78 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intelligent Code Editor using ts-morph and fuzzy matching
|
|
3
|
+
* Provides Aider-style SEARCH/REPLACE with smart matching
|
|
4
|
+
*/
|
|
5
|
+
import { Project, SyntaxKind } from 'ts-morph';
|
|
6
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
8
|
+
import { dirname } from 'path';
|
|
9
|
+
import * as diff from 'diff';
|
|
10
|
+
import levenshtein from 'fast-levenshtein';
|
|
11
|
+
/**
|
|
12
|
+
* Calculate similarity ratio between two strings (0-1)
|
|
13
|
+
*/
|
|
14
|
+
function similarity(a, b) {
|
|
15
|
+
if (a === b)
|
|
16
|
+
return 1;
|
|
17
|
+
if (!a || !b)
|
|
18
|
+
return 0;
|
|
19
|
+
const distance = levenshtein.get(a, b);
|
|
20
|
+
const maxLen = Math.max(a.length, b.length);
|
|
21
|
+
return 1 - distance / maxLen;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Find the most similar chunk in content
|
|
25
|
+
*/
|
|
26
|
+
function findMostSimilarChunk(content, search, threshold = 0.6) {
|
|
27
|
+
const contentLines = content.split('\n');
|
|
28
|
+
const searchLines = search.split('\n');
|
|
29
|
+
const searchLen = searchLines.length;
|
|
30
|
+
let bestMatch = { start: -1, end: -1, similarity: 0, text: '' };
|
|
31
|
+
// Sliding window search
|
|
32
|
+
for (let i = 0; i <= contentLines.length - searchLen; i++) {
|
|
33
|
+
const chunk = contentLines.slice(i, i + searchLen).join('\n');
|
|
34
|
+
const sim = similarity(chunk, search);
|
|
35
|
+
if (sim > bestMatch.similarity) {
|
|
36
|
+
bestMatch = {
|
|
37
|
+
start: i,
|
|
38
|
+
end: i + searchLen,
|
|
39
|
+
similarity: sim,
|
|
40
|
+
text: chunk,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Also try with flexible window sizes (+/- 2 lines)
|
|
45
|
+
for (let delta = -2; delta <= 2; delta++) {
|
|
46
|
+
if (delta === 0)
|
|
47
|
+
continue;
|
|
48
|
+
const adjustedLen = searchLen + delta;
|
|
49
|
+
if (adjustedLen < 1)
|
|
50
|
+
continue;
|
|
51
|
+
for (let i = 0; i <= contentLines.length - adjustedLen; i++) {
|
|
52
|
+
const chunk = contentLines.slice(i, i + adjustedLen).join('\n');
|
|
53
|
+
const sim = similarity(chunk, search);
|
|
54
|
+
if (sim > bestMatch.similarity) {
|
|
55
|
+
bestMatch = {
|
|
56
|
+
start: i,
|
|
57
|
+
end: i + adjustedLen,
|
|
58
|
+
similarity: sim,
|
|
59
|
+
text: chunk,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (bestMatch.similarity >= threshold) {
|
|
65
|
+
return bestMatch;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Handle whitespace-flexible matching
|
|
71
|
+
*/
|
|
72
|
+
function matchWithFlexibleWhitespace(contentLines, searchLines) {
|
|
73
|
+
// Strip leading whitespace from search to compare content only
|
|
74
|
+
const strippedSearch = searchLines.map(line => line.trimStart());
|
|
75
|
+
const searchLen = searchLines.length;
|
|
76
|
+
for (let i = 0; i <= contentLines.length - searchLen; i++) {
|
|
77
|
+
const chunk = contentLines.slice(i, i + searchLen);
|
|
78
|
+
const strippedChunk = chunk.map(line => line.trimStart());
|
|
79
|
+
// Check if content matches when ignoring leading whitespace
|
|
80
|
+
if (strippedChunk.every((line, idx) => line === strippedSearch[idx])) {
|
|
81
|
+
// Calculate the indent difference
|
|
82
|
+
const firstNonEmptyIdx = chunk.findIndex(line => line.trim());
|
|
83
|
+
if (firstNonEmptyIdx >= 0) {
|
|
84
|
+
const actualIndent = chunk[firstNonEmptyIdx].match(/^(\s*)/)?.[1] || '';
|
|
85
|
+
const searchIndent = searchLines[firstNonEmptyIdx].match(/^(\s*)/)?.[1] || '';
|
|
86
|
+
const indentDiff = actualIndent.slice(0, actualIndent.length - searchIndent.length);
|
|
87
|
+
return {
|
|
88
|
+
start: i,
|
|
89
|
+
end: i + searchLen,
|
|
90
|
+
indent: indentDiff,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return { start: i, end: i + searchLen, indent: '' };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Apply indent adjustment to replacement text
|
|
100
|
+
*/
|
|
101
|
+
function applyIndent(text, indent) {
|
|
102
|
+
return text
|
|
103
|
+
.split('\n')
|
|
104
|
+
.map(line => (line.trim() ? indent + line : line))
|
|
105
|
+
.join('\n');
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Handle ... (ellipsis) in search/replace blocks
|
|
109
|
+
*/
|
|
110
|
+
function handleEllipsis(content, search, replace) {
|
|
111
|
+
const dotsPattern = /^\s*\.\.\.\s*$/m;
|
|
112
|
+
if (!dotsPattern.test(search)) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const searchParts = search.split(dotsPattern);
|
|
116
|
+
const replaceParts = replace.split(dotsPattern);
|
|
117
|
+
if (searchParts.length !== replaceParts.length) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
let result = content;
|
|
121
|
+
for (let i = 0; i < searchParts.length; i++) {
|
|
122
|
+
const searchPart = searchParts[i].trim();
|
|
123
|
+
const replacePart = replaceParts[i].trim();
|
|
124
|
+
if (!searchPart && !replacePart)
|
|
125
|
+
continue;
|
|
126
|
+
if (!searchPart && replacePart) {
|
|
127
|
+
// Append to end
|
|
128
|
+
result = result.trimEnd() + '\n' + replacePart;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (!result.includes(searchPart)) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
result = result.replace(searchPart, replacePart);
|
|
135
|
+
}
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Apply a single edit to content
|
|
140
|
+
*/
|
|
141
|
+
export function applyEdit(content, search, replace) {
|
|
142
|
+
// Normalize line endings
|
|
143
|
+
content = content.replace(/\r\n/g, '\n');
|
|
144
|
+
search = search.replace(/\r\n/g, '\n').trim();
|
|
145
|
+
replace = replace.replace(/\r\n/g, '\n');
|
|
146
|
+
// 1. Try exact match
|
|
147
|
+
if (content.includes(search)) {
|
|
148
|
+
return {
|
|
149
|
+
success: true,
|
|
150
|
+
content: content.replace(search, replace),
|
|
151
|
+
method: 'exact',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// 2. Try ellipsis handling
|
|
155
|
+
const ellipsisResult = handleEllipsis(content, search, replace);
|
|
156
|
+
if (ellipsisResult) {
|
|
157
|
+
return {
|
|
158
|
+
success: true,
|
|
159
|
+
content: ellipsisResult,
|
|
160
|
+
method: 'ellipsis',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// 3. Try whitespace-flexible matching
|
|
164
|
+
const contentLines = content.split('\n');
|
|
165
|
+
const searchLines = search.split('\n');
|
|
166
|
+
const wsMatch = matchWithFlexibleWhitespace(contentLines, searchLines);
|
|
167
|
+
if (wsMatch) {
|
|
168
|
+
const adjustedReplace = applyIndent(replace, wsMatch.indent);
|
|
169
|
+
const newLines = [
|
|
170
|
+
...contentLines.slice(0, wsMatch.start),
|
|
171
|
+
...adjustedReplace.split('\n'),
|
|
172
|
+
...contentLines.slice(wsMatch.end),
|
|
173
|
+
];
|
|
174
|
+
return {
|
|
175
|
+
success: true,
|
|
176
|
+
content: newLines.join('\n'),
|
|
177
|
+
method: 'whitespace-flex',
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// 4. Try fuzzy matching
|
|
181
|
+
const fuzzyMatch = findMostSimilarChunk(content, search, 0.7);
|
|
182
|
+
if (fuzzyMatch) {
|
|
183
|
+
const newLines = [
|
|
184
|
+
...contentLines.slice(0, fuzzyMatch.start),
|
|
185
|
+
...replace.split('\n'),
|
|
186
|
+
...contentLines.slice(fuzzyMatch.end),
|
|
187
|
+
];
|
|
188
|
+
return {
|
|
189
|
+
success: true,
|
|
190
|
+
content: newLines.join('\n'),
|
|
191
|
+
method: `fuzzy (${(fuzzyMatch.similarity * 100).toFixed(0)}% match)`,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
// 5. Failed - provide suggestion
|
|
195
|
+
const similarChunk = findMostSimilarChunk(content, search, 0.4);
|
|
196
|
+
let suggestion;
|
|
197
|
+
if (similarChunk) {
|
|
198
|
+
suggestion = `Did you mean to match these lines (${(similarChunk.similarity * 100).toFixed(0)}% similar)?
|
|
199
|
+
|
|
200
|
+
\`\`\`
|
|
201
|
+
${similarChunk.text}
|
|
202
|
+
\`\`\``;
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
success: false,
|
|
206
|
+
content,
|
|
207
|
+
method: 'none',
|
|
208
|
+
suggestion,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Apply edits to a file
|
|
213
|
+
*/
|
|
214
|
+
export async function applyFileEdits(edits) {
|
|
215
|
+
const results = [];
|
|
216
|
+
const fileContents = new Map();
|
|
217
|
+
// Group edits by file
|
|
218
|
+
const editsByFile = new Map();
|
|
219
|
+
for (const edit of edits) {
|
|
220
|
+
const existing = editsByFile.get(edit.file) || [];
|
|
221
|
+
existing.push(edit);
|
|
222
|
+
editsByFile.set(edit.file, existing);
|
|
223
|
+
}
|
|
224
|
+
for (const [file, fileEdits] of editsByFile) {
|
|
225
|
+
// Load file content
|
|
226
|
+
let content;
|
|
227
|
+
try {
|
|
228
|
+
if (existsSync(file)) {
|
|
229
|
+
content = await readFile(file, 'utf-8');
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
// New file - ensure directory exists
|
|
233
|
+
await mkdir(dirname(file), { recursive: true });
|
|
234
|
+
content = '';
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
for (const edit of fileEdits) {
|
|
239
|
+
results.push({
|
|
240
|
+
file,
|
|
241
|
+
success: false,
|
|
242
|
+
applied: false,
|
|
243
|
+
error: `Failed to read file: ${error instanceof Error ? error.message : error}`,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
// Apply each edit
|
|
249
|
+
let currentContent = content;
|
|
250
|
+
let anyApplied = false;
|
|
251
|
+
for (const edit of fileEdits) {
|
|
252
|
+
// Handle empty search (new file or append)
|
|
253
|
+
if (!edit.search.trim()) {
|
|
254
|
+
currentContent = currentContent + edit.replace;
|
|
255
|
+
results.push({
|
|
256
|
+
file,
|
|
257
|
+
success: true,
|
|
258
|
+
applied: true,
|
|
259
|
+
diff: `+++ ${file}\n+ ${edit.replace.split('\n').join('\n+ ')}`,
|
|
260
|
+
});
|
|
261
|
+
anyApplied = true;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const result = applyEdit(currentContent, edit.search, edit.replace);
|
|
265
|
+
if (result.success) {
|
|
266
|
+
const diffText = diff.createPatch(file, currentContent, result.content);
|
|
267
|
+
currentContent = result.content;
|
|
268
|
+
results.push({
|
|
269
|
+
file,
|
|
270
|
+
success: true,
|
|
271
|
+
applied: true,
|
|
272
|
+
diff: diffText,
|
|
273
|
+
});
|
|
274
|
+
anyApplied = true;
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
results.push({
|
|
278
|
+
file,
|
|
279
|
+
success: false,
|
|
280
|
+
applied: false,
|
|
281
|
+
error: 'SEARCH block did not match any content in file',
|
|
282
|
+
suggestion: result.suggestion,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// Write back if any edits were applied
|
|
287
|
+
if (anyApplied) {
|
|
288
|
+
try {
|
|
289
|
+
await writeFile(file, currentContent);
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
results.push({
|
|
293
|
+
file,
|
|
294
|
+
success: false,
|
|
295
|
+
applied: false,
|
|
296
|
+
error: `Failed to write file: ${error instanceof Error ? error.message : error}`,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return results;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Parse SEARCH/REPLACE blocks from LLM response
|
|
305
|
+
*/
|
|
306
|
+
export function parseEditBlocks(response, validFiles) {
|
|
307
|
+
const blocks = [];
|
|
308
|
+
const seen = new Set(); // Track unique blocks by content
|
|
309
|
+
// Pattern for <<<<<<< SEARCH ... ======= ... >>>>>>> REPLACE inside code fence
|
|
310
|
+
// Captures: filename on line before ```, then search, then replace
|
|
311
|
+
const blockPattern = /([^\n]+)\n```[^\n]*\n<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE\n```/g;
|
|
312
|
+
// Also try without code fence (for plain text format)
|
|
313
|
+
const altPattern = /([^\n]+)\n<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE/g;
|
|
314
|
+
const processMatch = (match) => {
|
|
315
|
+
let file = match[1].trim();
|
|
316
|
+
const search = match[2];
|
|
317
|
+
const replace = match[3];
|
|
318
|
+
// Create a unique key for deduplication
|
|
319
|
+
const key = `${search}|||${replace}`;
|
|
320
|
+
if (seen.has(key)) {
|
|
321
|
+
return; // Skip duplicate
|
|
322
|
+
}
|
|
323
|
+
seen.add(key);
|
|
324
|
+
// Clean up filename - strip markdown formatting
|
|
325
|
+
file = file.replace(/^[#*]+\s*/, ''); // Remove leading # and *
|
|
326
|
+
file = file.replace(/`/g, ''); // Remove all backticks
|
|
327
|
+
file = file.replace(/\s*[#*]+$/, ''); // Remove trailing # and *
|
|
328
|
+
file = file.replace(/^(File:\s*|Filename:\s*)/i, '');
|
|
329
|
+
file = file.trim();
|
|
330
|
+
// Skip if filename looks like a language hint (e.g., "typescript", "javascript")
|
|
331
|
+
const languageHints = ['typescript', 'javascript', 'python', 'java', 'go', 'rust', 'tsx', 'jsx', 'ts', 'js', 'py', 'rb', 'cpp', 'c', 'cs', 'sh', 'bash', 'json', 'yaml', 'yml', 'md', 'html', 'css'];
|
|
332
|
+
if (languageHints.includes(file.toLowerCase())) {
|
|
333
|
+
return; // Skip - this is likely a language hint, not a filename
|
|
334
|
+
}
|
|
335
|
+
// Try to match against valid files
|
|
336
|
+
if (validFiles && validFiles.length > 0) {
|
|
337
|
+
const exactMatch = validFiles.find(f => f === file || f.endsWith('/' + file));
|
|
338
|
+
if (exactMatch) {
|
|
339
|
+
file = exactMatch;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
blocks.push({ file, search, replace });
|
|
343
|
+
};
|
|
344
|
+
// First try the fenced pattern (more specific)
|
|
345
|
+
let match;
|
|
346
|
+
while ((match = blockPattern.exec(response)) !== null) {
|
|
347
|
+
processMatch(match);
|
|
348
|
+
}
|
|
349
|
+
// Then try the alt pattern for any remaining blocks
|
|
350
|
+
while ((match = altPattern.exec(response)) !== null) {
|
|
351
|
+
processMatch(match);
|
|
352
|
+
}
|
|
353
|
+
return blocks;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* TypeScript/JavaScript AST-aware editing using ts-morph
|
|
357
|
+
*/
|
|
358
|
+
export class ASTEditor {
|
|
359
|
+
project;
|
|
360
|
+
constructor() {
|
|
361
|
+
this.project = new Project({
|
|
362
|
+
useInMemoryFileSystem: true,
|
|
363
|
+
compilerOptions: {
|
|
364
|
+
allowJs: true,
|
|
365
|
+
checkJs: false,
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Add a file to the project
|
|
371
|
+
*/
|
|
372
|
+
addFile(path, content) {
|
|
373
|
+
return this.project.createSourceFile(path, content, { overwrite: true });
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Get or create a source file
|
|
377
|
+
*/
|
|
378
|
+
getSourceFile(path, content) {
|
|
379
|
+
let sourceFile = this.project.getSourceFile(path);
|
|
380
|
+
if (!sourceFile && content) {
|
|
381
|
+
sourceFile = this.addFile(path, content);
|
|
382
|
+
}
|
|
383
|
+
return sourceFile;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Find a function by name
|
|
387
|
+
*/
|
|
388
|
+
findFunction(sourceFile, name) {
|
|
389
|
+
// Try function declarations
|
|
390
|
+
const funcDecl = sourceFile.getFunction(name);
|
|
391
|
+
if (funcDecl)
|
|
392
|
+
return funcDecl;
|
|
393
|
+
// Try variable declarations with arrow functions
|
|
394
|
+
const varDecl = sourceFile.getVariableDeclaration(name);
|
|
395
|
+
if (varDecl)
|
|
396
|
+
return varDecl;
|
|
397
|
+
// Try method declarations in classes
|
|
398
|
+
for (const classDecl of sourceFile.getClasses()) {
|
|
399
|
+
const method = classDecl.getMethod(name);
|
|
400
|
+
if (method)
|
|
401
|
+
return method;
|
|
402
|
+
}
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Find a class by name
|
|
407
|
+
*/
|
|
408
|
+
findClass(sourceFile, name) {
|
|
409
|
+
return sourceFile.getClass(name);
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Rename a symbol across the project
|
|
413
|
+
*/
|
|
414
|
+
renameSymbol(sourceFile, oldName, newName) {
|
|
415
|
+
const identifier = sourceFile.getDescendantsOfKind(SyntaxKind.Identifier)
|
|
416
|
+
.find(id => id.getText() === oldName);
|
|
417
|
+
if (identifier) {
|
|
418
|
+
identifier.rename(newName);
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Get the modified content of a file
|
|
425
|
+
*/
|
|
426
|
+
getContent(path) {
|
|
427
|
+
return this.project.getSourceFile(path)?.getFullText();
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Save all changes
|
|
431
|
+
*/
|
|
432
|
+
async saveAll() {
|
|
433
|
+
await this.project.save();
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Create a code editor instance
|
|
438
|
+
*/
|
|
439
|
+
export function createEditor() {
|
|
440
|
+
return new ASTEditor();
|
|
441
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Operations using simple-git
|
|
3
|
+
* Reliable git operations leveraging the native Git binary
|
|
4
|
+
*/
|
|
5
|
+
import { SimpleGit, StatusResult, LogResult, BranchSummary } from 'simple-git';
|
|
6
|
+
export interface GitConfig {
|
|
7
|
+
cwd?: string;
|
|
8
|
+
timeout?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface CommitOptions {
|
|
11
|
+
message: string;
|
|
12
|
+
files?: string[];
|
|
13
|
+
amend?: boolean;
|
|
14
|
+
noVerify?: boolean;
|
|
15
|
+
author?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface DiffOptions {
|
|
18
|
+
staged?: boolean;
|
|
19
|
+
file?: string;
|
|
20
|
+
from?: string;
|
|
21
|
+
to?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* GitManager - Wrapper around simple-git for reliable git operations
|
|
25
|
+
*/
|
|
26
|
+
export declare class GitManager {
|
|
27
|
+
private git;
|
|
28
|
+
private cwd;
|
|
29
|
+
constructor(config?: GitConfig);
|
|
30
|
+
/**
|
|
31
|
+
* Check if current directory is a git repository
|
|
32
|
+
*/
|
|
33
|
+
isRepo(): Promise<boolean>;
|
|
34
|
+
/**
|
|
35
|
+
* Initialize a new git repository
|
|
36
|
+
*/
|
|
37
|
+
init(): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Get repository status
|
|
40
|
+
*/
|
|
41
|
+
status(): Promise<StatusResult>;
|
|
42
|
+
/**
|
|
43
|
+
* Get current branch name
|
|
44
|
+
*/
|
|
45
|
+
currentBranch(): Promise<string | null>;
|
|
46
|
+
/**
|
|
47
|
+
* Get all branches
|
|
48
|
+
*/
|
|
49
|
+
branches(): Promise<BranchSummary>;
|
|
50
|
+
/**
|
|
51
|
+
* Checkout a branch
|
|
52
|
+
*/
|
|
53
|
+
checkout(branch: string, create?: boolean): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Stage files
|
|
56
|
+
*/
|
|
57
|
+
add(files: string | string[]): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Stage all changes
|
|
60
|
+
*/
|
|
61
|
+
addAll(): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Commit changes
|
|
64
|
+
*/
|
|
65
|
+
commit(options: CommitOptions): Promise<{
|
|
66
|
+
hash: string;
|
|
67
|
+
message: string;
|
|
68
|
+
} | null>;
|
|
69
|
+
/**
|
|
70
|
+
* Get diff
|
|
71
|
+
*/
|
|
72
|
+
diff(options?: DiffOptions): Promise<string>;
|
|
73
|
+
/**
|
|
74
|
+
* Get staged diff
|
|
75
|
+
*/
|
|
76
|
+
stagedDiff(): Promise<string>;
|
|
77
|
+
/**
|
|
78
|
+
* Get commit log
|
|
79
|
+
*/
|
|
80
|
+
log(maxCount?: number): Promise<LogResult>;
|
|
81
|
+
/**
|
|
82
|
+
* Get the last commit
|
|
83
|
+
*/
|
|
84
|
+
lastCommit(): Promise<{
|
|
85
|
+
hash: string;
|
|
86
|
+
message: string;
|
|
87
|
+
author: string;
|
|
88
|
+
date: string;
|
|
89
|
+
} | null>;
|
|
90
|
+
/**
|
|
91
|
+
* Undo the last commit (soft reset)
|
|
92
|
+
*/
|
|
93
|
+
undoLastCommit(): Promise<boolean>;
|
|
94
|
+
/**
|
|
95
|
+
* Hard reset to a commit
|
|
96
|
+
*/
|
|
97
|
+
hardReset(commit?: string): Promise<void>;
|
|
98
|
+
/**
|
|
99
|
+
* Stash changes
|
|
100
|
+
*/
|
|
101
|
+
stash(message?: string): Promise<void>;
|
|
102
|
+
/**
|
|
103
|
+
* Pop stash
|
|
104
|
+
*/
|
|
105
|
+
stashPop(): Promise<void>;
|
|
106
|
+
/**
|
|
107
|
+
* Get list of tracked files
|
|
108
|
+
*/
|
|
109
|
+
trackedFiles(): Promise<string[]>;
|
|
110
|
+
/**
|
|
111
|
+
* Get list of changed files (staged + unstaged)
|
|
112
|
+
*/
|
|
113
|
+
changedFiles(): Promise<string[]>;
|
|
114
|
+
/**
|
|
115
|
+
* Check if a file is ignored
|
|
116
|
+
*/
|
|
117
|
+
isIgnored(file: string): Promise<boolean>;
|
|
118
|
+
/**
|
|
119
|
+
* Get blame for a file
|
|
120
|
+
*/
|
|
121
|
+
blame(file: string): Promise<string>;
|
|
122
|
+
/**
|
|
123
|
+
* Show a specific commit
|
|
124
|
+
*/
|
|
125
|
+
show(commit: string): Promise<string>;
|
|
126
|
+
/**
|
|
127
|
+
* Get the root directory of the repository
|
|
128
|
+
*/
|
|
129
|
+
rootDir(): Promise<string | null>;
|
|
130
|
+
/**
|
|
131
|
+
* Pull changes from remote
|
|
132
|
+
*/
|
|
133
|
+
pull(remote?: string, branch?: string): Promise<void>;
|
|
134
|
+
/**
|
|
135
|
+
* Push changes to remote
|
|
136
|
+
*/
|
|
137
|
+
push(remote?: string, branch?: string, options?: {
|
|
138
|
+
setUpstream?: boolean;
|
|
139
|
+
}): Promise<void>;
|
|
140
|
+
/**
|
|
141
|
+
* Create a new branch from current HEAD
|
|
142
|
+
*/
|
|
143
|
+
createBranch(name: string): Promise<void>;
|
|
144
|
+
/**
|
|
145
|
+
* Delete a branch
|
|
146
|
+
*/
|
|
147
|
+
deleteBranch(name: string, force?: boolean): Promise<void>;
|
|
148
|
+
/**
|
|
149
|
+
* Merge a branch into current branch
|
|
150
|
+
*/
|
|
151
|
+
merge(branch: string): Promise<void>;
|
|
152
|
+
/**
|
|
153
|
+
* Get the raw simple-git instance for advanced operations
|
|
154
|
+
*/
|
|
155
|
+
raw(): SimpleGit;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Get or create a GitManager instance
|
|
159
|
+
*/
|
|
160
|
+
export declare function getGitManager(config?: GitConfig): GitManager;
|
|
161
|
+
/**
|
|
162
|
+
* Generate AI commit message from diff
|
|
163
|
+
*/
|
|
164
|
+
export declare function generateCommitMessage(diff: string, generateFn: (prompt: string) => Promise<string>, context?: string): Promise<string>;
|