codeep 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/LICENSE +201 -0
- package/README.md +576 -0
- package/dist/api/index.d.ts +8 -0
- package/dist/api/index.js +421 -0
- package/dist/app.d.ts +2 -0
- package/dist/app.js +1406 -0
- package/dist/components/AgentProgress.d.ts +33 -0
- package/dist/components/AgentProgress.js +97 -0
- package/dist/components/Export.d.ts +8 -0
- package/dist/components/Export.js +27 -0
- package/dist/components/Help.d.ts +2 -0
- package/dist/components/Help.js +3 -0
- package/dist/components/Input.d.ts +9 -0
- package/dist/components/Input.js +89 -0
- package/dist/components/Loading.d.ts +9 -0
- package/dist/components/Loading.js +31 -0
- package/dist/components/Login.d.ts +7 -0
- package/dist/components/Login.js +77 -0
- package/dist/components/Logo.d.ts +8 -0
- package/dist/components/Logo.js +89 -0
- package/dist/components/LogoutPicker.d.ts +8 -0
- package/dist/components/LogoutPicker.js +61 -0
- package/dist/components/Message.d.ts +10 -0
- package/dist/components/Message.js +234 -0
- package/dist/components/MessageList.d.ts +10 -0
- package/dist/components/MessageList.js +8 -0
- package/dist/components/ProjectPermission.d.ts +7 -0
- package/dist/components/ProjectPermission.js +52 -0
- package/dist/components/Search.d.ts +10 -0
- package/dist/components/Search.js +30 -0
- package/dist/components/SessionPicker.d.ts +9 -0
- package/dist/components/SessionPicker.js +88 -0
- package/dist/components/Sessions.d.ts +12 -0
- package/dist/components/Sessions.js +102 -0
- package/dist/components/Settings.d.ts +7 -0
- package/dist/components/Settings.js +162 -0
- package/dist/components/Status.d.ts +2 -0
- package/dist/components/Status.js +12 -0
- package/dist/config/config.test.d.ts +1 -0
- package/dist/config/config.test.js +157 -0
- package/dist/config/index.d.ts +121 -0
- package/dist/config/index.js +555 -0
- package/dist/config/providers.d.ts +43 -0
- package/dist/config/providers.js +82 -0
- package/dist/config/providers.test.d.ts +1 -0
- package/dist/config/providers.test.js +132 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +38 -0
- package/dist/utils/agent.d.ts +37 -0
- package/dist/utils/agent.js +627 -0
- package/dist/utils/codeReview.d.ts +36 -0
- package/dist/utils/codeReview.js +390 -0
- package/dist/utils/context.d.ts +49 -0
- package/dist/utils/context.js +216 -0
- package/dist/utils/diffPreview.d.ts +57 -0
- package/dist/utils/diffPreview.js +335 -0
- package/dist/utils/export.d.ts +19 -0
- package/dist/utils/export.js +94 -0
- package/dist/utils/git.d.ts +85 -0
- package/dist/utils/git.js +399 -0
- package/dist/utils/git.test.d.ts +1 -0
- package/dist/utils/git.test.js +193 -0
- package/dist/utils/history.d.ts +93 -0
- package/dist/utils/history.js +348 -0
- package/dist/utils/interactive.d.ts +34 -0
- package/dist/utils/interactive.js +206 -0
- package/dist/utils/keychain.d.ts +17 -0
- package/dist/utils/keychain.js +160 -0
- package/dist/utils/learning.d.ts +89 -0
- package/dist/utils/learning.js +330 -0
- package/dist/utils/logger.d.ts +33 -0
- package/dist/utils/logger.js +130 -0
- package/dist/utils/project.d.ts +86 -0
- package/dist/utils/project.js +415 -0
- package/dist/utils/project.test.d.ts +1 -0
- package/dist/utils/project.test.js +212 -0
- package/dist/utils/ratelimit.d.ts +26 -0
- package/dist/utils/ratelimit.js +132 -0
- package/dist/utils/ratelimit.test.d.ts +1 -0
- package/dist/utils/ratelimit.test.js +131 -0
- package/dist/utils/retry.d.ts +28 -0
- package/dist/utils/retry.js +109 -0
- package/dist/utils/retry.test.d.ts +1 -0
- package/dist/utils/retry.test.js +163 -0
- package/dist/utils/search.d.ts +11 -0
- package/dist/utils/search.js +29 -0
- package/dist/utils/shell.d.ts +45 -0
- package/dist/utils/shell.js +242 -0
- package/dist/utils/skills.d.ts +144 -0
- package/dist/utils/skills.js +1137 -0
- package/dist/utils/smartContext.d.ts +29 -0
- package/dist/utils/smartContext.js +441 -0
- package/dist/utils/tools.d.ts +224 -0
- package/dist/utils/tools.js +731 -0
- package/dist/utils/update.d.ts +22 -0
- package/dist/utils/update.js +128 -0
- package/dist/utils/validation.d.ts +28 -0
- package/dist/utils/validation.js +141 -0
- package/dist/utils/validation.test.d.ts +1 -0
- package/dist/utils/validation.test.js +164 -0
- package/dist/utils/verify.d.ts +78 -0
- package/dist/utils/verify.js +464 -0
- package/package.json +68 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import { execSync, spawnSync } from 'child_process';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
/**
|
|
5
|
+
* Check if current directory is a git repository
|
|
6
|
+
*/
|
|
7
|
+
export function isGitRepository(cwd = process.cwd()) {
|
|
8
|
+
try {
|
|
9
|
+
const gitDir = join(cwd, '.git');
|
|
10
|
+
if (existsSync(gitDir))
|
|
11
|
+
return true;
|
|
12
|
+
// Check if we're inside a git repo (not necessarily at root)
|
|
13
|
+
execSync('git rev-parse --git-dir', { cwd, stdio: 'ignore' });
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Get current git status
|
|
22
|
+
*/
|
|
23
|
+
export function getGitStatus(cwd = process.cwd()) {
|
|
24
|
+
if (!isGitRepository(cwd)) {
|
|
25
|
+
return { isRepo: false };
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
// Get current branch
|
|
29
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
30
|
+
cwd,
|
|
31
|
+
encoding: 'utf-8'
|
|
32
|
+
}).trim();
|
|
33
|
+
// Check for changes
|
|
34
|
+
const status = execSync('git status --porcelain', {
|
|
35
|
+
cwd,
|
|
36
|
+
encoding: 'utf-8'
|
|
37
|
+
});
|
|
38
|
+
const hasChanges = status.trim().length > 0;
|
|
39
|
+
// Check ahead/behind
|
|
40
|
+
let ahead = 0;
|
|
41
|
+
let behind = 0;
|
|
42
|
+
try {
|
|
43
|
+
const counts = execSync('git rev-list --left-right --count @{u}...HEAD', {
|
|
44
|
+
cwd,
|
|
45
|
+
encoding: 'utf-8'
|
|
46
|
+
}).trim();
|
|
47
|
+
const [behindStr, aheadStr] = counts.split('\t');
|
|
48
|
+
behind = parseInt(behindStr) || 0;
|
|
49
|
+
ahead = parseInt(aheadStr) || 0;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// No upstream branch
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
isRepo: true,
|
|
56
|
+
branch,
|
|
57
|
+
hasChanges,
|
|
58
|
+
ahead,
|
|
59
|
+
behind,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
return {
|
|
64
|
+
isRepo: true,
|
|
65
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get git diff (staged or unstaged)
|
|
71
|
+
*/
|
|
72
|
+
export function getGitDiff(staged = false, cwd = process.cwd()) {
|
|
73
|
+
if (!isGitRepository(cwd)) {
|
|
74
|
+
return {
|
|
75
|
+
success: false,
|
|
76
|
+
diff: '',
|
|
77
|
+
error: 'Not a git repository',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const command = staged ? 'git diff --cached' : 'git diff';
|
|
82
|
+
const diff = execSync(command, {
|
|
83
|
+
cwd,
|
|
84
|
+
encoding: 'utf-8',
|
|
85
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large diffs
|
|
86
|
+
});
|
|
87
|
+
if (!diff.trim()) {
|
|
88
|
+
return {
|
|
89
|
+
success: true,
|
|
90
|
+
diff: '',
|
|
91
|
+
error: staged ? 'No staged changes' : 'No unstaged changes',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
success: true,
|
|
96
|
+
diff: diff.trim(),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
return {
|
|
101
|
+
success: false,
|
|
102
|
+
diff: '',
|
|
103
|
+
error: error instanceof Error ? error.message : 'Failed to get diff',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get list of changed files
|
|
109
|
+
*/
|
|
110
|
+
export function getChangedFiles(cwd = process.cwd()) {
|
|
111
|
+
if (!isGitRepository(cwd)) {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const output = execSync('git status --porcelain', {
|
|
116
|
+
cwd,
|
|
117
|
+
encoding: 'utf-8'
|
|
118
|
+
});
|
|
119
|
+
return output
|
|
120
|
+
.trim()
|
|
121
|
+
.split('\n')
|
|
122
|
+
.filter(line => line.trim())
|
|
123
|
+
.map(line => {
|
|
124
|
+
// Format: "XY filename" where XY are status codes
|
|
125
|
+
return line.substring(3).trim();
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Generate commit message suggestion based on diff
|
|
134
|
+
*/
|
|
135
|
+
export function suggestCommitMessage(diff) {
|
|
136
|
+
// Simple heuristics for commit message suggestions
|
|
137
|
+
const lines = diff.split('\n');
|
|
138
|
+
const additions = lines.filter(l => l.startsWith('+')).length;
|
|
139
|
+
const deletions = lines.filter(l => l.startsWith('-')).length;
|
|
140
|
+
// Look for common patterns
|
|
141
|
+
if (diff.includes('new file mode')) {
|
|
142
|
+
return 'feat: add new files';
|
|
143
|
+
}
|
|
144
|
+
if (diff.includes('deleted file mode')) {
|
|
145
|
+
return 'chore: remove files';
|
|
146
|
+
}
|
|
147
|
+
if (diff.includes('package.json') || diff.includes('package-lock.json')) {
|
|
148
|
+
return 'chore: update dependencies';
|
|
149
|
+
}
|
|
150
|
+
if (diff.includes('README') || diff.includes('.md')) {
|
|
151
|
+
return 'docs: update documentation';
|
|
152
|
+
}
|
|
153
|
+
if (diff.includes('test') || diff.includes('spec')) {
|
|
154
|
+
return 'test: update tests';
|
|
155
|
+
}
|
|
156
|
+
// Generic based on size
|
|
157
|
+
if (additions > deletions * 2) {
|
|
158
|
+
return 'feat: add functionality';
|
|
159
|
+
}
|
|
160
|
+
if (deletions > additions * 2) {
|
|
161
|
+
return 'refactor: remove code';
|
|
162
|
+
}
|
|
163
|
+
return 'chore: update code';
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Create a commit with the given message
|
|
167
|
+
*/
|
|
168
|
+
export function createCommit(message, cwd = process.cwd()) {
|
|
169
|
+
if (!isGitRepository(cwd)) {
|
|
170
|
+
return {
|
|
171
|
+
success: false,
|
|
172
|
+
error: 'Not a git repository',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
// Check if there are staged changes
|
|
177
|
+
const staged = execSync('git diff --cached --name-only', {
|
|
178
|
+
cwd,
|
|
179
|
+
encoding: 'utf-8'
|
|
180
|
+
}).trim();
|
|
181
|
+
if (!staged) {
|
|
182
|
+
return {
|
|
183
|
+
success: false,
|
|
184
|
+
error: 'No staged changes to commit',
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
// Create commit using spawnSync to prevent command injection
|
|
188
|
+
const result = spawnSync('git', ['commit', '-m', message], {
|
|
189
|
+
cwd,
|
|
190
|
+
encoding: 'utf-8',
|
|
191
|
+
stdio: 'pipe',
|
|
192
|
+
});
|
|
193
|
+
if (result.status !== 0) {
|
|
194
|
+
throw new Error(result.stderr || 'Commit failed');
|
|
195
|
+
}
|
|
196
|
+
// Get commit hash
|
|
197
|
+
const hash = execSync('git rev-parse --short HEAD', {
|
|
198
|
+
cwd,
|
|
199
|
+
encoding: 'utf-8'
|
|
200
|
+
}).trim();
|
|
201
|
+
return {
|
|
202
|
+
success: true,
|
|
203
|
+
hash,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
return {
|
|
208
|
+
success: false,
|
|
209
|
+
error: error instanceof Error ? error.message : 'Commit failed',
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Stage all changes
|
|
215
|
+
*/
|
|
216
|
+
export function stageAll(cwd = process.cwd()) {
|
|
217
|
+
if (!isGitRepository(cwd)) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
execSync('git add -A', { cwd, stdio: 'ignore' });
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Format git diff for display
|
|
230
|
+
*/
|
|
231
|
+
export function formatDiffForDisplay(diff, maxLines = 50) {
|
|
232
|
+
const lines = diff.split('\n');
|
|
233
|
+
if (lines.length <= maxLines) {
|
|
234
|
+
return diff;
|
|
235
|
+
}
|
|
236
|
+
const truncated = lines.slice(0, maxLines).join('\n');
|
|
237
|
+
const remaining = lines.length - maxLines;
|
|
238
|
+
return `${truncated}\n\n... (${remaining} more lines, showing first ${maxLines})`;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Create a new branch
|
|
242
|
+
*/
|
|
243
|
+
export function createBranch(branchName, cwd = process.cwd()) {
|
|
244
|
+
if (!isGitRepository(cwd)) {
|
|
245
|
+
return { success: false, error: 'Not a git repository' };
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
// Check if branch already exists
|
|
249
|
+
const branches = execSync('git branch --list', { cwd, encoding: 'utf-8' });
|
|
250
|
+
if (branches.includes(branchName)) {
|
|
251
|
+
return { success: false, error: `Branch '${branchName}' already exists` };
|
|
252
|
+
}
|
|
253
|
+
execSync(`git checkout -b ${branchName}`, { cwd, stdio: 'ignore' });
|
|
254
|
+
return { success: true };
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
return {
|
|
258
|
+
success: false,
|
|
259
|
+
error: error instanceof Error ? error.message : 'Failed to create branch',
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Switch to a branch
|
|
265
|
+
*/
|
|
266
|
+
export function switchBranch(branchName, cwd = process.cwd()) {
|
|
267
|
+
if (!isGitRepository(cwd)) {
|
|
268
|
+
return { success: false, error: 'Not a git repository' };
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
execSync(`git checkout ${branchName}`, { cwd, stdio: 'ignore' });
|
|
272
|
+
return { success: true };
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
return {
|
|
276
|
+
success: false,
|
|
277
|
+
error: error instanceof Error ? error.message : 'Failed to switch branch',
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Generate a commit message based on agent actions
|
|
283
|
+
*/
|
|
284
|
+
export function generateCommitMessage(prompt, actions) {
|
|
285
|
+
// Analyze actions to determine commit type
|
|
286
|
+
const hasWrites = actions.some(a => a.type === 'write');
|
|
287
|
+
const hasEdits = actions.some(a => a.type === 'edit');
|
|
288
|
+
const hasDeletes = actions.some(a => a.type === 'delete');
|
|
289
|
+
const hasCommands = actions.some(a => a.type === 'command');
|
|
290
|
+
// Determine prefix
|
|
291
|
+
let prefix = 'chore';
|
|
292
|
+
// Check prompt for common patterns
|
|
293
|
+
const promptLower = prompt.toLowerCase();
|
|
294
|
+
if (promptLower.includes('fix') || promptLower.includes('bug')) {
|
|
295
|
+
prefix = 'fix';
|
|
296
|
+
}
|
|
297
|
+
else if (promptLower.includes('add') || promptLower.includes('create') || promptLower.includes('implement')) {
|
|
298
|
+
prefix = 'feat';
|
|
299
|
+
}
|
|
300
|
+
else if (promptLower.includes('refactor') || promptLower.includes('clean')) {
|
|
301
|
+
prefix = 'refactor';
|
|
302
|
+
}
|
|
303
|
+
else if (promptLower.includes('test')) {
|
|
304
|
+
prefix = 'test';
|
|
305
|
+
}
|
|
306
|
+
else if (promptLower.includes('doc') || promptLower.includes('readme')) {
|
|
307
|
+
prefix = 'docs';
|
|
308
|
+
}
|
|
309
|
+
else if (hasWrites && !hasEdits) {
|
|
310
|
+
prefix = 'feat';
|
|
311
|
+
}
|
|
312
|
+
else if (hasDeletes && !hasWrites) {
|
|
313
|
+
prefix = 'refactor';
|
|
314
|
+
}
|
|
315
|
+
// Generate message body from prompt
|
|
316
|
+
let body = prompt
|
|
317
|
+
.replace(/^(please\s+)?/i, '')
|
|
318
|
+
.replace(/[.!?]+$/, '')
|
|
319
|
+
.trim();
|
|
320
|
+
// Truncate if too long
|
|
321
|
+
if (body.length > 50) {
|
|
322
|
+
body = body.substring(0, 47) + '...';
|
|
323
|
+
}
|
|
324
|
+
// Make first letter lowercase
|
|
325
|
+
body = body.charAt(0).toLowerCase() + body.slice(1);
|
|
326
|
+
return `${prefix}: ${body}`;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Auto-commit agent changes
|
|
330
|
+
*/
|
|
331
|
+
export function autoCommitAgentChanges(prompt, actions, cwd = process.cwd()) {
|
|
332
|
+
if (!isGitRepository(cwd)) {
|
|
333
|
+
return { success: false, error: 'Not a git repository' };
|
|
334
|
+
}
|
|
335
|
+
// Check if there are any file changes
|
|
336
|
+
const fileActions = actions.filter(a => a.type === 'write' || a.type === 'edit' || a.type === 'delete' || a.type === 'mkdir');
|
|
337
|
+
if (fileActions.length === 0) {
|
|
338
|
+
return { success: false, error: 'No file changes to commit' };
|
|
339
|
+
}
|
|
340
|
+
// Check for actual git changes
|
|
341
|
+
const status = getGitStatus(cwd);
|
|
342
|
+
if (!status.hasChanges) {
|
|
343
|
+
return { success: false, error: 'No changes detected by git' };
|
|
344
|
+
}
|
|
345
|
+
// Stage all changes
|
|
346
|
+
if (!stageAll(cwd)) {
|
|
347
|
+
return { success: false, error: 'Failed to stage changes' };
|
|
348
|
+
}
|
|
349
|
+
// Generate commit message
|
|
350
|
+
const message = generateCommitMessage(prompt, actions);
|
|
351
|
+
// Create commit
|
|
352
|
+
return createCommit(message, cwd);
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Generate branch name from prompt
|
|
356
|
+
*/
|
|
357
|
+
export function generateBranchName(prompt) {
|
|
358
|
+
// Clean up prompt
|
|
359
|
+
let name = prompt
|
|
360
|
+
.toLowerCase()
|
|
361
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
362
|
+
.replace(/\s+/g, '-')
|
|
363
|
+
.replace(/-+/g, '-')
|
|
364
|
+
.replace(/^-|-$/g, '')
|
|
365
|
+
.substring(0, 40);
|
|
366
|
+
// Add prefix
|
|
367
|
+
const prefix = 'agent';
|
|
368
|
+
const timestamp = Date.now().toString(36).slice(-4);
|
|
369
|
+
return `${prefix}/${name}-${timestamp}`;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Create branch and commit agent changes
|
|
373
|
+
*/
|
|
374
|
+
export function createBranchAndCommit(prompt, actions, cwd = process.cwd()) {
|
|
375
|
+
if (!isGitRepository(cwd)) {
|
|
376
|
+
return { success: false, error: 'Not a git repository' };
|
|
377
|
+
}
|
|
378
|
+
// Generate branch name
|
|
379
|
+
const branchName = generateBranchName(prompt);
|
|
380
|
+
// Create branch
|
|
381
|
+
const branchResult = createBranch(branchName, cwd);
|
|
382
|
+
if (!branchResult.success) {
|
|
383
|
+
return { success: false, error: branchResult.error };
|
|
384
|
+
}
|
|
385
|
+
// Commit changes
|
|
386
|
+
const commitResult = autoCommitAgentChanges(prompt, actions, cwd);
|
|
387
|
+
if (!commitResult.success) {
|
|
388
|
+
return {
|
|
389
|
+
success: false,
|
|
390
|
+
branch: branchName,
|
|
391
|
+
error: commitResult.error
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
success: true,
|
|
396
|
+
branch: branchName,
|
|
397
|
+
hash: commitResult.hash,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import { mkdirSync, rmSync, writeFileSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
import { isGitRepository, getGitStatus, getGitDiff, getChangedFiles, suggestCommitMessage, createCommit, stageAll, formatDiffForDisplay, } from './git';
|
|
7
|
+
// Create a temp directory for git tests
|
|
8
|
+
const TEST_DIR = join(tmpdir(), 'codeep-git-test-' + Date.now());
|
|
9
|
+
const NON_GIT_DIR = join(tmpdir(), 'codeep-non-git-test-' + Date.now());
|
|
10
|
+
describe('git utilities', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
// Create test directories
|
|
13
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
14
|
+
mkdirSync(NON_GIT_DIR, { recursive: true });
|
|
15
|
+
// Initialize git repo in TEST_DIR
|
|
16
|
+
execSync('git init', { cwd: TEST_DIR, stdio: 'ignore' });
|
|
17
|
+
execSync('git config user.email "test@test.com"', { cwd: TEST_DIR, stdio: 'ignore' });
|
|
18
|
+
execSync('git config user.name "Test User"', { cwd: TEST_DIR, stdio: 'ignore' });
|
|
19
|
+
});
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
// Cleanup
|
|
22
|
+
try {
|
|
23
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
24
|
+
rmSync(NON_GIT_DIR, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
catch { }
|
|
27
|
+
});
|
|
28
|
+
describe('isGitRepository', () => {
|
|
29
|
+
it('should return true for git repository', () => {
|
|
30
|
+
expect(isGitRepository(TEST_DIR)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
it('should return false for non-git directory', () => {
|
|
33
|
+
expect(isGitRepository(NON_GIT_DIR)).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
it('should return false for non-existent directory', () => {
|
|
36
|
+
expect(isGitRepository('/non/existent/path')).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe('getGitStatus', () => {
|
|
40
|
+
it('should return isRepo: false for non-git directory', () => {
|
|
41
|
+
const status = getGitStatus(NON_GIT_DIR);
|
|
42
|
+
expect(status.isRepo).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
it('should return correct status for git repo', () => {
|
|
45
|
+
// Create initial commit so we have a branch
|
|
46
|
+
writeFileSync(join(TEST_DIR, 'test.txt'), 'hello');
|
|
47
|
+
execSync('git add .', { cwd: TEST_DIR, stdio: 'ignore' });
|
|
48
|
+
execSync('git commit -m "initial"', { cwd: TEST_DIR, stdio: 'ignore' });
|
|
49
|
+
const status = getGitStatus(TEST_DIR);
|
|
50
|
+
expect(status.isRepo).toBe(true);
|
|
51
|
+
expect(status.branch).toBeDefined();
|
|
52
|
+
expect(status.hasChanges).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
it('should detect changes', () => {
|
|
55
|
+
// Create initial commit
|
|
56
|
+
writeFileSync(join(TEST_DIR, 'test.txt'), 'hello');
|
|
57
|
+
execSync('git add .', { cwd: TEST_DIR, stdio: 'ignore' });
|
|
58
|
+
execSync('git commit -m "initial"', { cwd: TEST_DIR, stdio: 'ignore' });
|
|
59
|
+
// Make a change
|
|
60
|
+
writeFileSync(join(TEST_DIR, 'test.txt'), 'hello world');
|
|
61
|
+
const status = getGitStatus(TEST_DIR);
|
|
62
|
+
expect(status.hasChanges).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe('getGitDiff', () => {
|
|
66
|
+
it('should return error for non-git directory', () => {
|
|
67
|
+
const result = getGitDiff(false, NON_GIT_DIR);
|
|
68
|
+
expect(result.success).toBe(false);
|
|
69
|
+
expect(result.error).toBe('Not a git repository');
|
|
70
|
+
});
|
|
71
|
+
it('should return empty diff when no changes', () => {
|
|
72
|
+
writeFileSync(join(TEST_DIR, 'test.txt'), 'hello');
|
|
73
|
+
execSync('git add .', { cwd: TEST_DIR, stdio: 'ignore' });
|
|
74
|
+
execSync('git commit -m "initial"', { cwd: TEST_DIR, stdio: 'ignore' });
|
|
75
|
+
const result = getGitDiff(false, TEST_DIR);
|
|
76
|
+
expect(result.success).toBe(true);
|
|
77
|
+
expect(result.diff).toBe('');
|
|
78
|
+
});
|
|
79
|
+
it('should return diff for unstaged changes', () => {
|
|
80
|
+
writeFileSync(join(TEST_DIR, 'test.txt'), 'hello');
|
|
81
|
+
execSync('git add .', { cwd: TEST_DIR, stdio: 'ignore' });
|
|
82
|
+
execSync('git commit -m "initial"', { cwd: TEST_DIR, stdio: 'ignore' });
|
|
83
|
+
writeFileSync(join(TEST_DIR, 'test.txt'), 'hello world');
|
|
84
|
+
const result = getGitDiff(false, TEST_DIR);
|
|
85
|
+
expect(result.success).toBe(true);
|
|
86
|
+
expect(result.diff).toContain('hello world');
|
|
87
|
+
});
|
|
88
|
+
it('should return diff for staged changes', () => {
|
|
89
|
+
writeFileSync(join(TEST_DIR, 'test.txt'), 'hello');
|
|
90
|
+
execSync('git add .', { cwd: TEST_DIR, stdio: 'ignore' });
|
|
91
|
+
execSync('git commit -m "initial"', { cwd: TEST_DIR, stdio: 'ignore' });
|
|
92
|
+
writeFileSync(join(TEST_DIR, 'test.txt'), 'hello world');
|
|
93
|
+
execSync('git add .', { cwd: TEST_DIR, stdio: 'ignore' });
|
|
94
|
+
const result = getGitDiff(true, TEST_DIR);
|
|
95
|
+
expect(result.success).toBe(true);
|
|
96
|
+
expect(result.diff).toContain('hello world');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe('getChangedFiles', () => {
|
|
100
|
+
it('should return empty array for non-git directory', () => {
|
|
101
|
+
expect(getChangedFiles(NON_GIT_DIR)).toEqual([]);
|
|
102
|
+
});
|
|
103
|
+
it('should return changed files', () => {
|
|
104
|
+
writeFileSync(join(TEST_DIR, 'file1.txt'), 'content1');
|
|
105
|
+
writeFileSync(join(TEST_DIR, 'file2.txt'), 'content2');
|
|
106
|
+
const files = getChangedFiles(TEST_DIR);
|
|
107
|
+
expect(files).toContain('file1.txt');
|
|
108
|
+
expect(files).toContain('file2.txt');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('suggestCommitMessage', () => {
|
|
112
|
+
it('should suggest feat for new files', () => {
|
|
113
|
+
const diff = 'new file mode 100644\n+++ b/newfile.ts';
|
|
114
|
+
expect(suggestCommitMessage(diff)).toBe('feat: add new files');
|
|
115
|
+
});
|
|
116
|
+
it('should suggest chore for deleted files', () => {
|
|
117
|
+
const diff = 'deleted file mode 100644\n--- a/oldfile.ts';
|
|
118
|
+
expect(suggestCommitMessage(diff)).toBe('chore: remove files');
|
|
119
|
+
});
|
|
120
|
+
it('should suggest chore for package.json changes', () => {
|
|
121
|
+
const diff = '+++ b/package.json\n+ "new-dep": "1.0.0"';
|
|
122
|
+
expect(suggestCommitMessage(diff)).toBe('chore: update dependencies');
|
|
123
|
+
});
|
|
124
|
+
it('should suggest docs for README changes', () => {
|
|
125
|
+
const diff = '+++ b/README.md\n+ New documentation';
|
|
126
|
+
expect(suggestCommitMessage(diff)).toBe('docs: update documentation');
|
|
127
|
+
});
|
|
128
|
+
it('should suggest test for test file changes', () => {
|
|
129
|
+
const diff = '+++ b/utils.test.ts\n+ test case';
|
|
130
|
+
expect(suggestCommitMessage(diff)).toBe('test: update tests');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
describe('createCommit', () => {
|
|
134
|
+
it('should return error for non-git directory', () => {
|
|
135
|
+
const result = createCommit('test message', NON_GIT_DIR);
|
|
136
|
+
expect(result.success).toBe(false);
|
|
137
|
+
expect(result.error).toBe('Not a git repository');
|
|
138
|
+
});
|
|
139
|
+
it('should return error when no staged changes', () => {
|
|
140
|
+
const result = createCommit('test message', TEST_DIR);
|
|
141
|
+
expect(result.success).toBe(false);
|
|
142
|
+
expect(result.error).toBe('No staged changes to commit');
|
|
143
|
+
});
|
|
144
|
+
it('should create commit successfully', () => {
|
|
145
|
+
writeFileSync(join(TEST_DIR, 'test.txt'), 'hello');
|
|
146
|
+
execSync('git add .', { cwd: TEST_DIR, stdio: 'ignore' });
|
|
147
|
+
const result = createCommit('test commit message', TEST_DIR);
|
|
148
|
+
expect(result.success).toBe(true);
|
|
149
|
+
expect(result.hash).toBeDefined();
|
|
150
|
+
expect(result.hash.length).toBeGreaterThan(0);
|
|
151
|
+
});
|
|
152
|
+
it('should handle special characters in commit message safely', () => {
|
|
153
|
+
writeFileSync(join(TEST_DIR, 'test.txt'), 'hello');
|
|
154
|
+
execSync('git add .', { cwd: TEST_DIR, stdio: 'ignore' });
|
|
155
|
+
// Test with potentially dangerous characters that could cause shell injection
|
|
156
|
+
const dangerousMessage = 'test `whoami` $(echo dangerous) ; rm -rf /';
|
|
157
|
+
const result = createCommit(dangerousMessage, TEST_DIR);
|
|
158
|
+
expect(result.success).toBe(true);
|
|
159
|
+
// Verify the commit message was stored correctly (not executed)
|
|
160
|
+
const log = execSync('git log -1 --format=%s', { cwd: TEST_DIR, encoding: 'utf-8' });
|
|
161
|
+
expect(log.trim()).toBe(dangerousMessage);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
describe('stageAll', () => {
|
|
165
|
+
it('should return false for non-git directory', () => {
|
|
166
|
+
expect(stageAll(NON_GIT_DIR)).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
it('should stage all files', () => {
|
|
169
|
+
writeFileSync(join(TEST_DIR, 'file1.txt'), 'content1');
|
|
170
|
+
writeFileSync(join(TEST_DIR, 'file2.txt'), 'content2');
|
|
171
|
+
expect(stageAll(TEST_DIR)).toBe(true);
|
|
172
|
+
// Verify files are staged
|
|
173
|
+
const staged = execSync('git diff --cached --name-only', { cwd: TEST_DIR, encoding: 'utf-8' });
|
|
174
|
+
expect(staged).toContain('file1.txt');
|
|
175
|
+
expect(staged).toContain('file2.txt');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
describe('formatDiffForDisplay', () => {
|
|
179
|
+
it('should return full diff if under limit', () => {
|
|
180
|
+
const diff = 'line1\nline2\nline3';
|
|
181
|
+
expect(formatDiffForDisplay(diff, 10)).toBe(diff);
|
|
182
|
+
});
|
|
183
|
+
it('should truncate long diffs', () => {
|
|
184
|
+
const lines = Array.from({ length: 100 }, (_, i) => `line${i}`);
|
|
185
|
+
const diff = lines.join('\n');
|
|
186
|
+
const result = formatDiffForDisplay(diff, 10);
|
|
187
|
+
expect(result).toContain('line0');
|
|
188
|
+
expect(result).toContain('line9');
|
|
189
|
+
expect(result).not.toContain('line99');
|
|
190
|
+
expect(result).toContain('90 more lines');
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent action history for undo/rollback functionality
|
|
3
|
+
*/
|
|
4
|
+
export interface ActionRecord {
|
|
5
|
+
id: string;
|
|
6
|
+
timestamp: number;
|
|
7
|
+
type: 'write' | 'edit' | 'delete' | 'mkdir' | 'command';
|
|
8
|
+
path?: string;
|
|
9
|
+
previousContent?: string;
|
|
10
|
+
previousExisted?: boolean;
|
|
11
|
+
wasDirectory?: boolean;
|
|
12
|
+
deletedContent?: string;
|
|
13
|
+
command?: string;
|
|
14
|
+
args?: string[];
|
|
15
|
+
undone?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface ActionSession {
|
|
18
|
+
id: string;
|
|
19
|
+
startTime: number;
|
|
20
|
+
endTime?: number;
|
|
21
|
+
prompt: string;
|
|
22
|
+
actions: ActionRecord[];
|
|
23
|
+
projectRoot: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Start a new action session
|
|
27
|
+
*/
|
|
28
|
+
export declare function startSession(prompt: string, projectRoot: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* End current session and save to disk
|
|
31
|
+
*/
|
|
32
|
+
export declare function endSession(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Record a file write action (before it happens)
|
|
35
|
+
*/
|
|
36
|
+
export declare function recordWrite(path: string): ActionRecord | null;
|
|
37
|
+
/**
|
|
38
|
+
* Record a file edit action (before it happens)
|
|
39
|
+
*/
|
|
40
|
+
export declare function recordEdit(path: string): ActionRecord | null;
|
|
41
|
+
/**
|
|
42
|
+
* Record a file/directory delete action (before it happens)
|
|
43
|
+
*/
|
|
44
|
+
export declare function recordDelete(path: string): ActionRecord | null;
|
|
45
|
+
/**
|
|
46
|
+
* Record a mkdir action
|
|
47
|
+
*/
|
|
48
|
+
export declare function recordMkdir(path: string): ActionRecord | null;
|
|
49
|
+
/**
|
|
50
|
+
* Record a command execution (can't be undone, but tracked)
|
|
51
|
+
*/
|
|
52
|
+
export declare function recordCommand(command: string, args: string[]): ActionRecord | null;
|
|
53
|
+
/**
|
|
54
|
+
* Get current session
|
|
55
|
+
*/
|
|
56
|
+
export declare function getCurrentSession(): ActionSession | null;
|
|
57
|
+
/**
|
|
58
|
+
* Undo the last action in current session
|
|
59
|
+
*/
|
|
60
|
+
export declare function undoLastAction(): {
|
|
61
|
+
success: boolean;
|
|
62
|
+
message: string;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Undo a specific action
|
|
66
|
+
*/
|
|
67
|
+
export declare function undoAction(action: ActionRecord): {
|
|
68
|
+
success: boolean;
|
|
69
|
+
message: string;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Undo all actions in current session
|
|
73
|
+
*/
|
|
74
|
+
export declare function undoAllActions(): {
|
|
75
|
+
success: boolean;
|
|
76
|
+
results: string[];
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Get list of recent sessions
|
|
80
|
+
*/
|
|
81
|
+
export declare function getRecentSessions(limit?: number): ActionSession[];
|
|
82
|
+
/**
|
|
83
|
+
* Get a specific session by ID
|
|
84
|
+
*/
|
|
85
|
+
export declare function getSession(sessionId: string): ActionSession | null;
|
|
86
|
+
/**
|
|
87
|
+
* Format session for display
|
|
88
|
+
*/
|
|
89
|
+
export declare function formatSession(session: ActionSession): string;
|
|
90
|
+
/**
|
|
91
|
+
* Clear all history
|
|
92
|
+
*/
|
|
93
|
+
export declare function clearHistory(): void;
|