difit 2.2.0 → 2.2.2

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 (31) hide show
  1. package/LICENSE +2 -2
  2. package/README.ja.md +4 -1
  3. package/README.ko.md +4 -1
  4. package/README.md +4 -1
  5. package/README.zh.md +4 -1
  6. package/dist/client/assets/index-9RijpjCf.js +220 -0
  7. package/dist/client/assets/index-DpUTViqw.css +1 -0
  8. package/dist/client/assets/{prism-csharp-B-CFRzGF.js → prism-csharp-BKbuORRj.js} +1 -1
  9. package/dist/client/assets/{prism-java-IqkDgVxE.js → prism-java-C2ppxXW3.js} +1 -1
  10. package/dist/client/assets/{prism-php-COdrRzRl.js → prism-php-JOWoQkNd.js} +1 -1
  11. package/dist/client/assets/{prism-ruby-ig7xCRi-.js → prism-ruby-C2sQ8Hg4.js} +1 -1
  12. package/dist/client/assets/{prism-solidity-lkGQYrB_.js → prism-solidity-BRuCGEwy.js} +1 -1
  13. package/dist/client/index.html +2 -2
  14. package/dist/server/git-diff.d.ts +4 -0
  15. package/dist/server/git-diff.js +131 -7
  16. package/dist/server/git-diff.test.js +318 -0
  17. package/dist/server/schemas/commentSchema.d.ts +22 -0
  18. package/dist/server/schemas/commentSchema.js +16 -0
  19. package/dist/server/schemas/commentSchema.test.d.ts +1 -0
  20. package/dist/server/schemas/commentSchema.test.js +229 -0
  21. package/dist/server/server.js +1 -13
  22. package/dist/server/server.test.js +103 -0
  23. package/dist/utils/commentFormatter.d.ts +26 -0
  24. package/dist/utils/commentFormatter.js +50 -0
  25. package/dist/utils/commentFormatting.d.ts +4 -0
  26. package/dist/utils/commentFormatting.js +24 -0
  27. package/dist/utils/commentFormatting.test.d.ts +1 -0
  28. package/dist/utils/commentFormatting.test.js +220 -0
  29. package/package.json +3 -4
  30. package/dist/client/assets/index-BG6tLkMt.js +0 -221
  31. package/dist/client/assets/index-Bz9yRRZ7.css +0 -1
@@ -4,6 +4,7 @@ import express from 'express';
4
4
  import open from 'open';
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = dirname(__filename);
7
+ import { formatCommentsOutput } from '../utils/commentFormatting.js';
7
8
  import { getFileExtension } from '../utils/fileUtils.js';
8
9
  import { FileWatcherService } from './file-watcher.js';
9
10
  import { GitDiffParser } from './git-diff.js';
@@ -124,19 +125,6 @@ export async function startServer(options) {
124
125
  res.send('');
125
126
  }
126
127
  });
127
- // Function to format comments for output
128
- function formatCommentsOutput(comments) {
129
- const prompts = comments.map((comment) => {
130
- return `${comment.file}:${Array.isArray(comment.line) ? `L${comment.line[0]}-L${comment.line[1]}` : `L${comment.line}`}\n${comment.body}`;
131
- });
132
- return [
133
- '\n📝 Comments from review session:',
134
- '='.repeat(50),
135
- prompts.join('\n=====\n'),
136
- '='.repeat(50),
137
- `Total comments: ${comments.length}\n`,
138
- ].join('\n');
139
- }
140
128
  // Function to output comments when server shuts down
