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.
Files changed (103) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +576 -0
  3. package/dist/api/index.d.ts +8 -0
  4. package/dist/api/index.js +421 -0
  5. package/dist/app.d.ts +2 -0
  6. package/dist/app.js +1406 -0
  7. package/dist/components/AgentProgress.d.ts +33 -0
  8. package/dist/components/AgentProgress.js +97 -0
  9. package/dist/components/Export.d.ts +8 -0
  10. package/dist/components/Export.js +27 -0
  11. package/dist/components/Help.d.ts +2 -0
  12. package/dist/components/Help.js +3 -0
  13. package/dist/components/Input.d.ts +9 -0
  14. package/dist/components/Input.js +89 -0
  15. package/dist/components/Loading.d.ts +9 -0
  16. package/dist/components/Loading.js +31 -0
  17. package/dist/components/Login.d.ts +7 -0
  18. package/dist/components/Login.js +77 -0
  19. package/dist/components/Logo.d.ts +8 -0
  20. package/dist/components/Logo.js +89 -0
  21. package/dist/components/LogoutPicker.d.ts +8 -0
  22. package/dist/components/LogoutPicker.js +61 -0
  23. package/dist/components/Message.d.ts +10 -0
  24. package/dist/components/Message.js +234 -0
  25. package/dist/components/MessageList.d.ts +10 -0
  26. package/dist/components/MessageList.js +8 -0
  27. package/dist/components/ProjectPermission.d.ts +7 -0
  28. package/dist/components/ProjectPermission.js +52 -0
  29. package/dist/components/Search.d.ts +10 -0
  30. package/dist/components/Search.js +30 -0
  31. package/dist/components/SessionPicker.d.ts +9 -0
  32. package/dist/components/SessionPicker.js +88 -0
  33. package/dist/components/Sessions.d.ts +12 -0
  34. package/dist/components/Sessions.js +102 -0
  35. package/dist/components/Settings.d.ts +7 -0
  36. package/dist/components/Settings.js +162 -0
  37. package/dist/components/Status.d.ts +2 -0
  38. package/dist/components/Status.js +12 -0
  39. package/dist/config/config.test.d.ts +1 -0
  40. package/dist/config/config.test.js +157 -0
  41. package/dist/config/index.d.ts +121 -0
  42. package/dist/config/index.js +555 -0
  43. package/dist/config/providers.d.ts +43 -0
  44. package/dist/config/providers.js +82 -0
  45. package/dist/config/providers.test.d.ts +1 -0
  46. package/dist/config/providers.test.js +132 -0
  47. package/dist/index.d.ts +2 -0
  48. package/dist/index.js +38 -0
  49. package/dist/utils/agent.d.ts +37 -0
  50. package/dist/utils/agent.js +627 -0
  51. package/dist/utils/codeReview.d.ts +36 -0
  52. package/dist/utils/codeReview.js +390 -0
  53. package/dist/utils/context.d.ts +49 -0
  54. package/dist/utils/context.js +216 -0
  55. package/dist/utils/diffPreview.d.ts +57 -0
  56. package/dist/utils/diffPreview.js +335 -0
  57. package/dist/utils/export.d.ts +19 -0
  58. package/dist/utils/export.js +94 -0
  59. package/dist/utils/git.d.ts +85 -0
  60. package/dist/utils/git.js +399 -0
  61. package/dist/utils/git.test.d.ts +1 -0
  62. package/dist/utils/git.test.js +193 -0
  63. package/dist/utils/history.d.ts +93 -0
  64. package/dist/utils/history.js +348 -0
  65. package/dist/utils/interactive.d.ts +34 -0
  66. package/dist/utils/interactive.js +206 -0
  67. package/dist/utils/keychain.d.ts +17 -0
  68. package/dist/utils/keychain.js +160 -0
  69. package/dist/utils/learning.d.ts +89 -0
  70. package/dist/utils/learning.js +330 -0
  71. package/dist/utils/logger.d.ts +33 -0
  72. package/dist/utils/logger.js +130 -0
  73. package/dist/utils/project.d.ts +86 -0
  74. package/dist/utils/project.js +415 -0
  75. package/dist/utils/project.test.d.ts +1 -0
  76. package/dist/utils/project.test.js +212 -0
  77. package/dist/utils/ratelimit.d.ts +26 -0
  78. package/dist/utils/ratelimit.js +132 -0
  79. package/dist/utils/ratelimit.test.d.ts +1 -0
  80. package/dist/utils/ratelimit.test.js +131 -0
  81. package/dist/utils/retry.d.ts +28 -0
  82. package/dist/utils/retry.js +109 -0
  83. package/dist/utils/retry.test.d.ts +1 -0
  84. package/dist/utils/retry.test.js +163 -0
  85. package/dist/utils/search.d.ts +11 -0
  86. package/dist/utils/search.js +29 -0
  87. package/dist/utils/shell.d.ts +45 -0
  88. package/dist/utils/shell.js +242 -0
  89. package/dist/utils/skills.d.ts +144 -0
  90. package/dist/utils/skills.js +1137 -0
  91. package/dist/utils/smartContext.d.ts +29 -0
  92. package/dist/utils/smartContext.js +441 -0
  93. package/dist/utils/tools.d.ts +224 -0
  94. package/dist/utils/tools.js +731 -0
  95. package/dist/utils/update.d.ts +22 -0
  96. package/dist/utils/update.js +128 -0
  97. package/dist/utils/validation.d.ts +28 -0
  98. package/dist/utils/validation.js +141 -0
  99. package/dist/utils/validation.test.d.ts +1 -0
  100. package/dist/utils/validation.test.js +164 -0
  101. package/dist/utils/verify.d.ts +78 -0
  102. package/dist/utils/verify.js +464 -0
  103. 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;