reviw 0.9.2 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.cjs +412 -0
- package/package.json +6 -6
package/cli.cjs
CHANGED
|
@@ -839,6 +839,18 @@ function diffHtmlTemplate(diffData) {
|
|
|
839
839
|
z-index: 100;
|
|
840
840
|
}
|
|
841
841
|
.modal-overlay.visible { display: flex; }
|
|
842
|
+
/* Submit modal: top-right position, no blocking overlay */
|
|
843
|
+
#submit-modal {
|
|
844
|
+
background: transparent;
|
|
845
|
+
pointer-events: none;
|
|
846
|
+
align-items: flex-start;
|
|
847
|
+
justify-content: flex-end;
|
|
848
|
+
}
|
|
849
|
+
#submit-modal.visible { display: flex; }
|
|
850
|
+
#submit-modal .modal-dialog {
|
|
851
|
+
pointer-events: auto;
|
|
852
|
+
margin: 60px 20px 20px 20px; /* top margin avoids header button overlap */
|
|
853
|
+
}
|
|
842
854
|
.modal-dialog {
|
|
843
855
|
background: var(--panel);
|
|
844
856
|
border: 1px solid var(--border);
|
|
@@ -1991,6 +2003,55 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
1991
2003
|
border-radius: 8px;
|
|
1992
2004
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
|
1993
2005
|
}
|
|
2006
|
+
/* Video fullscreen overlay */
|
|
2007
|
+
.video-fullscreen-overlay {
|
|
2008
|
+
position: fixed;
|
|
2009
|
+
inset: 0;
|
|
2010
|
+
background: rgba(0, 0, 0, 0.95);
|
|
2011
|
+
z-index: 1001;
|
|
2012
|
+
display: none;
|
|
2013
|
+
justify-content: center;
|
|
2014
|
+
align-items: center;
|
|
2015
|
+
}
|
|
2016
|
+
.video-fullscreen-overlay.visible {
|
|
2017
|
+
display: flex;
|
|
2018
|
+
}
|
|
2019
|
+
.video-close-btn {
|
|
2020
|
+
position: absolute;
|
|
2021
|
+
top: 14px;
|
|
2022
|
+
right: 14px;
|
|
2023
|
+
width: 40px;
|
|
2024
|
+
height: 40px;
|
|
2025
|
+
display: flex;
|
|
2026
|
+
align-items: center;
|
|
2027
|
+
justify-content: center;
|
|
2028
|
+
background: rgba(0, 0, 0, 0.55);
|
|
2029
|
+
border: 1px solid rgba(255, 255, 255, 0.25);
|
|
2030
|
+
border-radius: 50%;
|
|
2031
|
+
cursor: pointer;
|
|
2032
|
+
color: #fff;
|
|
2033
|
+
font-size: 18px;
|
|
2034
|
+
z-index: 10;
|
|
2035
|
+
backdrop-filter: blur(4px);
|
|
2036
|
+
transition: background 120ms ease, transform 120ms ease;
|
|
2037
|
+
}
|
|
2038
|
+
.video-close-btn:hover {
|
|
2039
|
+
background: rgba(0, 0, 0, 0.75);
|
|
2040
|
+
transform: scale(1.04);
|
|
2041
|
+
}
|
|
2042
|
+
.video-container {
|
|
2043
|
+
width: 90vw;
|
|
2044
|
+
height: 90vh;
|
|
2045
|
+
display: flex;
|
|
2046
|
+
justify-content: center;
|
|
2047
|
+
align-items: center;
|
|
2048
|
+
}
|
|
2049
|
+
.video-container video {
|
|
2050
|
+
max-width: 100%;
|
|
2051
|
+
max-height: 100%;
|
|
2052
|
+
border-radius: 8px;
|
|
2053
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
|
2054
|
+
}
|
|
1994
2055
|
/* Copy notification toast */
|
|
1995
2056
|
.copy-toast {
|
|
1996
2057
|
position: fixed;
|
|
@@ -2051,6 +2112,18 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
2051
2112
|
z-index: 100;
|
|
2052
2113
|
}
|
|
2053
2114
|
.modal-overlay.visible { display: flex; }
|
|
2115
|
+
/* Submit modal: top-right position, no blocking overlay */
|
|
2116
|
+
#submit-modal {
|
|
2117
|
+
background: transparent;
|
|
2118
|
+
pointer-events: none;
|
|
2119
|
+
align-items: flex-start;
|
|
2120
|
+
justify-content: flex-end;
|
|
2121
|
+
}
|
|
2122
|
+
#submit-modal.visible { display: flex; }
|
|
2123
|
+
#submit-modal .modal-dialog {
|
|
2124
|
+
pointer-events: auto;
|
|
2125
|
+
margin: 60px 20px 20px 20px; /* top margin avoids header button overlap */
|
|
2126
|
+
}
|
|
2054
2127
|
.modal-dialog {
|
|
2055
2128
|
background: var(--panel-solid);
|
|
2056
2129
|
border: 1px solid var(--border);
|
|
@@ -2352,6 +2425,10 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
2352
2425
|
<button class="image-close-btn" id="image-close" aria-label="Close image" title="Close (ESC)">✕</button>
|
|
2353
2426
|
<div class="image-container" id="image-container"></div>
|
|
2354
2427
|
</div>
|
|
2428
|
+
<div class="video-fullscreen-overlay" id="video-fullscreen">
|
|
2429
|
+
<button class="video-close-btn" id="video-close" aria-label="Close video" title="Close (ESC)">✕</button>
|
|
2430
|
+
<div class="video-container" id="video-container"></div>
|
|
2431
|
+
</div>
|
|
2355
2432
|
|
|
2356
2433
|
<script>
|
|
2357
2434
|
const DATA = ${serialized};
|
|
@@ -3652,6 +3729,321 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
3652
3729
|
});
|
|
3653
3730
|
});
|
|
3654
3731
|
})();
|
|
3732
|
+
|
|
3733
|
+
// --- Video Fullscreen ---
|
|
3734
|
+
(function initVideoFullscreen() {
|
|
3735
|
+
const preview = document.querySelector('.md-preview');
|
|
3736
|
+
if (!preview) return;
|
|
3737
|
+
|
|
3738
|
+
const videoOverlay = document.getElementById('video-fullscreen');
|
|
3739
|
+
const videoContainer = document.getElementById('video-container');
|
|
3740
|
+
const videoClose = document.getElementById('video-close');
|
|
3741
|
+
if (!videoOverlay || !videoContainer) return;
|
|
3742
|
+
|
|
3743
|
+
const videoExtensions = /\\.(mp4|mov|webm|avi|mkv|m4v|ogv)$/i;
|
|
3744
|
+
|
|
3745
|
+
function closeVideoOverlay() {
|
|
3746
|
+
videoOverlay.classList.remove('visible');
|
|
3747
|
+
// Stop and remove video
|
|
3748
|
+
const video = videoContainer.querySelector('video');
|
|
3749
|
+
if (video) {
|
|
3750
|
+
video.pause();
|
|
3751
|
+
video.src = '';
|
|
3752
|
+
video.remove();
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3756
|
+
if (videoClose) {
|
|
3757
|
+
videoClose.addEventListener('click', closeVideoOverlay);
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3760
|
+
if (videoOverlay) {
|
|
3761
|
+
videoOverlay.addEventListener('click', (e) => {
|
|
3762
|
+
if (e.target === videoOverlay) closeVideoOverlay();
|
|
3763
|
+
});
|
|
3764
|
+
}
|
|
3765
|
+
|
|
3766
|
+
document.addEventListener('keydown', (e) => {
|
|
3767
|
+
if (e.key === 'Escape' && videoOverlay.classList.contains('visible')) {
|
|
3768
|
+
closeVideoOverlay();
|
|
3769
|
+
}
|
|
3770
|
+
});
|
|
3771
|
+
|
|
3772
|
+
// Intercept video link clicks
|
|
3773
|
+
preview.querySelectorAll('a').forEach(link => {
|
|
3774
|
+
const href = link.getAttribute('href');
|
|
3775
|
+
if (href && videoExtensions.test(href)) {
|
|
3776
|
+
link.style.cursor = 'pointer';
|
|
3777
|
+
link.title = 'Click to play video fullscreen';
|
|
3778
|
+
|
|
3779
|
+
link.addEventListener('click', (e) => {
|
|
3780
|
+
e.preventDefault();
|
|
3781
|
+
e.stopPropagation();
|
|
3782
|
+
|
|
3783
|
+
// Remove existing video if any
|
|
3784
|
+
const existingVideo = videoContainer.querySelector('video');
|
|
3785
|
+
if (existingVideo) {
|
|
3786
|
+
existingVideo.pause();
|
|
3787
|
+
existingVideo.src = '';
|
|
3788
|
+
existingVideo.remove();
|
|
3789
|
+
}
|
|
3790
|
+
|
|
3791
|
+
const video = document.createElement('video');
|
|
3792
|
+
video.src = href;
|
|
3793
|
+
video.controls = true;
|
|
3794
|
+
video.autoplay = true;
|
|
3795
|
+
video.style.maxWidth = '100%';
|
|
3796
|
+
video.style.maxHeight = '100%';
|
|
3797
|
+
videoContainer.appendChild(video);
|
|
3798
|
+
|
|
3799
|
+
videoOverlay.classList.add('visible');
|
|
3800
|
+
});
|
|
3801
|
+
}
|
|
3802
|
+
});
|
|
3803
|
+
})();
|
|
3804
|
+
|
|
3805
|
+
// --- Preview Commenting ---
|
|
3806
|
+
(function initPreviewCommenting() {
|
|
3807
|
+
if (MODE !== 'markdown') return;
|
|
3808
|
+
|
|
3809
|
+
const preview = document.querySelector('.md-preview');
|
|
3810
|
+
if (!preview) return;
|
|
3811
|
+
|
|
3812
|
+
// Add visual hint for clickable elements
|
|
3813
|
+
const style = document.createElement('style');
|
|
3814
|
+
style.textContent = \`
|
|
3815
|
+
.md-preview > p:hover, .md-preview > h1:hover, .md-preview > h2:hover,
|
|
3816
|
+
.md-preview > h3:hover, .md-preview > h4:hover, .md-preview > h5:hover,
|
|
3817
|
+
.md-preview > h6:hover, .md-preview > ul > li:hover, .md-preview > ol > li:hover,
|
|
3818
|
+
.md-preview > pre:hover, .md-preview > blockquote:hover {
|
|
3819
|
+
background: rgba(99, 102, 241, 0.08);
|
|
3820
|
+
cursor: pointer;
|
|
3821
|
+
border-radius: 4px;
|
|
3822
|
+
}
|
|
3823
|
+
.md-preview img:hover {
|
|
3824
|
+
outline: 2px solid var(--accent);
|
|
3825
|
+
cursor: pointer;
|
|
3826
|
+
}
|
|
3827
|
+
\`;
|
|
3828
|
+
document.head.appendChild(style);
|
|
3829
|
+
|
|
3830
|
+
// Helper: find matching source line for text
|
|
3831
|
+
function findSourceLine(text) {
|
|
3832
|
+
if (!text) return -1;
|
|
3833
|
+
const normalized = text.trim().replace(/\\s+/g, ' ').slice(0, 100);
|
|
3834
|
+
if (!normalized) return -1;
|
|
3835
|
+
|
|
3836
|
+
for (let i = 0; i < DATA.length; i++) {
|
|
3837
|
+
const lineText = (DATA[i][0] || '').trim();
|
|
3838
|
+
if (!lineText) continue;
|
|
3839
|
+
|
|
3840
|
+
const lineNorm = lineText.replace(/\\s+/g, ' ').slice(0, 100);
|
|
3841
|
+
if (lineNorm === normalized) return i + 1;
|
|
3842
|
+
if (lineNorm.includes(normalized.slice(0, 30)) && normalized.length > 5) return i + 1;
|
|
3843
|
+
if (normalized.includes(lineNorm.slice(0, 30)) && lineNorm.length > 5) return i + 1;
|
|
3844
|
+
|
|
3845
|
+
// Check for markdown headings: strip # from source and compare
|
|
3846
|
+
if (lineText.match(/^#+\\s/)) {
|
|
3847
|
+
const headingText = lineText.replace(/^#+\\s*/, '').trim();
|
|
3848
|
+
if (headingText === normalized || headingText.toLowerCase() === normalized.toLowerCase()) {
|
|
3849
|
+
return i + 1;
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
}
|
|
3853
|
+
return -1;
|
|
3854
|
+
}
|
|
3855
|
+
|
|
3856
|
+
// Helper: find matching source line for table cell (prioritizes table rows)
|
|
3857
|
+
function findTableSourceLine(text) {
|
|
3858
|
+
if (!text) return -1;
|
|
3859
|
+
const normalized = text.trim().replace(/\\s+/g, ' ').slice(0, 100);
|
|
3860
|
+
if (!normalized) return -1;
|
|
3861
|
+
|
|
3862
|
+
// First pass: look for table rows (lines starting with |) containing the text
|
|
3863
|
+
for (let i = 0; i < DATA.length; i++) {
|
|
3864
|
+
const lineText = (DATA[i][0] || '').trim();
|
|
3865
|
+
if (!lineText || !lineText.startsWith('|')) continue;
|
|
3866
|
+
|
|
3867
|
+
const lineNorm = lineText.replace(/\\s+/g, ' ').slice(0, 100);
|
|
3868
|
+
if (lineNorm.includes(normalized.slice(0, 30)) && normalized.length > 5) return i + 1;
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
// Fallback to normal search
|
|
3872
|
+
return findSourceLine(text);
|
|
3873
|
+
}
|
|
3874
|
+
|
|
3875
|
+
// Helper: find code block range in source (fenced code blocks)
|
|
3876
|
+
function findCodeBlockRange(codeText) {
|
|
3877
|
+
const clickedLines = codeText.split('\\n').map(l => l.trim()).filter(l => l);
|
|
3878
|
+
const clickedContent = clickedLines.join('\\n');
|
|
3879
|
+
|
|
3880
|
+
// Extract all code blocks from DATA
|
|
3881
|
+
const codeBlocks = [];
|
|
3882
|
+
let currentBlock = null;
|
|
3883
|
+
|
|
3884
|
+
for (let i = 0; i < DATA.length; i++) {
|
|
3885
|
+
const lineText = (DATA[i][0] || '').trim();
|
|
3886
|
+
|
|
3887
|
+
if (lineText.startsWith('\`\`\`') && !currentBlock) {
|
|
3888
|
+
// Start of a code block
|
|
3889
|
+
currentBlock = { startLine: i + 1, lines: [] };
|
|
3890
|
+
} else if (lineText === '\`\`\`' && currentBlock) {
|
|
3891
|
+
// End of a code block
|
|
3892
|
+
currentBlock.endLine = i + 1;
|
|
3893
|
+
currentBlock.content = currentBlock.lines.map(l => l.trim()).filter(l => l).join('\\n');
|
|
3894
|
+
codeBlocks.push(currentBlock);
|
|
3895
|
+
currentBlock = null;
|
|
3896
|
+
} else if (currentBlock) {
|
|
3897
|
+
// Inside a code block
|
|
3898
|
+
currentBlock.lines.push(DATA[i][0] || '');
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3901
|
+
|
|
3902
|
+
// Find the best matching code block by content similarity
|
|
3903
|
+
let bestMatch = null;
|
|
3904
|
+
let bestScore = 0;
|
|
3905
|
+
|
|
3906
|
+
for (const block of codeBlocks) {
|
|
3907
|
+
// Calculate similarity score
|
|
3908
|
+
let score = 0;
|
|
3909
|
+
|
|
3910
|
+
// Exact match
|
|
3911
|
+
if (block.content === clickedContent) {
|
|
3912
|
+
score = 1000;
|
|
3913
|
+
} else {
|
|
3914
|
+
// Check line-by-line matches
|
|
3915
|
+
const blockLines = block.content.split('\\n');
|
|
3916
|
+
for (const clickedLine of clickedLines) {
|
|
3917
|
+
if (clickedLine.length > 3) {
|
|
3918
|
+
for (const blockLine of blockLines) {
|
|
3919
|
+
if (blockLine.includes(clickedLine) || clickedLine.includes(blockLine)) {
|
|
3920
|
+
score += clickedLine.length;
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3927
|
+
if (score > bestScore) {
|
|
3928
|
+
bestScore = score;
|
|
3929
|
+
bestMatch = block;
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
|
|
3933
|
+
if (bestMatch) {
|
|
3934
|
+
return { startLine: bestMatch.startLine, endLine: bestMatch.endLine };
|
|
3935
|
+
}
|
|
3936
|
+
|
|
3937
|
+
// Fallback: find by first line content matching
|
|
3938
|
+
const firstCodeLine = clickedLines[0];
|
|
3939
|
+
if (firstCodeLine && firstCodeLine.length > 3) {
|
|
3940
|
+
for (let i = 0; i < DATA.length; i++) {
|
|
3941
|
+
const lineText = (DATA[i][0] || '').trim();
|
|
3942
|
+
if (lineText.includes(firstCodeLine.slice(0, 30))) {
|
|
3943
|
+
return { startLine: i + 1, endLine: i + 1 };
|
|
3944
|
+
}
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
|
|
3948
|
+
return { startLine: -1, endLine: -1 };
|
|
3949
|
+
}
|
|
3950
|
+
|
|
3951
|
+
// Helper: find source line for image by src
|
|
3952
|
+
function findImageSourceLine(src) {
|
|
3953
|
+
if (!src) return -1;
|
|
3954
|
+
const filename = src.split('/').pop().split('?')[0];
|
|
3955
|
+
for (let i = 0; i < DATA.length; i++) {
|
|
3956
|
+
const lineText = DATA[i][0] || '';
|
|
3957
|
+
if (lineText.includes(filename) || lineText.includes(src)) {
|
|
3958
|
+
return i + 1;
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
return -1;
|
|
3962
|
+
}
|
|
3963
|
+
|
|
3964
|
+
// Trigger source cell selection (reuse existing comment flow)
|
|
3965
|
+
function selectSourceRange(startRow, endRow) {
|
|
3966
|
+
selection = { startRow, endRow: endRow || startRow, startCol: 1, endCol: 1 };
|
|
3967
|
+
updateSelectionVisual();
|
|
3968
|
+
|
|
3969
|
+
// Clear header selection
|
|
3970
|
+
document.querySelectorAll('thead th.selected').forEach(el => el.classList.remove('selected'));
|
|
3971
|
+
|
|
3972
|
+
// Scroll source table to show the selected row, then open card
|
|
3973
|
+
const sourceTd = tbody.querySelector('td[data-row="' + startRow + '"][data-col="1"]');
|
|
3974
|
+
if (sourceTd) {
|
|
3975
|
+
sourceTd.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
3976
|
+
// Wait for scroll to complete before positioning card
|
|
3977
|
+
setTimeout(() => openCardForSelection(), 350);
|
|
3978
|
+
} else {
|
|
3979
|
+
openCardForSelection();
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3982
|
+
|
|
3983
|
+
// Click on block elements
|
|
3984
|
+
preview.addEventListener('click', (e) => {
|
|
3985
|
+
// Handle image clicks
|
|
3986
|
+
if (e.target.tagName === 'IMG') {
|
|
3987
|
+
if (!e.defaultPrevented) {
|
|
3988
|
+
const line = findImageSourceLine(e.target.src);
|
|
3989
|
+
if (line > 0) {
|
|
3990
|
+
e.preventDefault();
|
|
3991
|
+
e.stopPropagation();
|
|
3992
|
+
selectSourceRange(line);
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3995
|
+
return;
|
|
3996
|
+
}
|
|
3997
|
+
|
|
3998
|
+
// Ignore clicks on links, mermaid, video overlay
|
|
3999
|
+
if (e.target.closest('a, .mermaid-container, .video-fullscreen-overlay')) return;
|
|
4000
|
+
|
|
4001
|
+
// Handle code blocks - select entire block
|
|
4002
|
+
const pre = e.target.closest('pre');
|
|
4003
|
+
if (pre) {
|
|
4004
|
+
const code = pre.querySelector('code') || pre;
|
|
4005
|
+
const { startLine, endLine } = findCodeBlockRange(code.textContent);
|
|
4006
|
+
if (startLine > 0) {
|
|
4007
|
+
e.preventDefault();
|
|
4008
|
+
selectSourceRange(startLine, endLine);
|
|
4009
|
+
}
|
|
4010
|
+
return;
|
|
4011
|
+
}
|
|
4012
|
+
|
|
4013
|
+
const target = e.target.closest('p, h1, h2, h3, h4, h5, h6, li, blockquote, td, th');
|
|
4014
|
+
if (!target) return;
|
|
4015
|
+
|
|
4016
|
+
// Use table-specific search for table cells
|
|
4017
|
+
const isTableCell = target.tagName === 'TD' || target.tagName === 'TH';
|
|
4018
|
+
const line = isTableCell ? findTableSourceLine(target.textContent) : findSourceLine(target.textContent);
|
|
4019
|
+
if (line <= 0) return;
|
|
4020
|
+
|
|
4021
|
+
e.preventDefault();
|
|
4022
|
+
selectSourceRange(line);
|
|
4023
|
+
});
|
|
4024
|
+
|
|
4025
|
+
// Text selection to open comment for range
|
|
4026
|
+
preview.addEventListener('mouseup', (e) => {
|
|
4027
|
+
setTimeout(() => {
|
|
4028
|
+
const sel = window.getSelection();
|
|
4029
|
+
if (!sel || sel.isCollapsed) return;
|
|
4030
|
+
|
|
4031
|
+
const text = sel.toString().trim();
|
|
4032
|
+
if (!text || text.length < 5) return;
|
|
4033
|
+
|
|
4034
|
+
const lines = text.split('\\n').filter(l => l.trim());
|
|
4035
|
+
if (lines.length === 0) return;
|
|
4036
|
+
|
|
4037
|
+
const startLine = findSourceLine(lines[0]);
|
|
4038
|
+
const endLine = lines.length > 1 ? findSourceLine(lines[lines.length - 1]) : startLine;
|
|
4039
|
+
|
|
4040
|
+
if (startLine <= 0) return;
|
|
4041
|
+
|
|
4042
|
+
sel.removeAllRanges();
|
|
4043
|
+
selectSourceRange(startLine, endLine > 0 ? endLine : startLine);
|
|
4044
|
+
}, 10);
|
|
4045
|
+
});
|
|
4046
|
+
})();
|
|
3655
4047
|
</script>
|
|
3656
4048
|
</body>
|
|
3657
4049
|
</html>`;
|
|
@@ -3890,7 +4282,10 @@ function createFileServer(filePath) {
|
|
|
3890
4282
|
res.end("not found");
|
|
3891
4283
|
});
|
|
3892
4284
|
|
|
4285
|
+
let serverStarted = false;
|
|
4286
|
+
|
|
3893
4287
|
function tryListen(attemptPort, attempts = 0) {
|
|
4288
|
+
if (serverStarted) return; // Prevent double-start race condition
|
|
3894
4289
|
if (attempts >= MAX_PORT_ATTEMPTS) {
|
|
3895
4290
|
console.error(
|
|
3896
4291
|
`Could not find an available port for ${baseName} after ${MAX_PORT_ATTEMPTS} attempts.`,
|
|
@@ -3901,6 +4296,7 @@ function createFileServer(filePath) {
|
|
|
3901
4296
|
}
|
|
3902
4297
|
|
|
3903
4298
|
ctx.server.once("error", (err) => {
|
|
4299
|
+
if (serverStarted) return; // Already started on another port
|
|
3904
4300
|
if (err.code === "EADDRINUSE") {
|
|
3905
4301
|
tryListen(attemptPort + 1, attempts + 1);
|
|
3906
4302
|
} else {
|
|
@@ -3911,6 +4307,12 @@ function createFileServer(filePath) {
|
|
|
3911
4307
|
});
|
|
3912
4308
|
|
|
3913
4309
|
ctx.server.listen(attemptPort, () => {
|
|
4310
|
+
if (serverStarted) {
|
|
4311
|
+
// Race condition: server started on multiple ports, close this one
|
|
4312
|
+
try { ctx.server.close(); } catch (_) {}
|
|
4313
|
+
return;
|
|
4314
|
+
}
|
|
4315
|
+
serverStarted = true;
|
|
3914
4316
|
ctx.port = attemptPort;
|
|
3915
4317
|
nextPort = attemptPort + 1;
|
|
3916
4318
|
activeServers.set(filePath, ctx);
|
|
@@ -4044,7 +4446,10 @@ function createDiffServer(diffContent) {
|
|
|
4044
4446
|
res.end("not found");
|
|
4045
4447
|
});
|
|
4046
4448
|
|
|
4449
|
+
let serverStarted = false;
|
|
4450
|
+
|
|
4047
4451
|
function tryListen(attemptPort, attempts = 0) {
|
|
4452
|
+
if (serverStarted) return; // Prevent double-start race condition
|
|
4048
4453
|
if (attempts >= MAX_PORT_ATTEMPTS) {
|
|
4049
4454
|
console.error(
|
|
4050
4455
|
`Could not find an available port for diff viewer after ${MAX_PORT_ATTEMPTS} attempts.`,
|
|
@@ -4055,6 +4460,7 @@ function createDiffServer(diffContent) {
|
|
|
4055
4460
|
}
|
|
4056
4461
|
|
|
4057
4462
|
ctx.server.once("error", (err) => {
|
|
4463
|
+
if (serverStarted) return; // Already started on another port
|
|
4058
4464
|
if (err.code === "EADDRINUSE") {
|
|
4059
4465
|
tryListen(attemptPort + 1, attempts + 1);
|
|
4060
4466
|
} else {
|
|
@@ -4065,6 +4471,12 @@ function createDiffServer(diffContent) {
|
|
|
4065
4471
|
});
|
|
4066
4472
|
|
|
4067
4473
|
ctx.server.listen(attemptPort, () => {
|
|
4474
|
+
if (serverStarted) {
|
|
4475
|
+
// Race condition: server started on multiple ports, close this one
|
|
4476
|
+
try { ctx.server.close(); } catch (_) {}
|
|
4477
|
+
return;
|
|
4478
|
+
}
|
|
4479
|
+
serverStarted = true;
|
|
4068
4480
|
ctx.port = attemptPort;
|
|
4069
4481
|
ctx.heartbeat = setInterval(() => broadcast("ping"), 25000);
|
|
4070
4482
|
console.log(`Diff viewer started: http://localhost:${attemptPort}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reviw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Lightweight file reviewer with in-browser comments for CSV, TSV, Markdown, and Git diffs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,10 +9,6 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"cli.cjs"
|
|
11
11
|
],
|
|
12
|
-
"scripts": {
|
|
13
|
-
"test": "vitest run",
|
|
14
|
-
"test:watch": "vitest"
|
|
15
|
-
},
|
|
16
12
|
"license": "MIT",
|
|
17
13
|
"author": "kazuph",
|
|
18
14
|
"publishConfig": {
|
|
@@ -31,5 +27,9 @@
|
|
|
31
27
|
"@playwright/test": "^1.57.0",
|
|
32
28
|
"playwright": "^1.57.0",
|
|
33
29
|
"vitest": "^4.0.14"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"test:watch": "vitest"
|
|
34
34
|
}
|
|
35
|
-
}
|
|
35
|
+
}
|