reviw 0.15.3 → 0.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/cli.cjs +392 -83
- 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");
|
|
@@ -1282,11 +1283,11 @@ function diffHtmlTemplate(diffData) {
|
|
|
1282
1283
|
<label for="global-comment">Overall comment (optional)</label>
|
|
1283
1284
|
<textarea id="global-comment" placeholder="Add a summary or overall feedback..."></textarea>
|
|
1284
1285
|
<div class="modal-checkboxes">
|
|
1285
|
-
<label><input type="checkbox" id="prompt-subagents" checked />
|
|
1286
|
-
<label><input type="checkbox" id="prompt-reviw" checked /> Open in REVIW next time
|
|
1287
|
-
<label><input type="checkbox" id="prompt-screenshots" checked /> Update all screenshots
|
|
1288
|
-
<label><input type="checkbox" id="prompt-user-feedback-todo" checked /> Add
|
|
1289
|
-
<label><input type="checkbox" id="prompt-deep-dive" checked />
|
|
1286
|
+
<label><input type="checkbox" id="prompt-subagents" checked /> 🤖 Delegate to sub-agents (implement, verify, report)</label>
|
|
1287
|
+
<label><input type="checkbox" id="prompt-reviw" checked /> 👁️ Open in REVIW next time</label>
|
|
1288
|
+
<label><input type="checkbox" id="prompt-screenshots" checked /> 📸 Update all screenshots/videos</label>
|
|
1289
|
+
<label><input type="checkbox" id="prompt-user-feedback-todo" checked /> ✅ Add feedback to Todo (require approval)</label>
|
|
1290
|
+
<label><input type="checkbox" id="prompt-deep-dive" checked /> 🔍 Probe requirements before implementing</label>
|
|
1290
1291
|
</div>
|
|
1291
1292
|
<div class="modal-actions">
|
|
1292
1293
|
<button id="modal-cancel">Cancel</button>
|
|
@@ -1656,13 +1657,13 @@ function diffHtmlTemplate(diffData) {
|
|
|
1656
1657
|
const modalSummary = document.getElementById('modal-summary');
|
|
1657
1658
|
const globalCommentInput = document.getElementById('global-comment');
|
|
1658
1659
|
|
|
1659
|
-
// Prompt checkboxes
|
|
1660
|
+
// Prompt checkboxes - text is the strong enforcement prompt for YAML output
|
|
1660
1661
|
const promptCheckboxes = [
|
|
1661
|
-
{ id: 'prompt-subagents', text: '
|
|
1662
|
-
{ id: 'prompt-reviw', text: '
|
|
1663
|
-
{ id: 'prompt-screenshots', text: '
|
|
1664
|
-
{ id: 'prompt-user-feedback-todo', text: "Add
|
|
1665
|
-
{ id: 'prompt-deep-dive', text: "Before
|
|
1662
|
+
{ id: 'prompt-subagents', text: 'MANDATORY: You MUST delegate ALL implementation, verification, and report creation to sub-agents. Direct execution on the main thread is PROHIBITED.' },
|
|
1663
|
+
{ id: 'prompt-reviw', text: 'REQUIRED: Before reporting completion, you MUST open the result in REVIW for user review. Skipping this step is NOT allowed.' },
|
|
1664
|
+
{ id: 'prompt-screenshots', text: 'MANDATORY: You MUST update ALL screenshots and videos as evidence. Reports without visual proof are REJECTED.' },
|
|
1665
|
+
{ id: 'prompt-user-feedback-todo', text: "STRICT RULE: Add ALL user feedback to the Todo list. You are FORBIDDEN from marking any item complete without explicit user approval." },
|
|
1666
|
+
{ id: 'prompt-deep-dive', text: "REQUIRED: Before ANY implementation, you MUST deeply probe the user's requirements using AskUserQuestion and EnterPlanMode. Starting implementation without thorough requirement analysis is PROHIBITED." }
|
|
1666
1667
|
];
|
|
1667
1668
|
const PROMPT_STORAGE_KEY = 'reviw-prompt-prefs';
|
|
1668
1669
|
|
|
@@ -1709,7 +1710,7 @@ function diffHtmlTemplate(diffData) {
|
|
|
1709
1710
|
}
|
|
1710
1711
|
|
|
1711
1712
|
function payload(reason) {
|
|
1712
|
-
const data = { file: FILE_NAME, mode: MODE, reason,
|
|
1713
|
+
const data = { file: FILE_NAME, mode: MODE, submittedBy: reason, submittedAt: new Date().toISOString(), comments: Object.values(comments) };
|
|
1713
1714
|
if (globalComment.trim()) data.summary = globalComment.trim();
|
|
1714
1715
|
const prompts = getSelectedPrompts();
|
|
1715
1716
|
if (prompts.length > 0) data.prompts = prompts;
|
|
@@ -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();
|
|
@@ -2385,7 +2396,7 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
2385
2396
|
.md-preview table:not(.frontmatter-table table) {
|
|
2386
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,14 +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;
|
|
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;
|
|
2399
2423
|
}
|
|
2400
2424
|
.md-preview table:not(.frontmatter-table table) td:has(video),
|
|
2401
2425
|
.md-preview table:not(.frontmatter-table table) td:has(img) {
|
|
2402
2426
|
padding: 4px;
|
|
2403
|
-
min-width: 150px;
|
|
2404
|
-
white-space: nowrap;
|
|
2405
2427
|
line-height: 0;
|
|
2406
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
|
+
}
|
|
2407
2435
|
.md-preview table:not(.frontmatter-table table) th {
|
|
2408
2436
|
background: var(--panel);
|
|
2409
2437
|
font-weight: 600;
|
|
@@ -3188,11 +3216,11 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
3188
3216
|
<label for="global-comment">Overall comment (optional)</label>
|
|
3189
3217
|
<textarea id="global-comment" placeholder="Add a summary or overall feedback..."></textarea>
|
|
3190
3218
|
<div class="modal-checkboxes">
|
|
3191
|
-
<label><input type="checkbox" id="prompt-subagents" checked />
|
|
3192
|
-
<label><input type="checkbox" id="prompt-reviw" checked /> Open in REVIW next time
|
|
3193
|
-
<label><input type="checkbox" id="prompt-screenshots" checked /> Update all screenshots
|
|
3194
|
-
<label><input type="checkbox" id="prompt-user-feedback-todo" checked /> Add
|
|
3195
|
-
<label><input type="checkbox" id="prompt-deep-dive" checked />
|
|
3219
|
+
<label><input type="checkbox" id="prompt-subagents" checked /> 🤖 Delegate to sub-agents (implement, verify, report)</label>
|
|
3220
|
+
<label><input type="checkbox" id="prompt-reviw" checked /> 👁️ Open in REVIW next time</label>
|
|
3221
|
+
<label><input type="checkbox" id="prompt-screenshots" checked /> 📸 Update all screenshots/videos</label>
|
|
3222
|
+
<label><input type="checkbox" id="prompt-user-feedback-todo" checked /> ✅ Add feedback to Todo (require approval)</label>
|
|
3223
|
+
<label><input type="checkbox" id="prompt-deep-dive" checked /> 🔍 Probe requirements before implementing</label>
|
|
3196
3224
|
</div>
|
|
3197
3225
|
<div class="modal-actions">
|
|
3198
3226
|
<button id="modal-cancel">Cancel</button>
|
|
@@ -3362,6 +3390,14 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
3362
3390
|
if (ev.data === 'reload') {
|
|
3363
3391
|
location.reload();
|
|
3364
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
|
+
}
|
|
3365
3401
|
};
|
|
3366
3402
|
es.onerror = () => {
|
|
3367
3403
|
es.close();
|
|
@@ -4181,13 +4217,13 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4181
4217
|
const modalCancel = document.getElementById('modal-cancel');
|
|
4182
4218
|
const modalSubmit = document.getElementById('modal-submit');
|
|
4183
4219
|
|
|
4184
|
-
// Prompt checkboxes
|
|
4220
|
+
// Prompt checkboxes - text is the strong enforcement prompt for YAML output
|
|
4185
4221
|
const promptCheckboxes = [
|
|
4186
|
-
{ id: 'prompt-subagents', text: '
|
|
4187
|
-
{ id: 'prompt-reviw', text: '
|
|
4188
|
-
{ id: 'prompt-screenshots', text: '
|
|
4189
|
-
{ id: 'prompt-user-feedback-todo', text: "Add
|
|
4190
|
-
{ id: 'prompt-deep-dive', text: "Before
|
|
4222
|
+
{ id: 'prompt-subagents', text: 'MANDATORY: You MUST delegate ALL implementation, verification, and report creation to sub-agents. Direct execution on the main thread is PROHIBITED.' },
|
|
4223
|
+
{ id: 'prompt-reviw', text: 'REQUIRED: Before reporting completion, you MUST open the result in REVIW for user review. Skipping this step is NOT allowed.' },
|
|
4224
|
+
{ id: 'prompt-screenshots', text: 'MANDATORY: You MUST update ALL screenshots and videos as evidence. Reports without visual proof are REJECTED.' },
|
|
4225
|
+
{ id: 'prompt-user-feedback-todo', text: "STRICT RULE: Add ALL user feedback to the Todo list. You are FORBIDDEN from marking any item complete without explicit user approval." },
|
|
4226
|
+
{ id: 'prompt-deep-dive', text: "REQUIRED: Before ANY implementation, you MUST deeply probe the user's requirements using AskUserQuestion and EnterPlanMode. Starting implementation without thorough requirement analysis is PROHIBITED." }
|
|
4191
4227
|
];
|
|
4192
4228
|
const PROMPT_STORAGE_KEY = 'reviw-prompt-prefs';
|
|
4193
4229
|
|
|
@@ -4233,13 +4269,75 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4233
4269
|
return prompts;
|
|
4234
4270
|
}
|
|
4235
4271
|
|
|
4272
|
+
// Find nearest heading for a given line number (markdown context)
|
|
4273
|
+
function findNearestHeading(lineNum) {
|
|
4274
|
+
let nearestHeading = null;
|
|
4275
|
+
for (let i = lineNum - 1; i >= 0; i--) {
|
|
4276
|
+
const line = DATA[i] ? DATA[i][0] : '';
|
|
4277
|
+
const match = line.match(/^(#{1,6})\\s+(.+)/);
|
|
4278
|
+
if (match) {
|
|
4279
|
+
nearestHeading = match[2].trim();
|
|
4280
|
+
break;
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
4283
|
+
return nearestHeading;
|
|
4284
|
+
}
|
|
4285
|
+
|
|
4286
|
+
// Check if line is inside a table
|
|
4287
|
+
function getTableContext(lineNum) {
|
|
4288
|
+
const line = DATA[lineNum] ? DATA[lineNum][0] : '';
|
|
4289
|
+
if (!line.includes('|')) return null;
|
|
4290
|
+
// Find table header (look backwards for header row)
|
|
4291
|
+
for (let i = lineNum; i >= 0; i--) {
|
|
4292
|
+
const l = DATA[i] ? DATA[i][0] : '';
|
|
4293
|
+
if (!l.includes('|')) break;
|
|
4294
|
+
// Check if next line is separator (---|---)
|
|
4295
|
+
const nextLine = DATA[i + 1] ? DATA[i + 1][0] : '';
|
|
4296
|
+
if (nextLine && nextLine.match(/^\\|?[\\s-:|]+\\|/)) {
|
|
4297
|
+
// This is the header row
|
|
4298
|
+
return l.replace(/^\\|\\s*/, '').replace(/\\s*\\|$/, '').split('|').map(h => h.trim()).slice(0, 3).join(' | ') + (l.split('|').length > 4 ? ' ...' : '');
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
return null;
|
|
4302
|
+
}
|
|
4303
|
+
|
|
4304
|
+
// Transform comments for markdown mode
|
|
4305
|
+
function transformMarkdownComments(rawComments) {
|
|
4306
|
+
return rawComments.map(c => {
|
|
4307
|
+
const lineNum = c.row || c.startRow || 0;
|
|
4308
|
+
const section = findNearestHeading(lineNum);
|
|
4309
|
+
const tableHeader = getTableContext(lineNum);
|
|
4310
|
+
const content = c.content || c.value || '';
|
|
4311
|
+
const truncatedContent = content.length > 60 ? content.substring(0, 60) + '...' : content;
|
|
4312
|
+
|
|
4313
|
+
const transformed = {
|
|
4314
|
+
line: lineNum + 1,
|
|
4315
|
+
context: {}
|
|
4316
|
+
};
|
|
4317
|
+
if (section) transformed.context.section = section;
|
|
4318
|
+
if (tableHeader) transformed.context.table = tableHeader;
|
|
4319
|
+
if (truncatedContent) transformed.context.content = truncatedContent;
|
|
4320
|
+
transformed.comment = c.text;
|
|
4321
|
+
|
|
4322
|
+
if (c.isRange) {
|
|
4323
|
+
transformed.lineEnd = (c.endRow || c.startRow) + 1;
|
|
4324
|
+
}
|
|
4325
|
+
return transformed;
|
|
4326
|
+
});
|
|
4327
|
+
}
|
|
4328
|
+
|
|
4236
4329
|
function payload(reason) {
|
|
4330
|
+
const rawComments = Object.values(comments);
|
|
4331
|
+
const transformedComments = MODE === 'markdown'
|
|
4332
|
+
? transformMarkdownComments(rawComments)
|
|
4333
|
+
: rawComments;
|
|
4334
|
+
|
|
4237
4335
|
const data = {
|
|
4238
4336
|
file: FILE_NAME,
|
|
4239
4337
|
mode: MODE,
|
|
4240
|
-
reason,
|
|
4241
|
-
|
|
4242
|
-
comments:
|
|
4338
|
+
submittedBy: reason,
|
|
4339
|
+
submittedAt: new Date().toISOString(),
|
|
4340
|
+
comments: transformedComments
|
|
4243
4341
|
};
|
|
4244
4342
|
if (globalComment.trim()) {
|
|
4245
4343
|
data.summary = globalComment.trim();
|
|
@@ -5612,6 +5710,229 @@ function readBody(req) {
|
|
|
5612
5710
|
const MAX_PORT_ATTEMPTS = 100;
|
|
5613
5711
|
const activeServers = new Map();
|
|
5614
5712
|
|
|
5713
|
+
// --- Lock File Management (for detecting existing servers) ---
|
|
5714
|
+
const LOCK_DIR = path.join(os.homedir(), '.reviw', 'locks');
|
|
5715
|
+
|
|
5716
|
+
function getLockFilePath(filePath) {
|
|
5717
|
+
// Use SHA256 hash of absolute path to prevent path traversal attacks
|
|
5718
|
+
const hash = crypto.createHash('sha256').update(path.resolve(filePath)).digest('hex').slice(0, 16);
|
|
5719
|
+
return path.join(LOCK_DIR, hash + '.lock');
|
|
5720
|
+
}
|
|
5721
|
+
|
|
5722
|
+
function ensureLockDir() {
|
|
5723
|
+
try {
|
|
5724
|
+
if (!fs.existsSync(LOCK_DIR)) {
|
|
5725
|
+
fs.mkdirSync(LOCK_DIR, { recursive: true, mode: 0o700 });
|
|
5726
|
+
}
|
|
5727
|
+
} catch (err) {
|
|
5728
|
+
// Ignore errors - locks are optional optimization
|
|
5729
|
+
}
|
|
5730
|
+
}
|
|
5731
|
+
|
|
5732
|
+
function writeLockFile(filePath, port) {
|
|
5733
|
+
try {
|
|
5734
|
+
ensureLockDir();
|
|
5735
|
+
const lockPath = getLockFilePath(filePath);
|
|
5736
|
+
const lockData = {
|
|
5737
|
+
pid: process.pid,
|
|
5738
|
+
port: port,
|
|
5739
|
+
file: path.resolve(filePath),
|
|
5740
|
+
created: Date.now()
|
|
5741
|
+
};
|
|
5742
|
+
fs.writeFileSync(lockPath, JSON.stringify(lockData), { mode: 0o600 });
|
|
5743
|
+
} catch (err) {
|
|
5744
|
+
// Ignore errors - locks are optional
|
|
5745
|
+
}
|
|
5746
|
+
}
|
|
5747
|
+
|
|
5748
|
+
function removeLockFile(filePath) {
|
|
5749
|
+
try {
|
|
5750
|
+
const lockPath = getLockFilePath(filePath);
|
|
5751
|
+
if (fs.existsSync(lockPath)) {
|
|
5752
|
+
fs.unlinkSync(lockPath);
|
|
5753
|
+
}
|
|
5754
|
+
} catch (err) {
|
|
5755
|
+
// Ignore errors
|
|
5756
|
+
}
|
|
5757
|
+
}
|
|
5758
|
+
|
|
5759
|
+
function checkExistingServer(filePath) {
|
|
5760
|
+
try {
|
|
5761
|
+
const lockPath = getLockFilePath(filePath);
|
|
5762
|
+
if (!fs.existsSync(lockPath)) {
|
|
5763
|
+
return null;
|
|
5764
|
+
}
|
|
5765
|
+
|
|
5766
|
+
const lockData = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
5767
|
+
|
|
5768
|
+
// Verify the process is still alive
|
|
5769
|
+
try {
|
|
5770
|
+
process.kill(lockData.pid, 0); // Signal 0 just checks if process exists
|
|
5771
|
+
} catch (err) {
|
|
5772
|
+
// Process doesn't exist - stale lock
|
|
5773
|
+
fs.unlinkSync(lockPath);
|
|
5774
|
+
return null;
|
|
5775
|
+
}
|
|
5776
|
+
|
|
5777
|
+
// Verify the server is actually responding
|
|
5778
|
+
return new Promise((resolve) => {
|
|
5779
|
+
const req = http.request({
|
|
5780
|
+
hostname: 'localhost',
|
|
5781
|
+
port: lockData.port,
|
|
5782
|
+
path: '/healthz',
|
|
5783
|
+
method: 'GET',
|
|
5784
|
+
timeout: 1000
|
|
5785
|
+
}, (res) => {
|
|
5786
|
+
if (res.statusCode === 200) {
|
|
5787
|
+
resolve(lockData);
|
|
5788
|
+
} else {
|
|
5789
|
+
// Server not healthy - remove stale lock
|
|
5790
|
+
try { fs.unlinkSync(lockPath); } catch (e) {}
|
|
5791
|
+
resolve(null);
|
|
5792
|
+
}
|
|
5793
|
+
});
|
|
5794
|
+
req.on('error', () => {
|
|
5795
|
+
// Server not responding - remove stale lock
|
|
5796
|
+
try { fs.unlinkSync(lockPath); } catch (e) {}
|
|
5797
|
+
resolve(null);
|
|
5798
|
+
});
|
|
5799
|
+
req.on('timeout', () => {
|
|
5800
|
+
req.destroy();
|
|
5801
|
+
try { fs.unlinkSync(lockPath); } catch (e) {}
|
|
5802
|
+
resolve(null);
|
|
5803
|
+
});
|
|
5804
|
+
req.end();
|
|
5805
|
+
});
|
|
5806
|
+
} catch (err) {
|
|
5807
|
+
return null;
|
|
5808
|
+
}
|
|
5809
|
+
}
|
|
5810
|
+
|
|
5811
|
+
// Try to activate an existing browser tab with the given URL (macOS only)
|
|
5812
|
+
// Returns true if a tab was activated, false otherwise
|
|
5813
|
+
function tryActivateExistingTab(url) {
|
|
5814
|
+
if (process.platform !== "darwin") {
|
|
5815
|
+
return false;
|
|
5816
|
+
}
|
|
5817
|
+
|
|
5818
|
+
try {
|
|
5819
|
+
// AppleScript to find and activate Chrome tab with URL
|
|
5820
|
+
// Uses "found" variable instead of return (AppleScript doesn't allow return at top level)
|
|
5821
|
+
const chromeScript = [
|
|
5822
|
+
'set found to false',
|
|
5823
|
+
'tell application "System Events"',
|
|
5824
|
+
' if exists process "Google Chrome" then',
|
|
5825
|
+
' tell application "Google Chrome"',
|
|
5826
|
+
' set targetUrl to "' + url + '"',
|
|
5827
|
+
' repeat with w in windows',
|
|
5828
|
+
' set tabIndex to 1',
|
|
5829
|
+
' repeat with t in tabs of w',
|
|
5830
|
+
' if URL of t starts with targetUrl then',
|
|
5831
|
+
' set active tab index of w to tabIndex',
|
|
5832
|
+
' set index of w to 1',
|
|
5833
|
+
' activate',
|
|
5834
|
+
' set found to true',
|
|
5835
|
+
' exit repeat',
|
|
5836
|
+
' end if',
|
|
5837
|
+
' set tabIndex to tabIndex + 1',
|
|
5838
|
+
' end repeat',
|
|
5839
|
+
' if found then exit repeat',
|
|
5840
|
+
' end repeat',
|
|
5841
|
+
' end tell',
|
|
5842
|
+
' end if',
|
|
5843
|
+
'end tell',
|
|
5844
|
+
'found'
|
|
5845
|
+
].join('\n');
|
|
5846
|
+
|
|
5847
|
+
const chromeResult = spawnSync('osascript', ['-e', chromeScript], {
|
|
5848
|
+
encoding: "utf8",
|
|
5849
|
+
timeout: 3000
|
|
5850
|
+
});
|
|
5851
|
+
|
|
5852
|
+
if (chromeResult.stdout && chromeResult.stdout.trim() === "true") {
|
|
5853
|
+
console.log("Activated existing Chrome tab: " + url);
|
|
5854
|
+
return true;
|
|
5855
|
+
}
|
|
5856
|
+
|
|
5857
|
+
// Try Safari as fallback
|
|
5858
|
+
const safariScript = [
|
|
5859
|
+
'set found to false',
|
|
5860
|
+
'tell application "System Events"',
|
|
5861
|
+
' if exists process "Safari" then',
|
|
5862
|
+
' tell application "Safari"',
|
|
5863
|
+
' set targetUrl to "' + url + '"',
|
|
5864
|
+
' repeat with w in windows',
|
|
5865
|
+
' repeat with t in tabs of w',
|
|
5866
|
+
' if URL of t starts with targetUrl then',
|
|
5867
|
+
' set current tab of w to t',
|
|
5868
|
+
' set index of w to 1',
|
|
5869
|
+
' activate',
|
|
5870
|
+
' set found to true',
|
|
5871
|
+
' exit repeat',
|
|
5872
|
+
' end if',
|
|
5873
|
+
' end repeat',
|
|
5874
|
+
' if found then exit repeat',
|
|
5875
|
+
' end repeat',
|
|
5876
|
+
' end tell',
|
|
5877
|
+
' end if',
|
|
5878
|
+
'end tell',
|
|
5879
|
+
'found'
|
|
5880
|
+
].join('\n');
|
|
5881
|
+
|
|
5882
|
+
const safariResult = spawnSync('osascript', ['-e', safariScript], {
|
|
5883
|
+
encoding: "utf8",
|
|
5884
|
+
timeout: 3000
|
|
5885
|
+
});
|
|
5886
|
+
|
|
5887
|
+
if (safariResult.stdout && safariResult.stdout.trim() === "true") {
|
|
5888
|
+
console.log("Activated existing Safari tab: " + url);
|
|
5889
|
+
return true;
|
|
5890
|
+
}
|
|
5891
|
+
|
|
5892
|
+
return false;
|
|
5893
|
+
} catch (err) {
|
|
5894
|
+
// AppleScript failed (not macOS, Chrome/Safari not installed, etc.)
|
|
5895
|
+
return false;
|
|
5896
|
+
}
|
|
5897
|
+
}
|
|
5898
|
+
|
|
5899
|
+
// Open browser with the given URL, trying to reuse existing tab first (macOS)
|
|
5900
|
+
function openBrowser(url, delay = 0) {
|
|
5901
|
+
const opener =
|
|
5902
|
+
process.platform === "darwin"
|
|
5903
|
+
? "open"
|
|
5904
|
+
: process.platform === "win32"
|
|
5905
|
+
? "start"
|
|
5906
|
+
: "xdg-open";
|
|
5907
|
+
|
|
5908
|
+
setTimeout(function() {
|
|
5909
|
+
// On macOS, try to activate existing tab first
|
|
5910
|
+
if (process.platform === "darwin") {
|
|
5911
|
+
var activated = tryActivateExistingTab(url);
|
|
5912
|
+
if (activated) {
|
|
5913
|
+
return; // Successfully activated existing tab
|
|
5914
|
+
}
|
|
5915
|
+
// If activation failed, fall through to open new tab
|
|
5916
|
+
}
|
|
5917
|
+
|
|
5918
|
+
try {
|
|
5919
|
+
var child = spawn(opener, [url], { stdio: "ignore", detached: true });
|
|
5920
|
+
child.on('error', function(err) {
|
|
5921
|
+
console.warn(
|
|
5922
|
+
"Failed to open browser automatically. Please open this URL manually:",
|
|
5923
|
+
url
|
|
5924
|
+
);
|
|
5925
|
+
});
|
|
5926
|
+
child.unref();
|
|
5927
|
+
} catch (err) {
|
|
5928
|
+
console.warn(
|
|
5929
|
+
"Failed to open browser automatically. Please open this URL manually:",
|
|
5930
|
+
url
|
|
5931
|
+
);
|
|
5932
|
+
}
|
|
5933
|
+
}, delay);
|
|
5934
|
+
}
|
|
5935
|
+
|
|
5615
5936
|
function outputAllResults() {
|
|
5616
5937
|
console.log("=== All comments received ===");
|
|
5617
5938
|
if (allResults.length === 1) {
|
|
@@ -5723,6 +6044,7 @@ function createFileServer(filePath, fileIndex = 0) {
|
|
|
5723
6044
|
ctx.server = null;
|
|
5724
6045
|
}
|
|
5725
6046
|
activeServers.delete(filePath);
|
|
6047
|
+
removeLockFile(filePath); // Clean up lock file
|
|
5726
6048
|
if (result) allResults.push(result);
|
|
5727
6049
|
serversRunning--;
|
|
5728
6050
|
console.log(`Server for ${baseName} closed. (${serversRunning} remaining)`);
|
|
@@ -5763,12 +6085,15 @@ function createFileServer(filePath, fileIndex = 0) {
|
|
|
5763
6085
|
}
|
|
5764
6086
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
5765
6087
|
res.end("bye");
|
|
5766
|
-
|
|
6088
|
+
// Notify all tabs to close before shutting down
|
|
6089
|
+
broadcast("submitted");
|
|
6090
|
+
setTimeout(() => shutdownServer(payload), 300);
|
|
5767
6091
|
} catch (err) {
|
|
5768
6092
|
console.error("payload parse error", err);
|
|
5769
6093
|
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
5770
6094
|
res.end("bad request");
|
|
5771
|
-
|
|
6095
|
+
broadcast("submitted");
|
|
6096
|
+
setTimeout(() => shutdownServer(null), 300);
|
|
5772
6097
|
}
|
|
5773
6098
|
return;
|
|
5774
6099
|
}
|
|
@@ -5930,33 +6255,12 @@ function createFileServer(filePath, fileIndex = 0) {
|
|
|
5930
6255
|
nextPort = attemptPort + 1;
|
|
5931
6256
|
activeServers.set(filePath, ctx);
|
|
5932
6257
|
console.log(`Viewer started: http://localhost:${attemptPort} (file: ${baseName})`);
|
|
6258
|
+
writeLockFile(filePath, attemptPort); // Write lock file for server detection
|
|
5933
6259
|
if (!noOpen) {
|
|
5934
6260
|
const url = `http://localhost:${attemptPort}`;
|
|
5935
|
-
const opener =
|
|
5936
|
-
process.platform === "darwin"
|
|
5937
|
-
? "open"
|
|
5938
|
-
: process.platform === "win32"
|
|
5939
|
-
? "start"
|
|
5940
|
-
: "xdg-open";
|
|
5941
6261
|
// Add delay for multiple files to avoid browser ignoring rapid open commands
|
|
5942
6262
|
const delay = fileIndex * 300;
|
|
5943
|
-
|
|
5944
|
-
try {
|
|
5945
|
-
const child = spawn(opener, [url], { stdio: "ignore", detached: true });
|
|
5946
|
-
child.on('error', (err) => {
|
|
5947
|
-
console.warn(
|
|
5948
|
-
"Failed to open browser automatically. Please open this URL manually:",
|
|
5949
|
-
url,
|
|
5950
|
-
);
|
|
5951
|
-
});
|
|
5952
|
-
child.unref();
|
|
5953
|
-
} catch (err) {
|
|
5954
|
-
console.warn(
|
|
5955
|
-
"Failed to open browser automatically. Please open this URL manually:",
|
|
5956
|
-
url,
|
|
5957
|
-
);
|
|
5958
|
-
}
|
|
5959
|
-
}, delay);
|
|
6263
|
+
openBrowser(url, delay);
|
|
5960
6264
|
}
|
|
5961
6265
|
startWatcher();
|
|
5962
6266
|
resolve(ctx);
|
|
@@ -6043,12 +6347,15 @@ function createDiffServer(diffContent) {
|
|
|
6043
6347
|
}
|
|
6044
6348
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
6045
6349
|
res.end("bye");
|
|
6046
|
-
|
|
6350
|
+
// Notify all tabs to close before shutting down
|
|
6351
|
+
broadcast("submitted");
|
|
6352
|
+
setTimeout(() => shutdownServer(payload), 300);
|
|
6047
6353
|
} catch (err) {
|
|
6048
6354
|
console.error("payload parse error", err);
|
|
6049
6355
|
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
6050
6356
|
res.end("bad request");
|
|
6051
|
-
|
|
6357
|
+
broadcast("submitted");
|
|
6358
|
+
setTimeout(() => shutdownServer(null), 300);
|
|
6052
6359
|
}
|
|
6053
6360
|
return;
|
|
6054
6361
|
}
|
|
@@ -6110,27 +6417,7 @@ function createDiffServer(diffContent) {
|
|
|
6110
6417
|
console.log(`Diff viewer started: http://localhost:${attemptPort}`);
|
|
6111
6418
|
if (!noOpen) {
|
|
6112
6419
|
const url = `http://localhost:${attemptPort}`;
|
|
6113
|
-
|
|
6114
|
-
process.platform === "darwin"
|
|
6115
|
-
? "open"
|
|
6116
|
-
: process.platform === "win32"
|
|
6117
|
-
? "start"
|
|
6118
|
-
: "xdg-open";
|
|
6119
|
-
try {
|
|
6120
|
-
const child = spawn(opener, [url], { stdio: "ignore", detached: true });
|
|
6121
|
-
child.on('error', (err) => {
|
|
6122
|
-
console.warn(
|
|
6123
|
-
"Failed to open browser automatically. Please open this URL manually:",
|
|
6124
|
-
url,
|
|
6125
|
-
);
|
|
6126
|
-
});
|
|
6127
|
-
child.unref();
|
|
6128
|
-
} catch (err) {
|
|
6129
|
-
console.warn(
|
|
6130
|
-
"Failed to open browser automatically. Please open this URL manually:",
|
|
6131
|
-
url,
|
|
6132
|
-
);
|
|
6133
|
-
}
|
|
6420
|
+
openBrowser(url, 0);
|
|
6134
6421
|
}
|
|
6135
6422
|
resolve(ctx);
|
|
6136
6423
|
});
|
|
@@ -6181,10 +6468,32 @@ if (require.main === module) {
|
|
|
6181
6468
|
}
|
|
6182
6469
|
} else if (resolvedPaths.length > 0) {
|
|
6183
6470
|
// File mode: files specified
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
for (
|
|
6187
|
-
await
|
|
6471
|
+
// Check for existing servers first
|
|
6472
|
+
let filesToStart = [];
|
|
6473
|
+
for (const fp of resolvedPaths) {
|
|
6474
|
+
const existing = await checkExistingServer(fp);
|
|
6475
|
+
if (existing) {
|
|
6476
|
+
console.log(`Server already running for ${path.basename(fp)} on port ${existing.port}`);
|
|
6477
|
+
const url = `http://localhost:${existing.port}`;
|
|
6478
|
+
if (!noOpen) {
|
|
6479
|
+
openBrowser(url, 0);
|
|
6480
|
+
}
|
|
6481
|
+
} else {
|
|
6482
|
+
filesToStart.push(fp);
|
|
6483
|
+
}
|
|
6484
|
+
}
|
|
6485
|
+
|
|
6486
|
+
if (filesToStart.length === 0) {
|
|
6487
|
+
console.log("All files already have running servers. Activating existing tabs.");
|
|
6488
|
+
// Wait a moment for browser activation to complete before exiting
|
|
6489
|
+
setTimeout(() => process.exit(0), 500);
|
|
6490
|
+
return;
|
|
6491
|
+
}
|
|
6492
|
+
|
|
6493
|
+
console.log(`Starting servers for ${filesToStart.length} file(s)...`);
|
|
6494
|
+
serversRunning = filesToStart.length;
|
|
6495
|
+
for (let i = 0; i < filesToStart.length; i++) {
|
|
6496
|
+
await createFileServer(filesToStart[i], i);
|
|
6188
6497
|
}
|
|
6189
6498
|
console.log("Close all browser tabs or Submit & Exit to finish.");
|
|
6190
6499
|
} else {
|