difit 2.0.11 → 2.1.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/README.ja.md +17 -2
- package/README.ko.md +17 -2
- package/README.md +17 -2
- package/README.zh.md +17 -2
- package/dist/cli/index.js +50 -0
- package/dist/cli/index.test.js +141 -12
- package/dist/cli/utils.js +2 -2
- package/dist/cli/utils.test.js +10 -1
- package/dist/client/assets/index-BtavrLIu.css +1 -0
- package/dist/client/assets/index-Bx2n4Aep.js +210 -0
- package/dist/client/assets/{prism-csharp-Dc46Fjt0.js → prism-csharp-BTkEzOdP.js} +1 -1
- package/dist/client/assets/{prism-java-CqBdPW_L.js → prism-java-B6gV82l4.js} +1 -1
- package/dist/client/assets/{prism-php-BLhwjsTl.js → prism-php-gnpy0VQF.js} +1 -1
- package/dist/client/assets/prism-protobuf-DiQ_z8B5.js +1 -0
- package/dist/client/assets/{prism-ruby-ExhPumJe.js → prism-ruby-CMkpRodx.js} +1 -1
- package/dist/client/assets/{prism-solidity-BCgmGzF-.js → prism-solidity-BDXCWkss.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/file-watcher.d.ts +23 -0
- package/dist/server/file-watcher.js +236 -0
- package/dist/server/file-watcher.test.d.ts +1 -0
- package/dist/server/file-watcher.test.js +225 -0
- package/dist/server/git-diff.d.ts +2 -0
- package/dist/server/git-diff.js +47 -4
- package/dist/server/git-diff.test.js +209 -0
- package/dist/server/server.d.ts +5 -2
- package/dist/server/server.js +66 -16
- package/dist/server/server.test.js +3 -3
- package/dist/types/watch.d.ts +30 -0
- package/dist/types/watch.js +8 -0
- package/package.json +3 -1
- package/dist/client/assets/index-B6vRltPu.css +0 -1
- package/dist/client/assets/index-CjocZrF8.js +0 -200
|
@@ -5,7 +5,9 @@ export declare class GitDiffParser {
|
|
|
5
5
|
parseDiff(targetCommitish: string, baseCommitish: string, ignoreWhitespace?: boolean): Promise<DiffResponse>;
|
|
6
6
|
private parseUnifiedDiff;
|
|
7
7
|
private parseFileBlock;
|
|
8
|
+
private countLinesFromChunks;
|
|
8
9
|
private parseChunks;
|
|
9
10
|
validateCommit(commitish: string): Promise<boolean>;
|
|
11
|
+
parseStdinDiff(diffContent: string): DiffResponse;
|
|
10
12
|
getBlobContent(filepath: string, ref: string): Promise<Buffer>;
|
|
11
13
|
}
|
package/dist/server/git-diff.js
CHANGED
|
@@ -65,8 +65,14 @@ export class GitDiffParser {
|
|
|
65
65
|
for (let i = 0; i < fileBlocks.length; i++) {
|
|
66
66
|
const block = `diff --git ${fileBlocks[i]}`;
|
|
67
67
|
const summaryItem = summary[i];
|
|
68
|
-
|
|
68
|
+
// For stdin diff, we don't have summary
|
|
69
|
+
if (!summaryItem) {
|
|
70
|
+
const file = this.parseFileBlock(block, null);
|
|
71
|
+
if (file) {
|
|
72
|
+
files.push(file);
|
|
73
|
+
}
|
|
69
74
|
continue;
|
|
75
|
+
}
|
|
70
76
|
const file = this.parseFileBlock(block, summaryItem);
|
|
71
77
|
if (file) {
|
|
72
78
|
files.push(file);
|
|
@@ -105,8 +111,20 @@ export class GitDiffParser {
|
|
|
105
111
|
oldPath: oldPath !== newPath ? oldPath : undefined,
|
|
106
112
|
status,
|
|
107
113
|
};
|
|
114
|
+
// Parse chunks
|
|
115
|
+
const chunks = this.parseChunks(lines);
|
|
108
116
|
// Handle different summary types
|
|
109
|
-
if (
|
|
117
|
+
if (!summary) {
|
|
118
|
+
// No summary - count additions/deletions from chunks
|
|
119
|
+
const { additions, deletions } = this.countLinesFromChunks(chunks);
|
|
120
|
+
return {
|
|
121
|
+
...baseFile,
|
|
122
|
+
additions,
|
|
123
|
+
deletions,
|
|
124
|
+
chunks,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
else if ('binary' in summary && summary.binary) {
|
|
110
128
|
// Binary file
|
|
111
129
|
return {
|
|
112
130
|
...baseFile,
|
|
@@ -116,15 +134,28 @@ export class GitDiffParser {
|
|
|
116
134
|
};
|
|
117
135
|
}
|
|
118
136
|
else {
|
|
119
|
-
// Text file
|
|
137
|
+
// Text file with summary
|
|
120
138
|
return {
|
|
121
139
|
...baseFile,
|
|
122
140
|
additions: summary.insertions,
|
|
123
141
|
deletions: summary.deletions,
|
|
124
|
-
chunks
|
|
142
|
+
chunks,
|
|
125
143
|
};
|
|
126
144
|
}
|
|
127
145
|
}
|
|
146
|
+
countLinesFromChunks(chunks) {
|
|
147
|
+
let additions = 0;
|
|
148
|
+
let deletions = 0;
|
|
149
|
+
for (const chunk of chunks) {
|
|
150
|
+
for (const line of chunk.lines) {
|
|
151
|
+
if (line.type === 'add')
|
|
152
|
+
additions++;
|
|
153
|
+
else if (line.type === 'delete')
|
|
154
|
+
deletions++;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return { additions, deletions };
|
|
158
|
+
}
|
|
128
159
|
parseChunks(lines) {
|
|
129
160
|
const chunks = [];
|
|
130
161
|
let currentChunk = null;
|
|
@@ -190,6 +221,18 @@ export class GitDiffParser {
|
|
|
190
221
|
return false;
|
|
191
222
|
}
|
|
192
223
|
}
|
|
224
|
+
parseStdinDiff(diffContent) {
|
|
225
|
+
// For stdin diff, we pass an empty summary array
|
|
226
|
+
// parseUnifiedDiff will handle it by counting additions/deletions from the actual diff content
|
|
227
|
+
const emptySummary = [];
|
|
228
|
+
// Use the existing parseUnifiedDiff method
|
|
229
|
+
const files = this.parseUnifiedDiff(diffContent, emptySummary);
|
|
230
|
+
return {
|
|
231
|
+
commit: 'stdin diff',
|
|
232
|
+
files,
|
|
233
|
+
isEmpty: files.length === 0,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
193
236
|
async getBlobContent(filepath, ref) {
|
|
194
237
|
try {
|
|
195
238
|
// For working directory, read directly from filesystem
|
|
@@ -314,4 +314,213 @@ describe('GitDiffParser', () => {
|
|
|
314
314
|
expect(result.status).toBe('deleted');
|
|
315
315
|
});
|
|
316
316
|
});
|
|
317
|
+
describe('parseStdinDiff', () => {
|
|
318
|
+
it('should parse a simple unified diff', async () => {
|
|
319
|
+
const diffContent = `diff --git a/test.txt b/test.txt
|
|
320
|
+
index abc123..def456 100644
|
|
321
|
+
--- a/test.txt
|
|
322
|
+
+++ b/test.txt
|
|
323
|
+
@@ -1,3 +1,3 @@
|
|
324
|
+
line1
|
|
325
|
+
-line2
|
|
326
|
+
+line2 modified
|
|
327
|
+
line3`;
|
|
328
|
+
const result = parser.parseStdinDiff(diffContent);
|
|
329
|
+
expect(result).toMatchObject({
|
|
330
|
+
commit: 'stdin diff',
|
|
331
|
+
isEmpty: false,
|
|
332
|
+
files: [
|
|
333
|
+
{
|
|
334
|
+
path: 'test.txt',
|
|
335
|
+
status: 'modified',
|
|
336
|
+
additions: 1,
|
|
337
|
+
deletions: 1,
|
|
338
|
+
chunks: expect.any(Array),
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
});
|
|
342
|
+
expect(result.files[0].chunks).toHaveLength(1);
|
|
343
|
+
expect(result.files[0].chunks[0].lines).toHaveLength(4);
|
|
344
|
+
});
|
|
345
|
+
it('should parse multiple files', async () => {
|
|
346
|
+
const diffContent = `diff --git a/file1.txt b/file1.txt
|
|
347
|
+
index abc123..def456 100644
|
|
348
|
+
--- a/file1.txt
|
|
349
|
+
+++ b/file1.txt
|
|
350
|
+
@@ -1 +1 @@
|
|
351
|
+
-old content
|
|
352
|
+
+new content
|
|
353
|
+
diff --git a/file2.js b/file2.js
|
|
354
|
+
new file mode 100644
|
|
355
|
+
index 0000000..1234567
|
|
356
|
+
--- /dev/null
|
|
357
|
+
+++ b/file2.js
|
|
358
|
+
@@ -0,0 +1,3 @@
|
|
359
|
+
+function hello() {
|
|
360
|
+
+ console.log('Hello');
|
|
361
|
+
+}`;
|
|
362
|
+
const result = parser.parseStdinDiff(diffContent);
|
|
363
|
+
expect(result.files).toHaveLength(2);
|
|
364
|
+
expect(result.files[0]).toMatchObject({
|
|
365
|
+
path: 'file1.txt',
|
|
366
|
+
status: 'modified',
|
|
367
|
+
additions: 1,
|
|
368
|
+
deletions: 1,
|
|
369
|
+
});
|
|
370
|
+
expect(result.files[1]).toMatchObject({
|
|
371
|
+
path: 'file2.js',
|
|
372
|
+
status: 'added',
|
|
373
|
+
additions: 3,
|
|
374
|
+
deletions: 0,
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
it('should handle deleted files', async () => {
|
|
378
|
+
const diffContent = `diff --git a/deleted.txt b/deleted.txt
|
|
379
|
+
deleted file mode 100644
|
|
380
|
+
index abc123..0000000
|
|
381
|
+
--- a/deleted.txt
|
|
382
|
+
+++ /dev/null
|
|
383
|
+
@@ -1,2 +0,0 @@
|
|
384
|
+
-line1
|
|
385
|
+
-line2`;
|
|
386
|
+
const result = parser.parseStdinDiff(diffContent);
|
|
387
|
+
expect(result.files[0]).toMatchObject({
|
|
388
|
+
path: 'deleted.txt',
|
|
389
|
+
status: 'deleted',
|
|
390
|
+
additions: 0,
|
|
391
|
+
deletions: 2,
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
it('should handle renamed files', async () => {
|
|
395
|
+
const diffContent = `diff --git a/old-name.txt b/new-name.txt
|
|
396
|
+
similarity index 100%
|
|
397
|
+
rename from old-name.txt
|
|
398
|
+
rename to new-name.txt`;
|
|
399
|
+
const result = parser.parseStdinDiff(diffContent);
|
|
400
|
+
expect(result.files[0]).toMatchObject({
|
|
401
|
+
path: 'new-name.txt',
|
|
402
|
+
oldPath: 'old-name.txt',
|
|
403
|
+
status: 'renamed',
|
|
404
|
+
additions: 0,
|
|
405
|
+
deletions: 0,
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
it('should handle empty diff', async () => {
|
|
409
|
+
const result = parser.parseStdinDiff('');
|
|
410
|
+
expect(result).toMatchObject({
|
|
411
|
+
commit: 'stdin diff',
|
|
412
|
+
isEmpty: true,
|
|
413
|
+
files: [],
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
it('should count additions and deletions correctly', async () => {
|
|
417
|
+
const diffContent = `diff --git a/test.txt b/test.txt
|
|
418
|
+
index abc123..def456 100644
|
|
419
|
+
--- a/test.txt
|
|
420
|
+
+++ b/test.txt
|
|
421
|
+
@@ -1,5 +1,6 @@
|
|
422
|
+
unchanged line
|
|
423
|
+
-deleted line 1
|
|
424
|
+
-deleted line 2
|
|
425
|
+
+added line 1
|
|
426
|
+
+added line 2
|
|
427
|
+
+added line 3
|
|
428
|
+
another unchanged
|
|
429
|
+
final unchanged`;
|
|
430
|
+
const result = parser.parseStdinDiff(diffContent);
|
|
431
|
+
expect(result.files[0]).toMatchObject({
|
|
432
|
+
additions: 3,
|
|
433
|
+
deletions: 2,
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
it('should handle diffs with multiple chunks', async () => {
|
|
437
|
+
const diffContent = `diff --git a/test.txt b/test.txt
|
|
438
|
+
index abc123..def456 100644
|
|
439
|
+
--- a/test.txt
|
|
440
|
+
+++ b/test.txt
|
|
441
|
+
@@ -1,3 +1,3 @@
|
|
442
|
+
line1
|
|
443
|
+
-line2
|
|
444
|
+
+line2 modified
|
|
445
|
+
line3
|
|
446
|
+
@@ -10,3 +10,4 @@
|
|
447
|
+
line10
|
|
448
|
+
line11
|
|
449
|
+
line12
|
|
450
|
+
+line13 added`;
|
|
451
|
+
const result = parser.parseStdinDiff(diffContent);
|
|
452
|
+
expect(result.files[0].chunks).toHaveLength(2);
|
|
453
|
+
expect(result.files[0]).toMatchObject({
|
|
454
|
+
additions: 2,
|
|
455
|
+
deletions: 1,
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
it('should handle binary files', async () => {
|
|
459
|
+
const diffContent = `diff --git a/image.png b/image.png
|
|
460
|
+
new file mode 100644
|
|
461
|
+
index 0000000..1234567
|
|
462
|
+
Binary files /dev/null and b/image.png differ`;
|
|
463
|
+
const result = parser.parseStdinDiff(diffContent);
|
|
464
|
+
expect(result.files[0]).toMatchObject({
|
|
465
|
+
path: 'image.png',
|
|
466
|
+
status: 'added',
|
|
467
|
+
additions: 0,
|
|
468
|
+
deletions: 0,
|
|
469
|
+
chunks: [],
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
it('should handle diffs with context lines', async () => {
|
|
473
|
+
const diffContent = `diff --git a/test.txt b/test.txt
|
|
474
|
+
index abc123..def456 100644
|
|
475
|
+
--- a/test.txt
|
|
476
|
+
+++ b/test.txt
|
|
477
|
+
@@ -1,7 +1,7 @@
|
|
478
|
+
context before 1
|
|
479
|
+
context before 2
|
|
480
|
+
context before 3
|
|
481
|
+
-old line
|
|
482
|
+
+new line
|
|
483
|
+
context after 1
|
|
484
|
+
context after 2
|
|
485
|
+
context after 3`;
|
|
486
|
+
const result = parser.parseStdinDiff(diffContent);
|
|
487
|
+
const lines = result.files[0].chunks[0].lines;
|
|
488
|
+
expect(lines.filter((l) => l.type === 'normal')).toHaveLength(6);
|
|
489
|
+
expect(lines.filter((l) => l.type === 'delete')).toHaveLength(1);
|
|
490
|
+
expect(lines.filter((l) => l.type === 'add')).toHaveLength(1);
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
describe('countLinesFromChunks', () => {
|
|
494
|
+
it('should count additions and deletions correctly', () => {
|
|
495
|
+
const chunks = [
|
|
496
|
+
{
|
|
497
|
+
header: '@@ -1,3 +1,3 @@',
|
|
498
|
+
oldStart: 1,
|
|
499
|
+
oldLines: 3,
|
|
500
|
+
newStart: 1,
|
|
501
|
+
newLines: 3,
|
|
502
|
+
lines: [
|
|
503
|
+
{ type: 'normal', content: 'line1' },
|
|
504
|
+
{ type: 'delete', content: '-line2' },
|
|
505
|
+
{ type: 'add', content: '+line2 modified' },
|
|
506
|
+
{ type: 'normal', content: 'line3' },
|
|
507
|
+
],
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
header: '@@ -5,2 +5,3 @@',
|
|
511
|
+
oldStart: 5,
|
|
512
|
+
oldLines: 2,
|
|
513
|
+
newStart: 5,
|
|
514
|
+
newLines: 3,
|
|
515
|
+
lines: [
|
|
516
|
+
{ type: 'normal', content: 'line5' },
|
|
517
|
+
{ type: 'add', content: '+new line' },
|
|
518
|
+
{ type: 'normal', content: 'line6' },
|
|
519
|
+
],
|
|
520
|
+
},
|
|
521
|
+
];
|
|
522
|
+
const result = parser.countLinesFromChunks(chunks);
|
|
523
|
+
expect(result).toEqual({ additions: 2, deletions: 1 });
|
|
524
|
+
});
|
|
525
|
+
});
|
|
317
526
|
});
|
package/dist/server/server.d.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { type Server } from 'http';
|
|
2
|
+
import { type DiffMode } from '../types/watch.js';
|
|
2
3
|
interface ServerOptions {
|
|
3
|
-
targetCommitish
|
|
4
|
-
baseCommitish
|
|
4
|
+
targetCommitish?: string;
|
|
5
|
+
baseCommitish?: string;
|
|
6
|
+
stdinDiff?: string;
|
|
5
7
|
preferredPort?: number;
|
|
6
8
|
host?: string;
|
|
7
9
|
openBrowser?: boolean;
|
|
8
10
|
mode?: string;
|
|
9
11
|
ignoreWhitespace?: boolean;
|
|
10
12
|
clearComments?: boolean;
|
|
13
|
+
diffMode?: DiffMode;
|
|
11
14
|
}
|
|
12
15
|
export declare function startServer(options: ServerOptions): Promise<{
|
|
13
16
|
port: number;
|
package/dist/server/server.js
CHANGED
|
@@ -5,11 +5,13 @@ import open from 'open';
|
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
6
|
const __dirname = dirname(__filename);
|
|
7
7
|
import { getFileExtension } from '../utils/fileUtils.js';
|
|
8
|
+
import { FileWatcherService } from './file-watcher.js';
|
|
8
9
|
import { GitDiffParser } from './git-diff.js';
|
|
9
10
|
export async function startServer(options) {
|
|
10
11
|
const app = express();
|
|
11
12
|
const parser = new GitDiffParser();
|
|
12
|
-
|
|
13
|
+
const fileWatcher = new FileWatcherService();
|
|
14
|
+
let diffDataCache = null;
|
|
13
15
|
let currentIgnoreWhitespace = options.ignoreWhitespace || false;
|
|
14
16
|
const diffMode = options.mode || 'side-by-side';
|
|
15
17
|
app.use(express.json());
|
|
@@ -20,28 +22,48 @@ export async function startServer(options) {
|
|
|
20
22
|
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
|
|
21
23
|
next();
|
|
22
24
|
});
|
|
23
|
-
|
|
24
|
-
if (!
|
|
25
|
-
|
|
25
|
+
// Skip validation if using stdin diff
|
|
26
|
+
if (!options.stdinDiff) {
|
|
27
|
+
const isValidCommit = await parser.validateCommit(options.targetCommitish ?? '');
|
|
28
|
+
if (!isValidCommit) {
|
|
29
|
+
throw new Error(`Invalid or non-existent commit: ${options.targetCommitish}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Generate initial diff data for isEmpty check
|
|
33
|
+
if (options.stdinDiff) {
|
|
34
|
+
// Parse stdin diff directly
|
|
35
|
+
diffDataCache = parser.parseStdinDiff(options.stdinDiff);
|
|
26
36
|
}
|
|
27
|
-
|
|
37
|
+
else {
|
|
38
|
+
diffDataCache = await parser.parseDiff(options.targetCommitish ?? '', options.baseCommitish ?? '', currentIgnoreWhitespace);
|
|
39
|
+
}
|
|
40
|
+
// Function to invalidate cache when file changes are detected
|
|
41
|
+
const invalidateCache = () => {
|
|
42
|
+
diffDataCache = null;
|
|
43
|
+
};
|
|
28
44
|
app.get('/api/diff', async (req, res) => {
|
|
29
45
|
const ignoreWhitespace = req.query.ignoreWhitespace === 'true';
|
|
30
|
-
if
|
|
46
|
+
// Regenerate diff data if cache is invalid or whitespace setting changed
|
|
47
|
+
if (!diffDataCache || (ignoreWhitespace !== currentIgnoreWhitespace && !options.stdinDiff)) {
|
|
31
48
|
currentIgnoreWhitespace = ignoreWhitespace;
|
|
32
|
-
|
|
49
|
+
diffDataCache = await parser.parseDiff(options.targetCommitish ?? '', options.baseCommitish ?? '', ignoreWhitespace);
|
|
33
50
|
}
|
|
34
51
|
res.json({
|
|
35
|
-
...
|
|
52
|
+
...diffDataCache,
|
|
36
53
|
ignoreWhitespace,
|
|
37
54
|
mode: diffMode,
|
|
38
|
-
baseCommitish: options.baseCommitish,
|
|
39
|
-
targetCommitish: options.targetCommitish,
|
|
55
|
+
baseCommitish: options.baseCommitish || 'stdin',
|
|
56
|
+
targetCommitish: options.targetCommitish || 'stdin',
|
|
40
57
|
clearComments: options.clearComments,
|
|
41
58
|
});
|
|
42
59
|
});
|
|
43
60
|
app.get(/^\/api\/blob\/(.*)$/, async (req, res) => {
|
|
44
61
|
try {
|
|
62
|
+
// If using stdin diff, blob content is not available
|
|
63
|
+
if (options.stdinDiff) {
|
|
64
|
+
res.status(404).json({ error: 'Blob content not available for stdin diff' });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
45
67
|
const filepath = req.params[0];
|
|
46
68
|
const ref = req.query.ref || 'HEAD';
|
|
47
69
|
const blob = await parser.getBlobContent(filepath, ref);
|
|
@@ -121,6 +143,19 @@ export async function startServer(options) {
|
|
|
121
143
|
console.log(formatCommentsOutput(finalComments));
|
|
122
144
|
}
|
|
123
145
|
}
|
|
146
|
+
// SSE endpoint for file watching
|
|
147
|
+
app.get('/api/watch', (req, res) => {
|
|
148
|
+
res.writeHead(200, {
|
|
149
|
+
'Content-Type': 'text/event-stream',
|
|
150
|
+
'Cache-Control': 'no-cache',
|
|
151
|
+
Connection: 'keep-alive',
|
|
152
|
+
'Access-Control-Allow-Origin': '*',
|
|
153
|
+
});
|
|
154
|
+
fileWatcher.addClient(res);
|
|
155
|
+
req.on('close', () => {
|
|
156
|
+
fileWatcher.removeClient(res);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
124
159
|
// SSE endpoint to detect when tab is closed
|
|
125
160
|
app.get('/api/heartbeat', (req, res) => {
|
|
126
161
|
res.writeHead(200, {
|
|
@@ -138,9 +173,14 @@ export async function startServer(options) {
|
|
|
138
173
|
// When client disconnects (tab closed, navigation, etc.)
|
|
139
174
|
req.on('close', () => {
|
|
140
175
|
clearInterval(heartbeatInterval);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
176
|
+
// Add a small delay to ensure any pending sendBeacon requests are processed
|
|
177
|
+
setTimeout(async () => {
|
|
178
|
+
console.log('Client disconnected, shutting down server...');
|
|
179
|
+
// Stop file watcher
|
|
180
|
+
await fileWatcher.stop();
|
|
181
|
+
outputFinalComments();
|
|
182
|
+
process.exit(0);
|
|
183
|
+
}, 100);
|
|
144
184
|
});
|
|
145
185
|
});
|
|
146
186
|
// Always runs in production mode when distributed as a CLI tool
|
|
@@ -172,15 +212,25 @@ export async function startServer(options) {
|
|
|
172
212
|
`);
|
|
173
213
|
});
|
|
174
214
|
}
|
|
175
|
-
const { port, url, server } = await startServerWithFallback(app, options.preferredPort ||
|
|
215
|
+
const { port, url, server } = await startServerWithFallback(app, options.preferredPort || 4966, options.host || 'localhost');
|
|
176
216
|
// Security warning for non-localhost binding
|
|
177
217
|
if (options.host && options.host !== '127.0.0.1' && options.host !== 'localhost') {
|
|
178
218
|
console.warn('\n⚠️ WARNING: Server is accessible from external network!');
|
|
179
219
|
console.warn(` Binding to: ${options.host}:${port}`);
|
|
180
220
|
console.warn(' Make sure this is intended and your network is secure.\n');
|
|
181
221
|
}
|
|
222
|
+
// Start file watcher
|
|
223
|
+
if (options.diffMode) {
|
|
224
|
+
try {
|
|
225
|
+
await fileWatcher.start(options.diffMode, process.cwd(), 300, invalidateCache);
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
console.warn('⚠️ File watcher failed to start:', error);
|
|
229
|
+
console.warn(' Continuing without file watching...');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
182
232
|
// Check if diff is empty and skip browser opening
|
|
183
|
-
if (
|
|
233
|
+
if (diffDataCache?.isEmpty) {
|
|
184
234
|
// Don't open browser if no differences found
|
|
185
235
|
}
|
|
186
236
|
else if (options.openBrowser) {
|
|
@@ -191,7 +241,7 @@ export async function startServer(options) {
|
|
|
191
241
|
console.warn('Failed to open browser automatically');
|
|
192
242
|
}
|
|
193
243
|
}
|
|
194
|
-
return { port, url, isEmpty:
|
|
244
|
+
return { port, url, isEmpty: diffDataCache?.isEmpty || false, server };
|
|
195
245
|
}
|
|
196
246
|
async function startServerWithFallback(app, preferredPort, host) {
|
|
197
247
|
return new Promise((resolve, reject) => {
|
|
@@ -260,7 +260,7 @@ describe('Server Integration Tests', () => {
|
|
|
260
260
|
mode: 'inline',
|
|
261
261
|
});
|
|
262
262
|
servers.push(result.server);
|
|
263
|
-
expect(result.port).toBeGreaterThanOrEqual(
|
|
263
|
+
expect(result.port).toBeGreaterThanOrEqual(4966);
|
|
264
264
|
expect(result.url).toContain('http://localhost:');
|
|
265
265
|
});
|
|
266
266
|
it('accepts different mode values', async () => {
|
|
@@ -276,8 +276,8 @@ describe('Server Integration Tests', () => {
|
|
|
276
276
|
mode: 'side-by-side',
|
|
277
277
|
});
|
|
278
278
|
servers.push(sideBySideResult.server);
|
|
279
|
-
expect(inlineResult.port).toBeGreaterThanOrEqual(
|
|
280
|
-
expect(sideBySideResult.port).toBeGreaterThanOrEqual(
|
|
279
|
+
expect(inlineResult.port).toBeGreaterThanOrEqual(4966);
|
|
280
|
+
expect(sideBySideResult.port).toBeGreaterThanOrEqual(4966);
|
|
281
281
|
});
|
|
282
282
|
it('mode option should be included in diff response', async () => {
|
|
283
283
|
const result = await startServer({
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export declare enum DiffMode {
|
|
2
|
+
DEFAULT = "default",// HEAD^ vs HEAD
|
|
3
|
+
WORKING = "working",// staged vs working
|
|
4
|
+
STAGED = "staged",// HEAD vs staged
|
|
5
|
+
DOT = "dot",// HEAD vs working (all changes)
|
|
6
|
+
SPECIFIC = "specific"
|
|
7
|
+
}
|
|
8
|
+
export interface FileWatchConfig {
|
|
9
|
+
watchPath: string;
|
|
10
|
+
diffMode: DiffMode;
|
|
11
|
+
ignore: string[];
|
|
12
|
+
debounceMs: number;
|
|
13
|
+
backend?: 'fs-events' | 'watchman' | 'windows' | 'linux';
|
|
14
|
+
}
|
|
15
|
+
export interface WatchEvent {
|
|
16
|
+
type: 'reload' | 'error' | 'connected';
|
|
17
|
+
diffMode: DiffMode;
|
|
18
|
+
changeType: 'file' | 'commit' | 'staging';
|
|
19
|
+
timestamp: string;
|
|
20
|
+
message?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ClientWatchState {
|
|
23
|
+
isWatchEnabled: boolean;
|
|
24
|
+
diffMode: DiffMode;
|
|
25
|
+
shouldReload: boolean;
|
|
26
|
+
isReloading: boolean;
|
|
27
|
+
lastChangeTime: Date | null;
|
|
28
|
+
lastChangeType: 'file' | 'commit' | 'staging' | null;
|
|
29
|
+
connectionStatus: 'connected' | 'disconnected' | 'reconnecting';
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "difit",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
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": {
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@octokit/rest": "^22.0.0",
|
|
38
|
+
"@parcel/watcher": "^2.5.1",
|
|
38
39
|
"commander": "^14.0.0",
|
|
39
40
|
"dracula-prism": "^2.1.16",
|
|
40
41
|
"express": "^5.1.0",
|
|
@@ -46,6 +47,7 @@
|
|
|
46
47
|
"prismjs": "^1.30.0",
|
|
47
48
|
"react": "^19.1.0",
|
|
48
49
|
"react-dom": "^19.1.0",
|
|
50
|
+
"react-hotkeys-hook": "^5.1.0",
|
|
49
51
|
"simple-git": "^3.28.0"
|
|
50
52
|
},
|
|
51
53
|
"devDependencies": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
/*! tailwindcss v4.1.11 | MIT License | https://tailwindcss.com */@layer properties{@supports ((-webkit-hyphens:none) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-ease:initial;--tw-content:"";--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-100:oklch(93.6% .032 17.717);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-600:oklch(68.1% .162 75.834);--color-green-100:oklch(96.2% .044 156.743);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-md:28rem;--container-4xl:56rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--font-weight-medium:500;--font-weight-semibold:600;--radius-md:.375rem;--radius-lg:.5rem;--ease-out:cubic-bezier(0,0,.2,1);--ease-in-out:cubic-bezier(.4,0,.2,1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-github-bg-primary:#0d1117;--color-github-bg-secondary:#161b22;--color-github-bg-tertiary:#21262d;--color-github-border:#30363d;--color-github-text-primary:#f0f6fc;--color-github-text-secondary:#8b949e;--color-github-text-muted:#6e7681;--color-github-accent:#238636;--color-github-danger:#da3633;--color-github-warning:#d29922;--color-diff-addition-bg:#0d4429;--color-diff-addition-border:#1b7c3d;--color-diff-deletion-bg:#67060c;--color-diff-deletion-border:#da3633;--color-diff-neutral-bg:#21262d;--color-diff-selected-bg:#ae7c1426;--color-diff-selected-border:#ae7c1466;--color-comment-bg:#1c2128;--color-comment-border:#373e47;--color-comment-text:#e6edf3}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::-moz-placeholder{opacity:1}::placeholder{opacity:1}@supports (not (-webkit-appearance:-apple-pay-button)) or (contain-intrinsic-size:1px){::-moz-placeholder{color:currentColor}::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::-moz-placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing)*0)}.top-0{top:calc(var(--spacing)*0)}.top-1\/2{top:50%}.-right-2{right:calc(var(--spacing)*-2)}.right-0{right:calc(var(--spacing)*0)}.left-0{left:calc(var(--spacing)*0)}.left-3{left:calc(var(--spacing)*3)}.z-10{z-index:10}.z-50{z-index:50}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-0{margin:calc(var(--spacing)*0)}.m-2{margin:calc(var(--spacing)*2)}.mx-3{margin-inline:calc(var(--spacing)*3)}.mx-4{margin-inline:calc(var(--spacing)*4)}.mx-auto{margin-inline:auto}.mt-2{margin-top:calc(var(--spacing)*2)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.h-2{height:calc(var(--spacing)*2)}.h-4{height:calc(var(--spacing)*4)}.h-7{height:calc(var(--spacing)*7)}.h-full{height:100%}.h-screen{height:100vh}.max-h-96{max-height:calc(var(--spacing)*96)}.max-h-\[80vh\]{max-height:80vh}.min-h-\[16px\]{min-height:16px}.min-h-\[20px\]{min-height:20px}.min-h-\[60px\]{min-height:60px}.w-1\/2{width:50%}.w-4{width:calc(var(--spacing)*4)}.w-5{width:calc(var(--spacing)*5)}.w-7{width:calc(var(--spacing)*7)}.w-8{width:calc(var(--spacing)*8)}.w-\[50px\]{width:50px}.w-\[60px\]{width:60px}.w-full{width:100%}.max-w-4xl{max-width:var(--container-4xl)}.max-w-full{max-width:100%}.max-w-md{max-width:var(--container-md)}.min-w-0{min-width:calc(var(--spacing)*0)}.flex-1{flex:1}.flex-shrink-0{flex-shrink:0}.border-collapse{border-collapse:collapse}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.rotate-180{rotate:180deg}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.cursor-col-resize{cursor:col-resize}.cursor-pointer{cursor:pointer}.resize{resize:both}.resize-none{resize:none}.resize-y{resize:vertical}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-l{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-b{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-t-2{border-top-style:var(--tw-border-style);border-top-width:2px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-none{--tw-border-style:none;border-style:none}.border-\[var\(--border-muted\)\]{border-color:var(--border-muted)}.border-github-accent{border-color:var(--color-github-accent)}.border-github-border{border-color:var(--color-github-border)}.border-github-text-muted{border-color:var(--color-github-text-muted)}.border-yellow-600\/50{border-color:#cd890080}@supports (color:color-mix(in lab,red,red)){.border-yellow-600\/50{border-color:color-mix(in oklab,var(--color-yellow-600)50%,transparent)}}.border-t-github-accent{border-top-color:var(--color-github-accent)}.border-l-yellow-400{border-left-color:var(--color-yellow-400)}.bg-\[var\(--bg-secondary\)\]{background-color:var(--bg-secondary)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab,red,red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black)50%,transparent)}}.bg-blue-600{background-color:var(--color-blue-600)}.bg-diff-addition-bg{background-color:var(--color-diff-addition-bg)}.bg-diff-deletion-bg{background-color:var(--color-diff-deletion-bg)}.bg-github-accent{background-color:var(--color-github-accent)}.bg-github-bg-primary{background-color:var(--color-github-bg-primary)}.bg-github-bg-secondary{background-color:var(--color-github-bg-secondary)}.bg-github-bg-tertiary{background-color:var(--color-github-bg-tertiary)}.bg-github-border{background-color:var(--color-github-border)}.bg-green-100\/10{background-color:#dcfce71a}@supports (color:color-mix(in lab,red,red)){.bg-green-100\/10{background-color:color-mix(in oklab,var(--color-green-100)10%,transparent)}}.bg-red-100\/10{background-color:#ffe2e21a}@supports (color:color-mix(in lab,red,red)){.bg-red-100\/10{background-color:color-mix(in oklab,var(--color-red-100)10%,transparent)}}.bg-transparent{background-color:#0000}.p-0{padding:calc(var(--spacing)*0)}.p-1{padding:calc(var(--spacing)*1)}.p-1\.5{padding:calc(var(--spacing)*1.5)}.p-2{padding:calc(var(--spacing)*2)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.px-1{padding-inline:calc(var(--spacing)*1)}.px-1\.5{padding-inline:calc(var(--spacing)*1.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-5{padding-inline:calc(var(--spacing)*5)}.px-6{padding-inline:calc(var(--spacing)*6)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.pt-4{padding-top:calc(var(--spacing)*4)}.pr-2{padding-right:calc(var(--spacing)*2)}.pr-3{padding-right:calc(var(--spacing)*3)}.pr-5{padding-right:calc(var(--spacing)*5)}.pb-px{padding-bottom:1px}.pl-9{padding-left:calc(var(--spacing)*9)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-top{vertical-align:top}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-5{--tw-leading:calc(var(--spacing)*5);line-height:calc(var(--spacing)*5)}.leading-6{--tw-leading:calc(var(--spacing)*6);line-height:calc(var(--spacing)*6)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.break-all{word-break:break-all}.text-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-github-accent{color:var(--color-github-accent)}.text-github-danger{color:var(--color-github-danger)}.text-github-text-muted{color:var(--color-github-text-muted)}.text-github-text-primary{color:var(--color-github-text-primary)}.text-github-text-secondary{color:var(--color-github-text-secondary)}.text-github-warning{color:var(--color-github-warning)}.text-green-600{color:var(--color-green-600)}.text-white{color:var(--color-white)}.lowercase{text-transform:lowercase}.italic{font-style:italic}.line-through{text-decoration-line:line-through}.placeholder-github-text-muted::-moz-placeholder{color:var(--color-github-text-muted)}.placeholder-github-text-muted::placeholder{color:var(--color-github-text-muted)}.accent-github-accent{accent-color:var(--color-github-accent)}.opacity-70{opacity:.7}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.\!transition-all{transition-property:all!important;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))!important;transition-duration:var(--tw-duration,var(--default-transition-duration))!important}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.\!duration-300{--tw-duration:.3s!important;transition-duration:.3s!important}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.\!ease-in-out{--tw-ease:var(--ease-in-out)!important;transition-timing-function:var(--ease-in-out)!important}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.select-text{-webkit-user-select:text;-moz-user-select:text;user-select:text}.after\:pointer-events-none:after{content:var(--tw-content);pointer-events:none}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:inset-0:after{content:var(--tw-content);inset:calc(var(--spacing)*0)}.after\:border-t-2:after{content:var(--tw-content);border-top-style:var(--tw-border-style);border-top-width:2px}.after\:border-b-2:after{content:var(--tw-content);border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.after\:border-l-4:after{content:var(--tw-content);border-left-style:var(--tw-border-style);border-left-width:4px}.after\:border-l-5:after{content:var(--tw-content);border-left-style:var(--tw-border-style);border-left-width:5px}.after\:border-blue-500:after{content:var(--tw-content);border-color:var(--color-blue-500)}.after\:border-l-diff-selected-border:after{content:var(--tw-content);border-left-color:var(--color-diff-selected-border)}.after\:bg-blue-100:after{content:var(--tw-content);background-color:var(--color-blue-100)}.after\:bg-diff-selected-bg:after{content:var(--tw-content);background-color:var(--color-diff-selected-bg)}.after\:opacity-30:after{content:var(--tw-content);opacity:.3}@media (hover:hover){.hover\:scale-110:hover{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x)var(--tw-scale-y)}.hover\:border-github-accent\/50:hover{border-color:#23863680}@supports (color:color-mix(in lab,red,red)){.hover\:border-github-accent\/50:hover{border-color:color-mix(in oklab,var(--color-github-accent)50%,transparent)}}.hover\:border-github-text-muted:hover{border-color:var(--color-github-text-muted)}.hover\:border-green-600:hover{border-color:var(--color-green-600)}.hover\:bg-github-bg-primary:hover{background-color:var(--color-github-bg-primary)}.hover\:bg-github-bg-tertiary:hover{background-color:var(--color-github-bg-tertiary)}.hover\:bg-github-text-muted:hover{background-color:var(--color-github-text-muted)}.hover\:bg-green-500\/10:hover{background-color:#00c7581a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-green-500\/10:hover{background-color:color-mix(in oklab,var(--color-green-500)10%,transparent)}}.hover\:bg-green-600:hover{background-color:var(--color-green-600)}.hover\:text-github-text-primary:hover{color:var(--color-github-text-primary)}.hover\:opacity-80:hover{opacity:.8}}.focus\:min-h-\[80px\]:focus{min-height:80px}.focus\:border-blue-600:focus{border-color:var(--color-blue-600)}.focus\:border-github-accent:focus{border-color:var(--color-github-accent)}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-blue-600\/30:focus{--tw-ring-color:#155dfc4d}@supports (color:color-mix(in lab,red,red)){.focus\:ring-blue-600\/30:focus{--tw-ring-color:color-mix(in oklab,var(--color-blue-600)30%,transparent)}}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:opacity-50:disabled{opacity:.5}@media (min-width:64rem){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (prefers-color-scheme:dark){.dark\:border-slate-500{border-color:var(--color-slate-500)}.dark\:bg-slate-600{background-color:var(--color-slate-600)}.dark\:text-white{color:var(--color-white)}@media (hover:hover){.dark\:hover\:border-slate-400:hover{border-color:var(--color-slate-400)}.dark\:hover\:bg-slate-500:hover{background-color:var(--color-slate-500)}}}.\[\&_code\]\:\!bg-transparent code{background-color:#0000!important}.\[\&_code\]\:text-inherit code{color:inherit}.\[\&_pre\]\:m-0 pre{margin:calc(var(--spacing)*0)}.\[\&_pre\]\:\!bg-transparent pre{background-color:#0000!important}.\[\&_pre\]\:p-0 pre{padding:calc(var(--spacing)*0)}.\[\&_pre\]\:text-inherit pre{color:inherit}}:root{--app-font-size:14px;--app-font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif}html,body{background-color:var(--color-github-bg-primary);color:var(--color-github-text-primary);font-family:var(--app-font-family);line-height:1.5;font-size:var(--app-font-size)}:root{interpolate-size:allow-keywords}button{cursor:pointer}@keyframes sparkle-rise{0%{opacity:0;transform:translateY(20px)scale(.5)}20%{opacity:1;transform:translateY(10px)scale(1)}80%{opacity:1;transform:translateY(-30px)scale(1)}to{opacity:0;transform:translateY(-40px)scale(.8)}}.animate-sparkle-rise{animation:.8s ease-out both sparkle-rise}html,body,.bg-github-bg-primary,.bg-github-bg-secondary,.bg-github-bg-tertiary,[class*=bg-github],[class*=text-github],[class*=border-github],[class*=bg-diff],[class*=border-diff]{transition:background-color .3s,color .3s,border-color .3s}.keyboard-cursor{outline-offset:-2px;outline:2px solid #4d7adb}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}
|