141
129
  function outputFinalComments() {
142
130
  if (finalComments.length > 0) {
@@ -5,6 +5,23 @@ import { startServer } from './server.js';
5
5
  // Add fetch polyfill for Node.js test environment
6
6
  const { fetch } = await import('undici');
7
7
  globalThis.fetch = fetch;
8
+ // Helper function to get available port
9
+ async function getAvailablePort(preferredPort) {
10
+ let port = preferredPort;
11
+ const maxAttempts = 10;
12
+ for (let i = 0; i < maxAttempts; i++) {
13
+ try {
14
+ await fetch(`http://localhost:${port}`);
15
+ // If we get here, port is in use, try next one
16
+ port++;
17
+ }
18
+ catch {
19
+ // Port is available
20
+ return port;
21
+ }
22
+ }
23
+ return port;
24
+ }
8
25
  // Mock GitDiffParser
9
26
  vi.mock('./git-diff.js', () => ({
10
27
  GitDiffParser: vi.fn().mockImplementation(() => ({
@@ -29,6 +46,92 @@ vi.mock('./git-diff.js', () => ({
29
46
  })),
30
47
  }));
31
48
  describe('Server Integration Tests', () => {
49
+ describe('Comments API', () => {
50
+ it('should accept properly formatted comments', async () => {
51
+ const port = await getAvailablePort(4966);
52
+ const result = await startServer({
53
+ preferredPort: port,
54
+ openBrowser: false,
55
+ });
56
+ try {
57
+ const comments = [
58
+ {
59
+ id: '1',
60
+ file: 'src/App.tsx',
61
+ line: 10,
62
+ body: 'Test comment',
63
+ timestamp: '2024-01-01T00:00:00Z',
64
+ },
65
+ {
66
+ id: '2',
67
+ file: 'src/utils/helper.ts',
68
+ line: [20, 30],
69
+ body: 'Another comment',
70
+ timestamp: '2024-01-01T00:01:00Z',
71
+ },
72
+ ];
73
+ const response = await fetch(`http://localhost:${result.port}/api/comments`, {
74
+ method: 'POST',
75
+ headers: { 'Content-Type': 'application/json' },
76
+ body: JSON.stringify({ comments }),
77
+ });
78
+ expect(response.status).toBe(200);
79
+ const apiResult = await response.json();
80
+ expect(apiResult).toEqual({ success: true });
81
+ // Verify the formatted output
82
+ const outputResponse = await fetch(`http://localhost:${result.port}/api/comments-output`);
83
+ const output = await outputResponse.text();
84
+ expect(output).toContain('src/App.tsx:L10');
85
+ expect(output).toContain('Test comment');
86
+ expect(output).toContain('src/utils/helper.ts:L20-L30');
87
+ expect(output).toContain('Another comment');
88
+ expect(output).not.toContain('undefined');
89
+ }
90
+ finally {
91
+ if (result.server) {
92
+ await new Promise((resolve) => {
93
+ result.server.close(() => resolve());
94
+ });
95
+ }
96
+ }
97
+ });
98
+ it('should handle comments with missing file property gracefully', async () => {
99
+ const port = await getAvailablePort(4966);
100
+ const result = await startServer({
101
+ preferredPort: port,
102
+ openBrowser: false,
103
+ });
104
+ try {
105
+ const commentsWithMissingFile = [
106
+ {
107
+ id: '1',
108
+ // file property is missing/undefined
109
+ line: 10,
110
+ body: 'Comment without file',
111
+ timestamp: '2024-01-01T00:00:00Z',
112
+ },
113
+ ];
114
+ const response = await fetch(`http://localhost:${result.port}/api/comments`, {
115
+ method: 'POST',
116
+ headers: { 'Content-Type': 'application/json' },
117
+ body: JSON.stringify({ comments: commentsWithMissingFile }),
118
+ });
119
+ expect(response.status).toBe(200);
120
+ // Check the output handles undefined file gracefully
121
+ const outputResponse = await fetch(`http://localhost:${result.port}/api/comments-output`);
122
+ const output = await outputResponse.text();
123
+ expect(output).toContain('<unknown file>:L10');
124
+ expect(output).toContain('Comment without file');
125
+ }
126
+ finally {
127
+ if (result.server) {
128
+ await new Promise((resolve) => {
129
+ result.server.close(() => resolve());
130
+ });
131
+ }
132
+ }
133
+ });
134
+ });
32
135
  let servers = [];
33
136
  let originalProcessExit;
34
137
  beforeEach(() => {
@@ -0,0 +1,26 @@
1
+ import { type Comment, type LineNumber } from '../types/diff';
2
+ /**
3
+ * Formats a line number or range for display
4
+ * @param line - Single line number or line range
5
+ * @returns Formatted string like "L10" or "L10-L20", or empty string if line is undefined
6
+ */
7
+ export declare function formatLineNumber(line: LineNumber | undefined): string;
8
+ /**
9
+ * Formats a single comment for prompt output
10
+ * @param comment - The comment to format
11
+ * @returns Formatted string like "file.ts:L10\nComment body"
12
+ */
13
+ export declare function formatCommentPrompt(comment: Comment): string;
14
+ /**
15
+ * Formats multiple comments for batch output
16
+ * @param comments - Array of comments to format
17
+ * @param separator - Separator between comments (default: "=====")
18
+ * @returns Formatted string with all comments separated
19
+ */
20
+ export declare function formatCommentsPrompt(comments: Comment[], separator?: string): string;
21
+ /**
22
+ * Formats comments for server output with decorative elements
23
+ * @param comments - Array of comments to format
24
+ * @returns Formatted string with header and footer
25
+ */
26
+ export declare function formatCommentsOutput(comments: Comment[]): string;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Formats a line number or range for display
3
+ * @param line - Single line number or line range
4
+ * @returns Formatted string like "L10" or "L10-L20", or empty string if line is undefined
5
+ */
6
+ export function formatLineNumber(line) {
7
+ if (!line)
8
+ return '';
9
+ if (Array.isArray(line)) {
10
+ return `L${line[0]}-L${line[1]}`;
11
+ }
12
+ return `L${line}`;
13
+ }
14
+ /**
15
+ * Formats a single comment for prompt output
16
+ * @param comment - The comment to format
17
+ * @returns Formatted string like "file.ts:L10\nComment body"
18
+ */
19
+ export function formatCommentPrompt(comment) {
20
+ const lineDisplay = formatLineNumber(comment.line);
21
+ const location = lineDisplay ? `${comment.file}:${lineDisplay}` : comment.file;
22
+ return `${location}\n${comment.body}`;
23
+ }
24
+ /**
25
+ * Formats multiple comments for batch output
26
+ * @param comments - Array of comments to format
27
+ * @param separator - Separator between comments (default: "=====")
28
+ * @returns Formatted string with all comments separated
29
+ */
30
+ export function formatCommentsPrompt(comments, separator = '=====') {
31
+ if (comments.length === 0) {
32
+ return 'No comments available.';
33
+ }
34
+ return comments.map(formatCommentPrompt).join(`\n${separator}\n`);
35
+ }
36
+ /**
37
+ * Formats comments for server output with decorative elements
38
+ * @param comments - Array of comments to format
39
+ * @returns Formatted string with header and footer
40
+ */
41
+ export function formatCommentsOutput(comments) {
42
+ const prompts = formatCommentsPrompt(comments);
43
+ return [
44
+ '\n📝 Comments from review session:',
45
+ '='.repeat(50),
46
+ prompts,
47
+ '='.repeat(50),
48
+ `Total comments: ${comments.length}\n`,
49
+ ].join('\n');
50
+ }
@@ -0,0 +1,4 @@
1
+ import type { Comment } from '../types/diff';
2
+ export declare function formatCommentPrompt(file: string, line: number | number[], body: string): string;
3
+ export declare function formatAllCommentsPrompt(comments: Comment[]): string;
4
+ export declare function formatCommentsOutput(comments: Comment[]): string;
@@ -0,0 +1,24 @@
1
+ export function formatCommentPrompt(file, line, body) {
2
+ const lineInfo = typeof line === 'number' ? `L${line}`
3
+ : Array.isArray(line) ? `L${line[0]}-L${line[1]}`
4
+ : '';
5
+ // Handle undefined or null file paths
6
+ const filePath = file || '<unknown file>';
7
+ return `${filePath}:${lineInfo}\n${body}`;
8
+ }
9
+ export function formatAllCommentsPrompt(comments) {
10
+ if (comments.length === 0)
11
+ return '';
12
+ const prompts = comments.map((comment) => formatCommentPrompt(comment.file, comment.line, comment.body));
13
+ return prompts.join('\n=====\n');
14
+ }
15
+ export function formatCommentsOutput(comments) {
16
+ const allPrompts = formatAllCommentsPrompt(comments);
17
+ return [
18
+ '\n📝 Comments from review session:',
19
+ '='.repeat(50),
20
+ allPrompts,
21
+ '='.repeat(50),
22
+ `Total comments: ${comments.length}\n`,
23
+ ].join('\n');
24
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,220 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { formatCommentPrompt, formatAllCommentsPrompt, formatCommentsOutput, } from './commentFormatting';
3
+ describe('commentFormatting', () => {
4
+ describe('formatCommentPrompt', () => {
5
+ it('should format single line number correctly', () => {
6
+ const result = formatCommentPrompt('src/components/Button.tsx', 42, 'Fix accessibility');
7
+ expect(result).toBe('src/components/Button.tsx:L42\nFix accessibility');
8
+ });
9
+ it('should format line range correctly', () => {
10
+ const result = formatCommentPrompt('src/utils/validation.ts', [100, 120], 'Extract validation logic');
11
+ expect(result).toBe('src/utils/validation.ts:L100-L120\nExtract validation logic');
12
+ });
13
+ it('should handle line range as array correctly', () => {
14
+ const result = formatCommentPrompt('src/api/client.ts', [75, 80], 'Add retry logic');
15
+ expect(result).toBe('src/api/client.ts:L75-L80\nAdd retry logic');
16
+ });
17
+ it('should handle multi-line comment body', () => {
18
+ const body = 'This is a comment\nwith multiple lines\nof text';
19
+ const result = formatCommentPrompt('src/index.ts', 10, body);
20
+ expect(result).toBe('src/index.ts:L10\nThis is a comment\nwith multiple lines\nof text');
21
+ });
22
+ it('should handle file paths with special characters', () => {
23
+ const result = formatCommentPrompt('src/components/@shared/Button.tsx', 15, 'Update styles');
24
+ expect(result).toBe('src/components/@shared/Button.tsx:L15\nUpdate styles');
25
+ });
26
+ it('should handle undefined file path gracefully', () => {
27
+ const result = formatCommentPrompt(undefined, 10, 'Comment body');
28
+ expect(result).toBe('<unknown file>:L10\nComment body');
29
+ });
30
+ it('should handle null file path gracefully', () => {
31
+ const result = formatCommentPrompt(null, 10, 'Comment body');
32
+ expect(result).toBe('<unknown file>:L10\nComment body');
33
+ });
34
+ it('should handle empty string file path', () => {
35
+ const result = formatCommentPrompt('', 10, 'Comment body');
36
+ expect(result).toBe('<unknown file>:L10\nComment body');
37
+ });
38
+ });
39
+ describe('formatAllCommentsPrompt', () => {
40
+ it('should return empty string for empty comments array', () => {
41
+ const result = formatAllCommentsPrompt([]);
42
+ expect(result).toBe('');
43
+ });
44
+ it('should format single comment without separator', () => {
45
+ const comments = [
46
+ {
47
+ id: '1',
48
+ file: 'src/App.tsx',
49
+ line: 10,
50
+ body: 'Single comment',
51
+ timestamp: '2024-01-01T00:00:00Z',
52
+ },
53
+ ];
54
+ const result = formatAllCommentsPrompt(comments);
55
+ expect(result).toBe('src/App.tsx:L10\nSingle comment');
56
+ });
57
+ it('should format multiple comments with separator', () => {
58
+ const comments = [
59
+ {
60
+ id: '1',
61
+ file: 'src/App.tsx',
62
+ line: 10,
63
+ body: 'First comment',
64
+ timestamp: '2024-01-01T00:00:00Z',
65
+ },
66
+ {
67
+ id: '2',
68
+ file: 'src/utils/helper.ts',
69
+ line: [20, 25],
70
+ body: 'Second comment',
71
+ timestamp: '2024-01-01T00:01:00Z',
72
+ },
73
+ ];
74
+ const result = formatAllCommentsPrompt(comments);
75
+ expect(result).toBe('src/App.tsx:L10\nFirst comment\n=====\nsrc/utils/helper.ts:L20-L25\nSecond comment');
76
+ });
77
+ it('should handle comments with line ranges', () => {
78
+ const comments = [
79
+ {
80
+ id: '1',
81
+ file: 'src/components/Form.tsx',
82
+ line: [50, 75],
83
+ body: 'Refactor form validation',
84
+ timestamp: '2024-01-01T00:00:00Z',
85
+ },
86
+ {
87
+ id: '2',
88
+ file: 'src/api/endpoints.ts',
89
+ line: [100, 150],
90
+ body: 'Add error handling',
91
+ timestamp: '2024-01-01T00:01:00Z',
92
+ },
93
+ ];
94
+ const result = formatAllCommentsPrompt(comments);
95
+ expect(result).toBe('src/components/Form.tsx:L50-L75\nRefactor form validation\n=====\nsrc/api/endpoints.ts:L100-L150\nAdd error handling');
96
+ });
97
+ it('should preserve comment order', () => {
98
+ const comments = [
99
+ {
100
+ id: '3',
101
+ file: 'third.ts',
102
+ line: 30,
103
+ body: 'Third',
104
+ timestamp: '2024-01-01T00:02:00Z',
105
+ },
106
+ {
107
+ id: '1',
108
+ file: 'first.ts',
109
+ line: 10,
110
+ body: 'First',
111
+ timestamp: '2024-01-01T00:00:00Z',
112
+ },
113
+ {
114
+ id: '2',
115
+ file: 'second.ts',
116
+ line: 20,
117
+ body: 'Second',
118
+ timestamp: '2024-01-01T00:01:00Z',
119
+ },
120
+ ];
121
+ const result = formatAllCommentsPrompt(comments);
122
+ expect(result).toBe('third.ts:L30\nThird\n=====\nfirst.ts:L10\nFirst\n=====\nsecond.ts:L20\nSecond');
123
+ });
124
+ });
125
+ describe('formatCommentsOutput', () => {
126
+ it('should format empty comments with header and footer', () => {
127
+ const result = formatCommentsOutput([]);
128
+ const lines = result.split('\n');
129
+ expect(lines[0]).toBe('');
130
+ expect(lines[1]).toBe('📝 Comments from review session:');
131
+ expect(lines[2]).toBe('='.repeat(50));
132
+ expect(lines[3]).toBe('');
133
+ expect(lines[4]).toBe('='.repeat(50));
134
+ expect(lines[5]).toBe('Total comments: 0');
135
+ });
136
+ it('should format single comment with header and footer', () => {
137
+ const comments = [
138
+ {
139
+ id: '1',
140
+ file: 'src/App.tsx',
141
+ line: 10,
142
+ body: 'Fix this issue',
143
+ timestamp: '2024-01-01T00:00:00Z',
144
+ },
145
+ ];
146
+ const result = formatCommentsOutput(comments);
147
+ expect(result).toContain('📝 Comments from review session:');
148
+ expect(result).toContain('src/App.tsx:L10\nFix this issue');
149
+ expect(result).toContain('Total comments: 1');
150
+ expect(result).toContain('='.repeat(50));
151
+ });
152
+ it('should format multiple comments with separators', () => {
153
+ const comments = [
154
+ {
155
+ id: '1',
156
+ file: 'src/App.tsx',
157
+ line: 10,
158
+ body: 'First issue',
159
+ timestamp: '2024-01-01T00:00:00Z',
160
+ },
161
+ {
162
+ id: '2',
163
+ file: 'src/utils/helper.ts',
164
+ line: [20, 30],
165
+ body: 'Second issue',
166
+ timestamp: '2024-01-01T00:01:00Z',
167
+ },
168
+ {
169
+ id: '3',
170
+ file: 'src/components/Button.tsx',
171
+ line: 42,
172
+ body: 'Third issue',
173
+ timestamp: '2024-01-01T00:02:00Z',
174
+ },
175
+ ];
176
+ const result = formatCommentsOutput(comments);
177
+ expect(result).toContain('📝 Comments from review session:');
178
+ expect(result).toContain('src/App.tsx:L10\nFirst issue');
179
+ expect(result).toContain('=====');
180
+ expect(result).toContain('src/utils/helper.ts:L20-L30\nSecond issue');
181
+ expect(result).toContain('src/components/Button.tsx:L42\nThird issue');
182
+ expect(result).toContain('Total comments: 3');
183
+ });
184
+ it('should handle comments with multi-line bodies', () => {
185
+ const comments = [
186
+ {
187
+ id: '1',
188
+ file: 'src/complex.ts',
189
+ line: [100, 200],
190
+ body: 'This is a complex issue that\nspans multiple lines\nand needs attention',
191
+ timestamp: '2024-01-01T00:00:00Z',
192
+ },
193
+ ];
194
+ const result = formatCommentsOutput(comments);
195
+ expect(result).toContain('src/complex.ts:L100-L200');
196
+ expect(result).toContain('This is a complex issue that\nspans multiple lines\nand needs attention');
197
+ expect(result).toContain('Total comments: 1');
198
+ });
199
+ it('should format output with correct structure', () => {
200
+ const comments = [
201
+ {
202
+ id: '1',
203
+ file: 'test.ts',
204
+ line: 1,
205
+ body: 'Test comment',
206
+ timestamp: '2024-01-01T00:00:00Z',
207
+ },
208
+ ];
209
+ const result = formatCommentsOutput(comments);
210
+ const lines = result.split('\n');
211
+ expect(lines[0]).toBe('');
212
+ expect(lines[1]).toBe('📝 Comments from review session:');
213
+ expect(lines[2]).toBe('='.repeat(50));
214
+ expect(lines[3]).toBe('test.ts:L1');
215
+ expect(lines[4]).toBe('Test comment');
216
+ expect(lines[5]).toBe('='.repeat(50));
217
+ expect(lines[6]).toBe('Total comments: 1');
218
+ });
219
+ });
220
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "difit",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "A lightweight command-line tool that spins up a local web server to display Git commit diffs in a GitHub-like Files changed view",
5
5
  "type": "module",
6
6
  "engines": {
@@ -54,8 +54,7 @@
54
54
  "react": "^19.1.0",
55
55
  "react-dom": "^19.1.0",
56
56
  "react-hotkeys-hook": "^5.1.0",
57
- "simple-git": "^3.28.0",
58
- "uuid": "^11.1.0"
57
+ "simple-git": "^3.28.0"
59
58
  },
60
59
  "devDependencies": {
61
60
  "@eslint/js": "^9.30.1",
@@ -72,7 +71,6 @@
72
71
  "@types/prismjs": "^1.26.5",
73
72
  "@types/react": "^19.1.8",
74
73
  "@types/react-dom": "^19.1.6",
75
- "@types/uuid": "^10.0.0",
76
74
  "@typescript-eslint/eslint-plugin": "^8.35.1",
77
75
  "@typescript-eslint/parser": "^8.35.1",
78
76
  "@vitejs/plugin-react": "^4.6.0",
@@ -86,6 +84,7 @@
86
84
  "globals": "^16.3.0",
87
85
  "happy-dom": "^18.0.1",
88
86
  "lefthook": "^1.11.14",
87
+ "mri": "^1.2.0",
89
88
  "playwright": "^1.54.1",
90
89
  "postcss": "^8.5.6",
91
90
  "prettier": "^3.6.2",