reviw 0.15.2 → 0.16.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/README.md +3 -0
- package/cli.cjs +309 -63
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,6 +51,9 @@ A lightweight browser-based tool for reviewing and annotating tabular data, text
|
|
|
51
51
|
- **Real-time updates**: Hot reload on file changes via SSE
|
|
52
52
|
- **Comment persistence**: Auto-save comments to localStorage with recovery modal
|
|
53
53
|
- **Keyboard shortcuts**: Cmd/Ctrl+Enter to open submit modal
|
|
54
|
+
- **Multi-tab sync**: Submit from any tab closes all tabs for the same file
|
|
55
|
+
- **Server detection**: Reuse existing server instead of starting a new one (via lock files)
|
|
56
|
+
- **Tab activation (macOS)**: Automatically activates existing browser tab via AppleScript
|
|
54
57
|
|
|
55
58
|
### Output
|
|
56
59
|
- YAML format with file, mode, row, col, value, and comment text
|
package/cli.cjs
CHANGED
|
@@ -15,7 +15,8 @@ const fs = require("fs");
|
|
|
15
15
|
const http = require("http");
|
|
16
16
|
const os = require("os");
|
|
17
17
|
const path = require("path");
|
|
18
|
-
const
|
|
18
|
+
const crypto = require("crypto");
|
|
19
|
+
const { spawn, execSync, spawnSync } = require("child_process");
|
|
19
20
|
const chardet = require("chardet");
|
|
20
21
|
const iconv = require("iconv-lite");
|
|
21
22
|
const marked = require("marked");
|
|
@@ -1764,7 +1765,17 @@ function diffHtmlTemplate(diffData) {
|
|
|
1764
1765
|
let es = null;
|
|
1765
1766
|
const connect = () => {
|
|
1766
1767
|
es = new EventSource('/sse');
|
|
1767
|
-
es.onmessage = ev => {
|
|
1768
|
+
es.onmessage = ev => {
|
|
1769
|
+
if (ev.data === 'reload') location.reload();
|
|
1770
|
+
if (ev.data === 'submitted') {
|
|
1771
|
+
// Another tab submitted - try to close this tab
|
|
1772
|
+
window.close();
|
|
1773
|
+
// If window.close() didn't work, show completion message
|
|
1774
|
+
setTimeout(() => {
|
|
1775
|
+
document.body.innerHTML = '<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;background:var(--bg,#1a1a2e);color:var(--text,#e0e0e0);font-family:system-ui,sans-serif;"><h1 style="font-size:2rem;margin-bottom:1rem;">✅ Review Submitted</h1><p style="color:var(--muted,#888);">Submitted from another tab. You can close this tab now.</p></div>';
|
|
1776
|
+
}, 100);
|
|
1777
|
+
}
|
|
1778
|
+
};
|
|
1768
1779
|
es.onerror = () => { es.close(); setTimeout(connect, 1500); };
|
|
1769
1780
|
};
|
|
1770
1781
|
connect();
|
|
@@ -2252,10 +2263,10 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
2252
2263
|
.md-preview video.video-preview { max-width: 100%; height: auto; border-radius: 8px; background: #000; }
|
|
2253
2264
|
.md-preview table video.video-preview {
|
|
2254
2265
|
display: block;
|
|
2255
|
-
width:
|
|
2266
|
+
width: 100%;
|
|
2256
2267
|
height: auto;
|
|
2257
|
-
|
|
2258
|
-
max-
|
|
2268
|
+
min-width: 120px;
|
|
2269
|
+
max-width: 250px;
|
|
2259
2270
|
}
|
|
2260
2271
|
.md-preview code { background: rgba(255,255,255,0.08); padding: 2px 4px; border-radius: 4px; }
|
|
2261
2272
|
.md-preview pre {
|
|
@@ -2383,9 +2394,9 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
2383
2394
|
}
|
|
2384
2395
|
/* Markdown tables in preview */
|
|
2385
2396
|
.md-preview table:not(.frontmatter-table table) {
|
|
2386
|
-
width:
|
|
2397
|
+
width: 100%;
|
|
2387
2398
|
border-collapse: collapse;
|
|
2388
|
-
table-layout:
|
|
2399
|
+
table-layout: fixed;
|
|
2389
2400
|
margin: 16px 0;
|
|
2390
2401
|
border: 1px solid var(--border);
|
|
2391
2402
|
border-radius: 8px;
|
|
@@ -2396,15 +2407,31 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
2396
2407
|
text-align: left;
|
|
2397
2408
|
border-bottom: 1px solid var(--border);
|
|
2398
2409
|
vertical-align: top;
|
|
2399
|
-
|
|
2410
|
+
word-break: break-word;
|
|
2411
|
+
overflow-wrap: anywhere;
|
|
2412
|
+
}
|
|
2413
|
+
.md-preview table:not(.frontmatter-table table) th:first-child,
|
|
2414
|
+
.md-preview table:not(.frontmatter-table table) td:first-child {
|
|
2415
|
+
width: 30%;
|
|
2416
|
+
min-width: 200px;
|
|
2417
|
+
}
|
|
2418
|
+
.md-preview table:not(.frontmatter-table table) th:last-child,
|
|
2419
|
+
.md-preview table:not(.frontmatter-table table) td:last-child {
|
|
2420
|
+
width: 180px;
|
|
2421
|
+
min-width: 180px;
|
|
2422
|
+
max-width: 180px;
|
|
2400
2423
|
}
|
|
2401
2424
|
.md-preview table:not(.frontmatter-table table) td:has(video),
|
|
2402
2425
|
.md-preview table:not(.frontmatter-table table) td:has(img) {
|
|
2403
|
-
padding:
|
|
2404
|
-
width: 1%;
|
|
2405
|
-
white-space: nowrap;
|
|
2426
|
+
padding: 4px;
|
|
2406
2427
|
line-height: 0;
|
|
2407
2428
|
}
|
|
2429
|
+
.md-preview table:not(.frontmatter-table table) td video,
|
|
2430
|
+
.md-preview table:not(.frontmatter-table table) td img {
|
|
2431
|
+
width: 100%;
|
|
2432
|
+
max-width: 100%;
|
|
2433
|
+
height: auto;
|
|
2434
|
+
}
|
|
2408
2435
|
.md-preview table:not(.frontmatter-table table) th {
|
|
2409
2436
|
background: var(--panel);
|
|
2410
2437
|
font-weight: 600;
|
|
@@ -3363,6 +3390,14 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
3363
3390
|
if (ev.data === 'reload') {
|
|
3364
3391
|
location.reload();
|
|
3365
3392
|
}
|
|
3393
|
+
if (ev.data === 'submitted') {
|
|
3394
|
+
// Another tab submitted - try to close this tab
|
|
3395
|
+
window.close();
|
|
3396
|
+
// If window.close() didn't work, show completion message
|
|
3397
|
+
setTimeout(() => {
|
|
3398
|
+
document.body.innerHTML = '<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;background:var(--bg,#1a1a2e);color:var(--text,#e0e0e0);font-family:system-ui,sans-serif;"><h1 style="font-size:2rem;margin-bottom:1rem;">✅ Review Submitted</h1><p style="color:var(--muted,#888);">Submitted from another tab. You can close this tab now.</p></div>';
|
|
3399
|
+
}, 100);
|
|
3400
|
+
}
|
|
3366
3401
|
};
|
|
3367
3402
|
es.onerror = () => {
|
|
3368
3403
|
es.close();
|
|
@@ -5613,6 +5648,229 @@ function readBody(req) {
|
|
|
5613
5648
|
const MAX_PORT_ATTEMPTS = 100;
|
|
5614
5649
|
const activeServers = new Map();
|
|
5615
5650
|
|
|
5651
|
+
// --- Lock File Management (for detecting existing servers) ---
|
|
5652
|
+
const LOCK_DIR = path.join(os.homedir(), '.reviw', 'locks');
|
|
5653
|
+
|
|
5654
|
+
function getLockFilePath(filePath) {
|
|
5655
|
+
// Use SHA256 hash of absolute path to prevent path traversal attacks
|
|
5656
|
+
const hash = crypto.createHash('sha256').update(path.resolve(filePath)).digest('hex').slice(0, 16);
|
|
5657
|
+
return path.join(LOCK_DIR, hash + '.lock');
|
|
5658
|
+
}
|
|
5659
|
+
|
|
5660
|
+
function ensureLockDir() {
|
|
5661
|
+
try {
|
|
5662
|
+
if (!fs.existsSync(LOCK_DIR)) {
|
|
5663
|
+
fs.mkdirSync(LOCK_DIR, { recursive: true, mode: 0o700 });
|
|
5664
|
+
}
|
|
5665
|
+
} catch (err) {
|
|
5666
|
+
// Ignore errors - locks are optional optimization
|
|
5667
|
+
}
|
|
5668
|
+
}
|
|
5669
|
+
|
|
5670
|
+
function writeLockFile(filePath, port) {
|
|
5671
|
+
try {
|
|
5672
|
+
ensureLockDir();
|
|
5673
|
+
const lockPath = getLockFilePath(filePath);
|
|
5674
|
+
const lockData = {
|
|
5675
|
+
pid: process.pid,
|
|
5676
|
+
port: port,
|
|
5677
|
+
file: path.resolve(filePath),
|
|
5678
|
+
created: Date.now()
|
|
5679
|
+
};
|
|
5680
|
+
fs.writeFileSync(lockPath, JSON.stringify(lockData), { mode: 0o600 });
|
|
5681
|
+
} catch (err) {
|
|
5682
|
+
// Ignore errors - locks are optional
|
|
5683
|
+
}
|
|
5684
|
+
}
|
|
5685
|
+
|
|
5686
|
+
function removeLockFile(filePath) {
|
|
5687
|
+
try {
|
|
5688
|
+
const lockPath = getLockFilePath(filePath);
|
|
5689
|
+
if (fs.existsSync(lockPath)) {
|
|
5690
|
+
fs.unlinkSync(lockPath);
|
|
5691
|
+
}
|
|
5692
|
+
} catch (err) {
|
|
5693
|
+
// Ignore errors
|
|
5694
|
+
}
|
|
5695
|
+
}
|
|
5696
|
+
|
|
5697
|
+
function checkExistingServer(filePath) {
|
|
5698
|
+
try {
|
|
5699
|
+
const lockPath = getLockFilePath(filePath);
|
|
5700
|
+
if (!fs.existsSync(lockPath)) {
|
|
5701
|
+
return null;
|
|
5702
|
+
}
|
|
5703
|
+
|
|
5704
|
+
const lockData = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
5705
|
+
|
|
5706
|
+
// Verify the process is still alive
|
|
5707
|
+
try {
|
|
5708
|
+
process.kill(lockData.pid, 0); // Signal 0 just checks if process exists
|
|
5709
|
+
} catch (err) {
|
|
5710
|
+
// Process doesn't exist - stale lock
|
|
5711
|
+
fs.unlinkSync(lockPath);
|
|
5712
|
+
return null;
|
|
5713
|
+
}
|
|
5714
|
+
|
|
5715
|
+
// Verify the server is actually responding
|
|
5716
|
+
return new Promise((resolve) => {
|
|
5717
|
+
const req = http.request({
|
|
5718
|
+
hostname: 'localhost',
|
|
5719
|
+
port: lockData.port,
|
|
5720
|
+
path: '/healthz',
|
|
5721
|
+
method: 'GET',
|
|
5722
|
+
timeout: 1000
|
|
5723
|
+
}, (res) => {
|
|
5724
|
+
if (res.statusCode === 200) {
|
|
5725
|
+
resolve(lockData);
|
|
5726
|
+
} else {
|
|
5727
|
+
// Server not healthy - remove stale lock
|
|
5728
|
+
try { fs.unlinkSync(lockPath); } catch (e) {}
|
|
5729
|
+
resolve(null);
|
|
5730
|
+
}
|
|
5731
|
+
});
|
|
5732
|
+
req.on('error', () => {
|
|
5733
|
+
// Server not responding - remove stale lock
|
|
5734
|
+
try { fs.unlinkSync(lockPath); } catch (e) {}
|
|
5735
|
+
resolve(null);
|
|
5736
|
+
});
|
|
5737
|
+
req.on('timeout', () => {
|
|
5738
|
+
req.destroy();
|
|
5739
|
+
try { fs.unlinkSync(lockPath); } catch (e) {}
|
|
5740
|
+
resolve(null);
|
|
5741
|
+
});
|
|
5742
|
+
req.end();
|
|
5743
|
+
});
|
|
5744
|
+
} catch (err) {
|
|
5745
|
+
return null;
|
|
5746
|
+
}
|
|
5747
|
+
}
|
|
5748
|
+
|
|
5749
|
+
// Try to activate an existing browser tab with the given URL (macOS only)
|
|
5750
|
+
// Returns true if a tab was activated, false otherwise
|
|
5751
|
+
function tryActivateExistingTab(url) {
|
|
5752
|
+
if (process.platform !== "darwin") {
|
|
5753
|
+
return false;
|
|
5754
|
+
}
|
|
5755
|
+
|
|
5756
|
+
try {
|
|
5757
|
+
// AppleScript to find and activate Chrome tab with URL
|
|
5758
|
+
// Uses "found" variable instead of return (AppleScript doesn't allow return at top level)
|
|
5759
|
+
const chromeScript = [
|
|
5760
|
+
'set found to false',
|
|
5761
|
+
'tell application "System Events"',
|
|
5762
|
+
' if exists process "Google Chrome" then',
|
|
5763
|
+
' tell application "Google Chrome"',
|
|
5764
|
+
' set targetUrl to "' + url + '"',
|
|
5765
|
+
' repeat with w in windows',
|
|
5766
|
+
' set tabIndex to 1',
|
|
5767
|
+
' repeat with t in tabs of w',
|
|
5768
|
+
' if URL of t starts with targetUrl then',
|
|
5769
|
+
' set active tab index of w to tabIndex',
|
|
5770
|
+
' set index of w to 1',
|
|
5771
|
+
' activate',
|
|
5772
|
+
' set found to true',
|
|
5773
|
+
' exit repeat',
|
|
5774
|
+
' end if',
|
|
5775
|
+
' set tabIndex to tabIndex + 1',
|
|
5776
|
+
' end repeat',
|
|
5777
|
+
' if found then exit repeat',
|
|
5778
|
+
' end repeat',
|
|
5779
|
+
' end tell',
|
|
5780
|
+
' end if',
|
|
5781
|
+
'end tell',
|
|
5782
|
+
'found'
|
|
5783
|
+
].join('\n');
|
|
5784
|
+
|
|
5785
|
+
const chromeResult = spawnSync('osascript', ['-e', chromeScript], {
|
|
5786
|
+
encoding: "utf8",
|
|
5787
|
+
timeout: 3000
|
|
5788
|
+
});
|
|
5789
|
+
|
|
5790
|
+
if (chromeResult.stdout && chromeResult.stdout.trim() === "true") {
|
|
5791
|
+
console.log("Activated existing Chrome tab: " + url);
|
|
5792
|
+
return true;
|
|
5793
|
+
}
|
|
5794
|
+
|
|
5795
|
+
// Try Safari as fallback
|
|
5796
|
+
const safariScript = [
|
|
5797
|
+
'set found to false',
|
|
5798
|
+
'tell application "System Events"',
|
|
5799
|
+
' if exists process "Safari" then',
|
|
5800
|
+
' tell application "Safari"',
|
|
5801
|
+
' set targetUrl to "' + url + '"',
|
|
5802
|
+
' repeat with w in windows',
|
|
5803
|
+
' repeat with t in tabs of w',
|
|
5804
|
+
' if URL of t starts with targetUrl then',
|
|
5805
|
+
' set current tab of w to t',
|
|
5806
|
+
' set index of w to 1',
|
|
5807
|
+
' activate',
|
|
5808
|
+
' set found to true',
|
|
5809
|
+
' exit repeat',
|
|
5810
|
+
' end if',
|
|
5811
|
+
' end repeat',
|
|
5812
|
+
' if found then exit repeat',
|
|
5813
|
+
' end repeat',
|
|
5814
|
+
' end tell',
|
|
5815
|
+
' end if',
|
|
5816
|
+
'end tell',
|
|
5817
|
+
'found'
|
|
5818
|
+
].join('\n');
|
|
5819
|
+
|
|
5820
|
+
const safariResult = spawnSync('osascript', ['-e', safariScript], {
|
|
5821
|
+
encoding: "utf8",
|
|
5822
|
+
timeout: 3000
|
|
5823
|
+
});
|
|
5824
|
+
|
|
5825
|
+
if (safariResult.stdout && safariResult.stdout.trim() === "true") {
|
|
5826
|
+
console.log("Activated existing Safari tab: " + url);
|
|
5827
|
+
return true;
|
|
5828
|
+
}
|
|
5829
|
+
|
|
5830
|
+
return false;
|
|
5831
|
+
} catch (err) {
|
|
5832
|
+
// AppleScript failed (not macOS, Chrome/Safari not installed, etc.)
|
|
5833
|
+
return false;
|
|
5834
|
+
}
|
|
5835
|
+
}
|
|
5836
|
+
|
|
5837
|
+
// Open browser with the given URL, trying to reuse existing tab first (macOS)
|
|
5838
|
+
function openBrowser(url, delay = 0) {
|
|
5839
|
+
const opener =
|
|
5840
|
+
process.platform === "darwin"
|
|
5841
|
+
? "open"
|
|
5842
|
+
: process.platform === "win32"
|
|
5843
|
+
? "start"
|
|
5844
|
+
: "xdg-open";
|
|
5845
|
+
|
|
5846
|
+
setTimeout(function() {
|
|
5847
|
+
// On macOS, try to activate existing tab first
|
|
5848
|
+
if (process.platform === "darwin") {
|
|
5849
|
+
var activated = tryActivateExistingTab(url);
|
|
5850
|
+
if (activated) {
|
|
5851
|
+
return; // Successfully activated existing tab
|
|
5852
|
+
}
|
|
5853
|
+
// If activation failed, fall through to open new tab
|
|
5854
|
+
}
|
|
5855
|
+
|
|
5856
|
+
try {
|
|
5857
|
+
var child = spawn(opener, [url], { stdio: "ignore", detached: true });
|
|
5858
|
+
child.on('error', function(err) {
|
|
5859
|
+
console.warn(
|
|
5860
|
+
"Failed to open browser automatically. Please open this URL manually:",
|
|
5861
|
+
url
|
|
5862
|
+
);
|
|
5863
|
+
});
|
|
5864
|
+
child.unref();
|
|
5865
|
+
} catch (err) {
|
|
5866
|
+
console.warn(
|
|
5867
|
+
"Failed to open browser automatically. Please open this URL manually:",
|
|
5868
|
+
url
|
|
5869
|
+
);
|
|
5870
|
+
}
|
|
5871
|
+
}, delay);
|
|
5872
|
+
}
|
|
5873
|
+
|
|
5616
5874
|
function outputAllResults() {
|
|
5617
5875
|
console.log("=== All comments received ===");
|
|
5618
5876
|
if (allResults.length === 1) {
|
|
@@ -5724,6 +5982,7 @@ function createFileServer(filePath, fileIndex = 0) {
|
|
|
5724
5982
|
ctx.server = null;
|
|
5725
5983
|
}
|
|
5726
5984
|
activeServers.delete(filePath);
|
|
5985
|
+
removeLockFile(filePath); // Clean up lock file
|
|
5727
5986
|
if (result) allResults.push(result);
|
|
5728
5987
|
serversRunning--;
|
|
5729
5988
|
console.log(`Server for ${baseName} closed. (${serversRunning} remaining)`);
|
|
@@ -5764,12 +6023,15 @@ function createFileServer(filePath, fileIndex = 0) {
|
|
|
5764
6023
|
}
|
|
5765
6024
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
5766
6025
|
res.end("bye");
|
|
5767
|
-
|
|
6026
|
+
// Notify all tabs to close before shutting down
|
|
6027
|
+
broadcast("submitted");
|
|
6028
|
+
setTimeout(() => shutdownServer(payload), 300);
|
|
5768
6029
|
} catch (err) {
|
|
5769
6030
|
console.error("payload parse error", err);
|
|
5770
6031
|
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
5771
6032
|
res.end("bad request");
|
|
5772
|
-
|
|
6033
|
+
broadcast("submitted");
|
|
6034
|
+
setTimeout(() => shutdownServer(null), 300);
|
|
5773
6035
|
}
|
|
5774
6036
|
return;
|
|
5775
6037
|
}
|
|
@@ -5931,33 +6193,12 @@ function createFileServer(filePath, fileIndex = 0) {
|
|
|
5931
6193
|
nextPort = attemptPort + 1;
|
|
5932
6194
|
activeServers.set(filePath, ctx);
|
|
5933
6195
|
console.log(`Viewer started: http://localhost:${attemptPort} (file: ${baseName})`);
|
|
6196
|
+
writeLockFile(filePath, attemptPort); // Write lock file for server detection
|
|
5934
6197
|
if (!noOpen) {
|
|
5935
6198
|
const url = `http://localhost:${attemptPort}`;
|
|
5936
|
-
const opener =
|
|
5937
|
-
process.platform === "darwin"
|
|
5938
|
-
? "open"
|
|
5939
|
-
: process.platform === "win32"
|
|
5940
|
-
? "start"
|
|
5941
|
-
: "xdg-open";
|
|
5942
6199
|
// Add delay for multiple files to avoid browser ignoring rapid open commands
|
|
5943
6200
|
const delay = fileIndex * 300;
|
|
5944
|
-
|
|
5945
|
-
try {
|
|
5946
|
-
const child = spawn(opener, [url], { stdio: "ignore", detached: true });
|
|
5947
|
-
child.on('error', (err) => {
|
|
5948
|
-
console.warn(
|
|
5949
|
-
"Failed to open browser automatically. Please open this URL manually:",
|
|
5950
|
-
url,
|
|
5951
|
-
);
|
|
5952
|
-
});
|
|
5953
|
-
child.unref();
|
|
5954
|
-
} catch (err) {
|
|
5955
|
-
console.warn(
|
|
5956
|
-
"Failed to open browser automatically. Please open this URL manually:",
|
|
5957
|
-
url,
|
|
5958
|
-
);
|
|
5959
|
-
}
|
|
5960
|
-
}, delay);
|
|
6201
|
+
openBrowser(url, delay);
|
|
5961
6202
|
}
|
|
5962
6203
|
startWatcher();
|
|
5963
6204
|
resolve(ctx);
|
|
@@ -6044,12 +6285,15 @@ function createDiffServer(diffContent) {
|
|
|
6044
6285
|
}
|
|
6045
6286
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
6046
6287
|
res.end("bye");
|
|
6047
|
-
|
|
6288
|
+
// Notify all tabs to close before shutting down
|
|
6289
|
+
broadcast("submitted");
|
|
6290
|
+
setTimeout(() => shutdownServer(payload), 300);
|
|
6048
6291
|
} catch (err) {
|
|
6049
6292
|
console.error("payload parse error", err);
|
|
6050
6293
|
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
6051
6294
|
res.end("bad request");
|
|
6052
|
-
|
|
6295
|
+
broadcast("submitted");
|
|
6296
|
+
setTimeout(() => shutdownServer(null), 300);
|
|
6053
6297
|
}
|
|
6054
6298
|
return;
|
|
6055
6299
|
}
|
|
@@ -6111,27 +6355,7 @@ function createDiffServer(diffContent) {
|
|
|
6111
6355
|
console.log(`Diff viewer started: http://localhost:${attemptPort}`);
|
|
6112
6356
|
if (!noOpen) {
|
|
6113
6357
|
const url = `http://localhost:${attemptPort}`;
|
|
6114
|
-
|
|
6115
|
-
process.platform === "darwin"
|
|
6116
|
-
? "open"
|
|
6117
|
-
: process.platform === "win32"
|
|
6118
|
-
? "start"
|
|
6119
|
-
: "xdg-open";
|
|
6120
|
-
try {
|
|
6121
|
-
const child = spawn(opener, [url], { stdio: "ignore", detached: true });
|
|
6122
|
-
child.on('error', (err) => {
|
|
6123
|
-
console.warn(
|
|
6124
|
-
"Failed to open browser automatically. Please open this URL manually:",
|
|
6125
|
-
url,
|
|
6126
|
-
);
|
|
6127
|
-
});
|
|
6128
|
-
child.unref();
|
|
6129
|
-
} catch (err) {
|
|
6130
|
-
console.warn(
|
|
6131
|
-
"Failed to open browser automatically. Please open this URL manually:",
|
|
6132
|
-
url,
|
|
6133
|
-
);
|
|
6134
|
-
}
|
|
6358
|
+
openBrowser(url, 0);
|
|
6135
6359
|
}
|
|
6136
6360
|
resolve(ctx);
|
|
6137
6361
|
});
|
|
@@ -6182,10 +6406,32 @@ if (require.main === module) {
|
|
|
6182
6406
|
}
|
|
6183
6407
|
} else if (resolvedPaths.length > 0) {
|
|
6184
6408
|
// File mode: files specified
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
for (
|
|
6188
|
-
await
|
|
6409
|
+
// Check for existing servers first
|
|
6410
|
+
let filesToStart = [];
|
|
6411
|
+
for (const fp of resolvedPaths) {
|
|
6412
|
+
const existing = await checkExistingServer(fp);
|
|
6413
|
+
if (existing) {
|
|
6414
|
+
console.log(`Server already running for ${path.basename(fp)} on port ${existing.port}`);
|
|
6415
|
+
const url = `http://localhost:${existing.port}`;
|
|
6416
|
+
if (!noOpen) {
|
|
6417
|
+
openBrowser(url, 0);
|
|
6418
|
+
}
|
|
6419
|
+
} else {
|
|
6420
|
+
filesToStart.push(fp);
|
|
6421
|
+
}
|
|
6422
|
+
}
|
|
6423
|
+
|
|
6424
|
+
if (filesToStart.length === 0) {
|
|
6425
|
+
console.log("All files already have running servers. Activating existing tabs.");
|
|
6426
|
+
// Wait a moment for browser activation to complete before exiting
|
|
6427
|
+
setTimeout(() => process.exit(0), 500);
|
|
6428
|
+
return;
|
|
6429
|
+
}
|
|
6430
|
+
|
|
6431
|
+
console.log(`Starting servers for ${filesToStart.length} file(s)...`);
|
|
6432
|
+
serversRunning = filesToStart.length;
|
|
6433
|
+
for (let i = 0; i < filesToStart.length; i++) {
|
|
6434
|
+
await createFileServer(filesToStart[i], i);
|
|
6189
6435
|
}
|
|
6190
6436
|
console.log("Close all browser tabs or Submit & Exit to finish.");
|
|
6191
6437
|
} else {
|