reviw 0.9.0 → 0.9.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.
Files changed (3) hide show
  1. package/README.md +3 -3
  2. package/cli.cjs +296 -241
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -46,10 +46,10 @@ npx reviw <file>
46
46
 
47
47
  ```bash
48
48
  # Single file
49
- reviw <file> [--port 3000] [--encoding utf8|shift_jis|...]
49
+ reviw <file> [--port 4989] [--encoding utf8|shift_jis|...]
50
50
 
51
51
  # Multiple files (each opens on consecutive ports)
52
- reviw file1.csv file2.md file3.tsv --port 3000
52
+ reviw file1.csv file2.md file3.tsv --port 4989
53
53
 
54
54
  # Diff from stdin
55
55
  git diff HEAD | reviw
@@ -59,7 +59,7 @@ reviw changes.diff
59
59
  ```
60
60
 
61
61
  ### Options
62
- - `--port <number>`: Specify starting port (default: 3000)
62
+ - `--port <number>`: Specify starting port (default: 4989)
63
63
  - `--encoding <encoding>`: Force specific encoding (auto-detected by default)
64
64
  - `--no-open`: Prevent automatic browser opening
65
65
 
package/cli.cjs CHANGED
@@ -3,7 +3,7 @@
3
3
  * Lightweight CSV/Text/Markdown viewer with comment collection server
4
4
  *
5
5
  * Usage:
6
- * reviw <file...> [--port 3000] [--encoding utf8|shift_jis|...] [--no-open]
6
+ * reviw <file...> [--port 4989] [--encoding utf8|shift_jis|...] [--no-open]
7
7
  *
8
8
  * Multiple files can be specified. Each file opens on a separate port.
9
9
  * Click cells in the browser to add comments.
@@ -11,20 +11,20 @@
11
11
  * When all files are closed, outputs combined YAML to stdout and exits.
12
12
  */
13
13
 
14
- const fs = require('fs');
15
- const http = require('http');
16
- const path = require('path');
17
- const { spawn } = require('child_process');
18
- const chardet = require('chardet');
19
- const iconv = require('iconv-lite');
20
- const marked = require('marked');
21
- const yaml = require('js-yaml');
14
+ const fs = require("fs");
15
+ const http = require("http");
16
+ const path = require("path");
17
+ const { spawn } = require("child_process");
18
+ const chardet = require("chardet");
19
+ const iconv = require("iconv-lite");
20
+ const marked = require("marked");
21
+ const yaml = require("js-yaml");
22
22
 
23
23
  // --- CLI arguments ---------------------------------------------------------
24
24
  const args = process.argv.slice(2);
25
25
 
26
26
  const filePaths = [];
27
- let basePort = 3000;
27
+ let basePort = 4989;
28
28
  let encodingOpt = null;
29
29
  let noOpen = false;
30
30
  let stdinMode = false;
@@ -33,21 +33,21 @@ let stdinContent = null;
33
33
 
34
34
  for (let i = 0; i < args.length; i += 1) {
35
35
  const arg = args[i];
36
- if (arg === '--port' && args[i + 1]) {
36
+ if (arg === "--port" && args[i + 1]) {
37
37
  basePort = Number(args[i + 1]);
38
38
  i += 1;
39
- } else if ((arg === '--encoding' || arg === '-e') && args[i + 1]) {
39
+ } else if ((arg === "--encoding" || arg === "-e") && args[i + 1]) {
40
40
  encodingOpt = args[i + 1];
41
41
  i += 1;
42
- } else if (arg === '--no-open') {
42
+ } else if (arg === "--no-open") {
43
43
  noOpen = true;
44
- } else if (arg === '--help' || arg === '-h') {
44
+ } else if (arg === "--help" || arg === "-h") {
45
45
  console.log(`Usage: reviw <file...> [options]
46
46
  git diff | reviw [options]
47
47
  reviw [options] (auto runs git diff HEAD)
48
48
 
49
49
  Options:
50
- --port <number> Server port (default: 3000)
50
+ --port <number> Server port (default: 4989)
51
51
  --encoding <enc> Force encoding (utf8, shift_jis, etc.)
52
52
  --no-open Don't open browser automatically
53
53
  --help, -h Show this help message
@@ -59,7 +59,7 @@ Examples:
59
59
  git diff HEAD~3 | reviw # Review diff from last 3 commits
60
60
  reviw # Auto run git diff HEAD`);
61
61
  process.exit(0);
62
- } else if (!arg.startsWith('-')) {
62
+ } else if (!arg.startsWith("-")) {
63
63
  filePaths.push(arg);
64
64
  }
65
65
  }
@@ -71,7 +71,7 @@ async function checkStdin() {
71
71
  resolve(false);
72
72
  return;
73
73
  }
74
- let data = '';
74
+ let data = "";
75
75
  let resolved = false;
76
76
  const timeout = setTimeout(() => {
77
77
  if (!resolved) {
@@ -79,18 +79,18 @@ async function checkStdin() {
79
79
  resolve(data.length > 0 ? data : false);
80
80
  }
81
81
  }, 100);
82
- process.stdin.setEncoding('utf8');
83
- process.stdin.on('data', (chunk) => {
82
+ process.stdin.setEncoding("utf8");
83
+ process.stdin.on("data", (chunk) => {
84
84
  data += chunk;
85
85
  });
86
- process.stdin.on('end', () => {
86
+ process.stdin.on("end", () => {
87
87
  clearTimeout(timeout);
88
88
  if (!resolved) {
89
89
  resolved = true;
90
90
  resolve(data.length > 0 ? data : false);
91
91
  }
92
92
  });
93
- process.stdin.on('error', () => {
93
+ process.stdin.on("error", () => {
94
94
  clearTimeout(timeout);
95
95
  if (!resolved) {
96
96
  resolved = true;
@@ -103,15 +103,15 @@ async function checkStdin() {
103
103
  // Run git diff HEAD if no files and no stdin
104
104
  function runGitDiff() {
105
105
  return new Promise((resolve, reject) => {
106
- const { execSync } = require('child_process');
106
+ const { execSync } = require("child_process");
107
107
  try {
108
108
  // Check if we're in a git repo
109
- execSync('git rev-parse --is-inside-work-tree', { stdio: 'pipe' });
109
+ execSync("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
110
110
  // Run git diff HEAD
111
- const diff = execSync('git diff HEAD', { encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 });
111
+ const diff = execSync("git diff HEAD", { encoding: "utf8", maxBuffer: 50 * 1024 * 1024 });
112
112
  resolve(diff);
113
113
  } catch (err) {
114
- reject(new Error('Not a git repository or git command failed'));
114
+ reject(new Error("Not a git repository or git command failed"));
115
115
  }
116
116
  });
117
117
  }
@@ -130,7 +130,7 @@ for (const fp of filePaths) {
130
130
  // --- Diff parsing -----------------------------------------------------------
131
131
  function parseDiff(diffText) {
132
132
  const files = [];
133
- const lines = diffText.split('\n');
133
+ const lines = diffText.split("\n");
134
134
  let currentFile = null;
135
135
  let lineNumber = 0;
136
136
 
@@ -138,16 +138,16 @@ function parseDiff(diffText) {
138
138
  const line = lines[i];
139
139
 
140
140
  // New file header
141
- if (line.startsWith('diff --git')) {
141
+ if (line.startsWith("diff --git")) {
142
142
  if (currentFile) files.push(currentFile);
143
143
  const match = line.match(/diff --git a\/(.+?) b\/(.+)/);
144
144
  currentFile = {
145
- oldPath: match ? match[1] : '',
146
- newPath: match ? match[2] : '',
145
+ oldPath: match ? match[1] : "",
146
+ newPath: match ? match[2] : "",
147
147
  hunks: [],
148
148
  isNew: false,
149
149
  isDeleted: false,
150
- isBinary: false
150
+ isBinary: false,
151
151
  };
152
152
  lineNumber = 0;
153
153
  continue;
@@ -156,47 +156,47 @@ function parseDiff(diffText) {
156
156
  if (!currentFile) continue;
157
157
 
158
158
  // File mode info
159
- if (line.startsWith('new file mode')) {
159
+ if (line.startsWith("new file mode")) {
160
160
  currentFile.isNew = true;
161
161
  continue;
162
162
  }
163
- if (line.startsWith('deleted file mode')) {
163
+ if (line.startsWith("deleted file mode")) {
164
164
  currentFile.isDeleted = true;
165
165
  continue;
166
166
  }
167
- if (line.startsWith('Binary files')) {
167
+ if (line.startsWith("Binary files")) {
168
168
  currentFile.isBinary = true;
169
169
  continue;
170
170
  }
171
171
 
172
172
  // Hunk header
173
- if (line.startsWith('@@')) {
173
+ if (line.startsWith("@@")) {
174
174
  const match = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@(.*)/);
175
175
  if (match) {
176
176
  currentFile.hunks.push({
177
177
  oldStart: parseInt(match[1], 10),
178
178
  newStart: parseInt(match[2], 10),
179
- context: match[3] || '',
180
- lines: []
179
+ context: match[3] || "",
180
+ lines: [],
181
181
  });
182
182
  }
183
183
  continue;
184
184
  }
185
185
 
186
186
  // Skip other headers
187
- if (line.startsWith('---') || line.startsWith('+++') || line.startsWith('index ')) {
187
+ if (line.startsWith("---") || line.startsWith("+++") || line.startsWith("index ")) {
188
188
  continue;
189
189
  }
190
190
 
191
191
  // Diff content
192
192
  if (currentFile.hunks.length > 0) {
193
193
  const hunk = currentFile.hunks[currentFile.hunks.length - 1];
194
- if (line.startsWith('+')) {
195
- hunk.lines.push({ type: 'add', content: line.slice(1), lineNum: ++lineNumber });
196
- } else if (line.startsWith('-')) {
197
- hunk.lines.push({ type: 'del', content: line.slice(1), lineNum: ++lineNumber });
198
- } else if (line.startsWith(' ') || line === '') {
199
- hunk.lines.push({ type: 'ctx', content: line.slice(1) || '', lineNum: ++lineNumber });
194
+ if (line.startsWith("+")) {
195
+ hunk.lines.push({ type: "add", content: line.slice(1), lineNum: ++lineNumber });
196
+ } else if (line.startsWith("-")) {
197
+ hunk.lines.push({ type: "del", content: line.slice(1), lineNum: ++lineNumber });
198
+ } else if (line.startsWith(" ") || line === "") {
199
+ hunk.lines.push({ type: "ctx", content: line.slice(1) || "", lineNum: ++lineNumber });
200
200
  }
201
201
  }
202
202
  }
@@ -235,18 +235,18 @@ function loadDiff(diffText) {
235
235
  sortedFiles.forEach((file, fileIdx) => {
236
236
  // File header row
237
237
  let label = file.newPath || file.oldPath;
238
- if (file.isNew) label += ' (new)';
239
- if (file.isDeleted) label += ' (deleted)';
240
- if (file.isBinary) label += ' (binary)';
238
+ if (file.isNew) label += " (new)";
239
+ if (file.isDeleted) label += " (deleted)";
240
+ if (file.isBinary) label += " (binary)";
241
241
  rows.push({
242
- type: 'file',
242
+ type: "file",
243
243
  content: label,
244
244
  filePath: file.newPath || file.oldPath,
245
245
  fileIndex: fileIdx,
246
246
  lineCount: file.lineCount,
247
247
  collapsed: file.collapsed,
248
248
  isBinary: file.isBinary,
249
- rowIndex: rowIndex++
249
+ rowIndex: rowIndex++,
250
250
  });
251
251
 
252
252
  if (file.isBinary) return;
@@ -254,10 +254,10 @@ function loadDiff(diffText) {
254
254
  file.hunks.forEach((hunk) => {
255
255
  // Hunk header
256
256
  rows.push({
257
- type: 'hunk',
257
+ type: "hunk",
258
258
  content: `@@ -${hunk.oldStart} +${hunk.newStart} @@${hunk.context}`,
259
259
  fileIndex: fileIdx,
260
- rowIndex: rowIndex++
260
+ rowIndex: rowIndex++,
261
261
  });
262
262
 
263
263
  hunk.lines.forEach((line) => {
@@ -265,7 +265,7 @@ function loadDiff(diffText) {
265
265
  type: line.type,
266
266
  content: line.content,
267
267
  fileIndex: fileIdx,
268
- rowIndex: rowIndex++
268
+ rowIndex: rowIndex++,
269
269
  });
270
270
  });
271
271
  });
@@ -274,8 +274,8 @@ function loadDiff(diffText) {
274
274
  return {
275
275
  rows,
276
276
  files: sortedFiles,
277
- title: 'Git Diff',
278
- mode: 'diff'
277
+ title: "Git Diff",
278
+ mode: "diff",
279
279
  };
280
280
  }
281
281
 
@@ -285,10 +285,10 @@ let serversRunning = 0;
285
285
  let nextPort = basePort;
286
286
 
287
287
  // --- Simple CSV/TSV parser (RFC4180-style, handles " escaping and newlines) ----
288
- function parseCsv(text, separator = ',') {
288
+ function parseCsv(text, separator = ",") {
289
289
  const rows = [];
290
290
  let row = [];
291
- let field = '';
291
+ let field = "";
292
292
  let inQuotes = false;
293
293
 
294
294
  for (let i = 0; i < text.length; i += 1) {
@@ -309,13 +309,13 @@ function parseCsv(text, separator = ',') {
309
309
  inQuotes = true;
310
310
  } else if (ch === separator) {
311
311
  row.push(field);
312
- field = '';
313
- } else if (ch === '\n') {
312
+ field = "";
313
+ } else if (ch === "\n") {
314
314
  row.push(field);
315
315
  rows.push(row);
316
316
  row = [];
317
- field = '';
318
- } else if (ch === '\r') {
317
+ field = "";
318
+ } else if (ch === "\r") {
319
319
  // Ignore CR (for CRLF handling)
320
320
  } else {
321
321
  field += ch;
@@ -327,7 +327,7 @@ function parseCsv(text, separator = ',') {
327
327
 
328
328
  // Remove trailing empty row if present
329
329
  const last = rows[rows.length - 1];
330
- if (last && last.every((v) => v === '')) {
330
+ if (last && last.every((v) => v === "")) {
331
331
  rows.pop();
332
332
  }
333
333
 
@@ -335,15 +335,15 @@ function parseCsv(text, separator = ',') {
335
335
  }
336
336
 
337
337
  const ENCODING_MAP = {
338
- 'utf-8': 'utf8',
339
- utf8: 'utf8',
340
- 'shift_jis': 'shift_jis',
341
- sjis: 'shift_jis',
342
- 'windows-31j': 'cp932',
343
- cp932: 'cp932',
344
- 'euc-jp': 'euc-jp',
345
- 'iso-8859-1': 'latin1',
346
- latin1: 'latin1'
338
+ "utf-8": "utf8",
339
+ utf8: "utf8",
340
+ shift_jis: "shift_jis",
341
+ sjis: "shift_jis",
342
+ "windows-31j": "cp932",
343
+ cp932: "cp932",
344
+ "euc-jp": "euc-jp",
345
+ "iso-8859-1": "latin1",
346
+ latin1: "latin1",
347
347
  };
348
348
 
349
349
  function normalizeEncoding(name) {
@@ -356,9 +356,9 @@ function decodeBuffer(buf) {
356
356
  const specified = normalizeEncoding(encodingOpt);
357
357
  let encoding = specified;
358
358
  if (!encoding) {
359
- const detected = chardet.detect(buf) || '';
360
- encoding = normalizeEncoding(detected) || 'utf8';
361
- if (encoding !== 'utf8') {
359
+ const detected = chardet.detect(buf) || "";
360
+ encoding = normalizeEncoding(detected) || "utf8";
361
+ if (encoding !== "utf8") {
362
362
  console.log(`Detected encoding: ${detected} -> ${encoding}`);
363
363
  }
364
364
  }
@@ -366,7 +366,7 @@ function decodeBuffer(buf) {
366
366
  return iconv.decode(buf, encoding);
367
367
  } catch (err) {
368
368
  console.warn(`Decode failed (${encoding}): ${err.message}, falling back to utf8`);
369
- return buf.toString('utf8');
369
+ return buf.toString("utf8");
370
370
  }
371
371
  }
372
372
 
@@ -374,8 +374,8 @@ function loadCsv(filePath) {
374
374
  const raw = fs.readFileSync(filePath);
375
375
  const csvText = decodeBuffer(raw);
376
376
  const ext = path.extname(filePath).toLowerCase();
377
- const separator = ext === '.tsv' ? '\t' : ',';
378
- if (!csvText.includes('\n') && !csvText.includes(separator)) {
377
+ const separator = ext === ".tsv" ? "\t" : ",";
378
+ if (!csvText.includes("\n") && !csvText.includes(separator)) {
379
379
  // heuristic: if no newline/separators, still treat as single row
380
380
  }
381
381
  const rows = parseCsv(csvText, separator);
@@ -383,7 +383,7 @@ function loadCsv(filePath) {
383
383
  return {
384
384
  rows,
385
385
  cols: Math.max(1, maxCols),
386
- title: path.basename(filePath)
386
+ title: path.basename(filePath),
387
387
  };
388
388
  }
389
389
 
@@ -395,7 +395,7 @@ function loadText(filePath) {
395
395
  rows: lines.map((line) => [line]),
396
396
  cols: 1,
397
397
  title: path.basename(filePath),
398
- preview: null
398
+ preview: null,
399
399
  };
400
400
  }
401
401
 
@@ -405,13 +405,13 @@ function loadMarkdown(filePath) {
405
405
  const lines = text.split(/\r?\n/);
406
406
 
407
407
  // Parse YAML frontmatter
408
- let frontmatterHtml = '';
408
+ let frontmatterHtml = "";
409
409
  let contentStart = 0;
410
410
 
411
- if (lines[0] && lines[0].trim() === '---') {
411
+ if (lines[0] && lines[0].trim() === "---") {
412
412
  let frontmatterEnd = -1;
413
413
  for (let i = 1; i < lines.length; i++) {
414
- if (lines[i].trim() === '---') {
414
+ if (lines[i].trim() === "---") {
415
415
  frontmatterEnd = i;
416
416
  break;
417
417
  }
@@ -419,32 +419,35 @@ function loadMarkdown(filePath) {
419
419
 
420
420
  if (frontmatterEnd > 0) {
421
421
  const frontmatterLines = lines.slice(1, frontmatterEnd);
422
- const frontmatterText = frontmatterLines.join('\n');
422
+ const frontmatterText = frontmatterLines.join("\n");
423
423
 
424
424
  try {
425
425
  const frontmatter = yaml.load(frontmatterText);
426
- if (frontmatter && typeof frontmatter === 'object') {
426
+ if (frontmatter && typeof frontmatter === "object") {
427
427
  // Create HTML table for frontmatter
428
428
  frontmatterHtml = '<div class="frontmatter-table"><table>';
429
429
  frontmatterHtml += '<colgroup><col style="width:12%"><col style="width:88%"></colgroup>';
430
430
  frontmatterHtml += '<thead><tr><th colspan="2">Document Metadata</th></tr></thead>';
431
- frontmatterHtml += '<tbody>';
431
+ frontmatterHtml += "<tbody>";
432
432
 
433
433
  function renderValue(val) {
434
434
  if (Array.isArray(val)) {
435
- return val.map(v => '<span class="fm-tag">' + escapeHtmlChars(String(v)) + '</span>').join(' ');
435
+ return val
436
+ .map((v) => '<span class="fm-tag">' + escapeHtmlChars(String(v)) + "</span>")
437
+ .join(" ");
436
438
  }
437
- if (typeof val === 'object' && val !== null) {
438
- return '<pre>' + escapeHtmlChars(JSON.stringify(val, null, 2)) + '</pre>';
439
+ if (typeof val === "object" && val !== null) {
440
+ return "<pre>" + escapeHtmlChars(JSON.stringify(val, null, 2)) + "</pre>";
439
441
  }
440
442
  return escapeHtmlChars(String(val));
441
443
  }
442
444
 
443
445
  for (const [key, val] of Object.entries(frontmatter)) {
444
- frontmatterHtml += '<tr><th>' + escapeHtmlChars(key) + '</th><td>' + renderValue(val) + '</td></tr>';
446
+ frontmatterHtml +=
447
+ "<tr><th>" + escapeHtmlChars(key) + "</th><td>" + renderValue(val) + "</td></tr>";
445
448
  }
446
449
 
447
- frontmatterHtml += '</tbody></table></div>';
450
+ frontmatterHtml += "</tbody></table></div>";
448
451
  contentStart = frontmatterEnd + 1;
449
452
  }
450
453
  } catch (e) {
@@ -454,43 +457,43 @@ function loadMarkdown(filePath) {
454
457
  }
455
458
 
456
459
  // Parse markdown content (without frontmatter)
457
- const contentText = lines.slice(contentStart).join('\n');
460
+ const contentText = lines.slice(contentStart).join("\n");
458
461
  const preview = frontmatterHtml + marked.parse(contentText, { breaks: true });
459
462
 
460
463
  return {
461
464
  rows: lines.map((line) => [line]),
462
465
  cols: 1,
463
466
  title: path.basename(filePath),
464
- preview
467
+ preview,
465
468
  };
466
469
  }
467
470
 
468
471
  function escapeHtmlChars(str) {
469
472
  return str
470
- .replace(/&/g, '&amp;')
471
- .replace(/</g, '&lt;')
472
- .replace(/>/g, '&gt;')
473
- .replace(/"/g, '&quot;');
473
+ .replace(/&/g, "&amp;")
474
+ .replace(/</g, "&lt;")
475
+ .replace(/>/g, "&gt;")
476
+ .replace(/"/g, "&quot;");
474
477
  }
475
478
 
476
479
  function loadData(filePath) {
477
480
  const ext = path.extname(filePath).toLowerCase();
478
- if (ext === '.csv' || ext === '.tsv') {
481
+ if (ext === ".csv" || ext === ".tsv") {
479
482
  const data = loadCsv(filePath);
480
- return { ...data, mode: 'csv' };
483
+ return { ...data, mode: "csv" };
481
484
  }
482
- if (ext === '.md' || ext === '.markdown') {
485
+ if (ext === ".md" || ext === ".markdown") {
483
486
  const data = loadMarkdown(filePath);
484
- return { ...data, mode: 'markdown' };
487
+ return { ...data, mode: "markdown" };
485
488
  }
486
- if (ext === '.diff' || ext === '.patch') {
487
- const content = fs.readFileSync(filePath, 'utf8');
489
+ if (ext === ".diff" || ext === ".patch") {
490
+ const content = fs.readFileSync(filePath, "utf8");
488
491
  const data = loadDiff(content);
489
- return { ...data, mode: 'diff' };
492
+ return { ...data, mode: "diff" };
490
493
  }
491
494
  // default text
492
495
  const data = loadText(filePath);
493
- return { ...data, mode: 'text' };
496
+ return { ...data, mode: "text" };
494
497
  }
495
498
 
496
499
  // --- Safe JSON serialization for inline scripts ---------------------------
@@ -498,18 +501,18 @@ function loadData(filePath) {
498
501
  // the original values intact once parsed by JS.
499
502
  function serializeForScript(value) {
500
503
  return JSON.stringify(value)
501
- .replace(/</g, '\\u003c') // avoid closing the script tag
502
- .replace(/>/g, '\\u003e')
503
- .replace(/\u2028/g, '\\u2028') // line separator
504
- .replace(/\u2029/g, '\\u2029') // paragraph separator
505
- .replace(/`/g, '\\`') // keep template literal boundaries safe
506
- .replace(/\$\{/g, '\\${');
504
+ .replace(/</g, "\\u003c") // avoid closing the script tag
505
+ .replace(/>/g, "\\u003e")
506
+ .replace(/\u2028/g, "\\u2028") // line separator
507
+ .replace(/\u2029/g, "\\u2029") // paragraph separator
508
+ .replace(/`/g, "\\`") // keep template literal boundaries safe
509
+ .replace(/\$\{/g, "\\${");
507
510
  }
508
511
 
509
512
  function diffHtmlTemplate(diffData) {
510
513
  const { rows, title } = diffData;
511
514
  const serialized = serializeForScript(rows);
512
- const fileCount = rows.filter(r => r.type === 'file').length;
515
+ const fileCount = rows.filter((r) => r.type === "file").length;
513
516
 
514
517
  return `<!doctype html>
515
518
  <html lang="ja">
@@ -886,7 +889,7 @@ function diffHtmlTemplate(diffData) {
886
889
  <header>
887
890
  <div class="meta">
888
891
  <h1>${title}</h1>
889
- <span class="badge">${fileCount} file${fileCount !== 1 ? 's' : ''} changed</span>
892
+ <span class="badge">${fileCount} file${fileCount !== 1 ? "s" : ""} changed</span>
890
893
  <span class="pill">Comments <strong id="comment-count">0</strong></span>
891
894
  </div>
892
895
  <div class="actions">
@@ -2218,8 +2221,9 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2218
2221
  </header>
2219
2222
 
2220
2223
  <div class="wrap">
2221
- ${hasPreview && mode === 'markdown'
2222
- ? `<div class="md-layout">
2224
+ ${
2225
+ hasPreview && mode === "markdown"
2226
+ ? `<div class="md-layout">
2223
2227
  <div class="md-left">
2224
2228
  <div class="md-preview">${previewHtml}</div>
2225
2229
  </div>
@@ -2230,7 +2234,12 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2230
2234
  <thead>
2231
2235
  <tr>
2232
2236
  <th aria-label="row/col corner"></th>
2233
- ${Array.from({ length: cols }).map((_, i) => `<th data-col="${i + 1}"><div class="th-inner">${mode === 'csv' ? `C${i + 1}` : 'Text'}<span class="resizer" data-col="${i + 1}"></span></div></th>`).join('')}
2237
+ ${Array.from({ length: cols })
2238
+ .map(
2239
+ (_, i) =>
2240
+ `<th data-col="${i + 1}"><div class="th-inner">${mode === "csv" ? `C${i + 1}` : "Text"}<span class="resizer" data-col="${i + 1}"></span></div></th>`,
2241
+ )
2242
+ .join("")}
2234
2243
  </tr>
2235
2244
  </thead>
2236
2245
  <tbody id="tbody"></tbody>
@@ -2238,8 +2247,8 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2238
2247
  </div>
2239
2248
  </div>
2240
2249
  </div>`
2241
- : `
2242
- ${hasPreview ? `<div class="md-preview">${previewHtml}</div>` : ''}
2250
+ : `
2251
+ ${hasPreview ? `<div class="md-preview">${previewHtml}</div>` : ""}
2243
2252
  <div class="toolbar">
2244
2253
  <button id="fit-width">Fit to width</button>
2245
2254
  <span>Drag header edge to resize columns</span>
@@ -2250,13 +2259,19 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2250
2259
  <thead>
2251
2260
  <tr>
2252
2261
  <th aria-label="row/col corner"></th>
2253
- ${Array.from({ length: cols }).map((_, i) => `<th data-col="${i + 1}"><div class="th-inner">${mode === 'csv' ? `C${i + 1}` : 'Text'}<span class="resizer" data-col="${i + 1}"></span></div></th>`).join('')}
2262
+ ${Array.from({ length: cols })
2263
+ .map(
2264
+ (_, i) =>
2265
+ `<th data-col="${i + 1}"><div class="th-inner">${mode === "csv" ? `C${i + 1}` : "Text"}<span class="resizer" data-col="${i + 1}"></span></div></th>`,
2266
+ )
2267
+ .join("")}
2254
2268
  </tr>
2255
2269
  </thead>
2256
2270
  <tbody id="tbody"></tbody>
2257
2271
  </table>
2258
2272
  </div>
2259
- `}
2273
+ `
2274
+ }
2260
2275
  </div>
2261
2276
 
2262
2277
  <div class="floating" id="comment-card">
@@ -3642,7 +3657,7 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
3642
3657
 
3643
3658
  function buildHtml(filePath) {
3644
3659
  const data = loadData(filePath);
3645
- if (data.mode === 'diff') {
3660
+ if (data.mode === "diff") {
3646
3661
  return diffHtmlTemplate(data);
3647
3662
  }
3648
3663
  const { rows, cols, title, mode, preview } = data;
@@ -3652,16 +3667,16 @@ function buildHtml(filePath) {
3652
3667
  // --- HTTP Server -----------------------------------------------------------
3653
3668
  function readBody(req) {
3654
3669
  return new Promise((resolve, reject) => {
3655
- let data = '';
3656
- req.on('data', (chunk) => {
3670
+ let data = "";
3671
+ req.on("data", (chunk) => {
3657
3672
  data += chunk;
3658
3673
  if (data.length > 2 * 1024 * 1024) {
3659
- reject(new Error('payload too large'));
3674
+ reject(new Error("payload too large"));
3660
3675
  req.destroy();
3661
3676
  }
3662
3677
  });
3663
- req.on('end', () => resolve(data));
3664
- req.on('error', reject);
3678
+ req.on("end", () => resolve(data));
3679
+ req.on("error", reject);
3665
3680
  });
3666
3681
  }
3667
3682
 
@@ -3669,7 +3684,7 @@ const MAX_PORT_ATTEMPTS = 100;
3669
3684
  const activeServers = new Map();
3670
3685
 
3671
3686
  function outputAllResults() {
3672
- console.log('=== All comments received ===');
3687
+ console.log("=== All comments received ===");
3673
3688
  if (allResults.length === 1) {
3674
3689
  const yamlOut = yaml.dump(allResults[0], { noRefs: true, lineWidth: 120 });
3675
3690
  console.log(yamlOut.trim());
@@ -3691,15 +3706,19 @@ function shutdownAll() {
3691
3706
  for (const ctx of activeServers.values()) {
3692
3707
  if (ctx.watcher) ctx.watcher.close();
3693
3708
  if (ctx.heartbeat) clearInterval(ctx.heartbeat);
3694
- ctx.sseClients.forEach((res) => { try { res.end(); } catch (_) {} });
3709
+ ctx.sseClients.forEach((res) => {
3710
+ try {
3711
+ res.end();
3712
+ } catch (_) {}
3713
+ });
3695
3714
  if (ctx.server) ctx.server.close();
3696
3715
  }
3697
3716
  outputAllResults();
3698
3717
  setTimeout(() => process.exit(0), 500).unref();
3699
3718
  }
3700
3719
 
3701
- process.on('SIGINT', shutdownAll);
3702
- process.on('SIGTERM', shutdownAll);
3720
+ process.on("SIGINT", shutdownAll);
3721
+ process.on("SIGTERM", shutdownAll);
3703
3722
 
3704
3723
  function createFileServer(filePath) {
3705
3724
  return new Promise((resolve) => {
@@ -3716,19 +3735,21 @@ function createFileServer(filePath) {
3716
3735
  reloadTimer: null,
3717
3736
  server: null,
3718
3737
  opened: false,
3719
- port: 0
3738
+ port: 0,
3720
3739
  };
3721
3740
 
3722
3741
  function broadcast(data) {
3723
- const payload = typeof data === 'string' ? data : JSON.stringify(data);
3742
+ const payload = typeof data === "string" ? data : JSON.stringify(data);
3724
3743
  ctx.sseClients.forEach((res) => {
3725
- try { res.write(`data: ${payload}\n\n`); } catch (_) {}
3744
+ try {
3745
+ res.write(`data: ${payload}\n\n`);
3746
+ } catch (_) {}
3726
3747
  });
3727
3748
  }
3728
3749
 
3729
3750
  function notifyReload() {
3730
3751
  clearTimeout(ctx.reloadTimer);
3731
- ctx.reloadTimer = setTimeout(() => broadcast('reload'), 150);
3752
+ ctx.reloadTimer = setTimeout(() => broadcast("reload"), 150);
3732
3753
  }
3733
3754
 
3734
3755
  function startWatcher() {
@@ -3737,7 +3758,7 @@ function createFileServer(filePath) {
3737
3758
  } catch (err) {
3738
3759
  console.warn(`Failed to start file watcher for ${baseName}:`, err);
3739
3760
  }
3740
- ctx.heartbeat = setInterval(() => broadcast('ping'), 25000);
3761
+ ctx.heartbeat = setInterval(() => broadcast("ping"), 25000);
3741
3762
  }
3742
3763
 
3743
3764
  function shutdownServer(result) {
@@ -3749,7 +3770,11 @@ function createFileServer(filePath) {
3749
3770
  clearInterval(ctx.heartbeat);
3750
3771
  ctx.heartbeat = null;
3751
3772
  }
3752
- ctx.sseClients.forEach((res) => { try { res.end(); } catch (_) {} });
3773
+ ctx.sseClients.forEach((res) => {
3774
+ try {
3775
+ res.end();
3776
+ } catch (_) {}
3777
+ });
3753
3778
  if (ctx.server) {
3754
3779
  ctx.server.close();
3755
3780
  ctx.server = null;
@@ -3762,95 +3787,95 @@ function createFileServer(filePath) {
3762
3787
  }
3763
3788
 
3764
3789
  ctx.server = http.createServer(async (req, res) => {
3765
- if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
3790
+ if (req.method === "GET" && (req.url === "/" || req.url === "/index.html")) {
3766
3791
  try {
3767
3792
  const html = buildHtml(filePath);
3768
3793
  res.writeHead(200, {
3769
- 'Content-Type': 'text/html; charset=utf-8',
3770
- 'Cache-Control': 'no-store, no-cache, must-revalidate',
3771
- Pragma: 'no-cache',
3772
- Expires: '0'
3794
+ "Content-Type": "text/html; charset=utf-8",
3795
+ "Cache-Control": "no-store, no-cache, must-revalidate",
3796
+ Pragma: "no-cache",
3797
+ Expires: "0",
3773
3798
  });
3774
3799
  res.end(html);
3775
3800
  } catch (err) {
3776
- console.error('File load error', err);
3777
- res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
3778
- res.end('Failed to load file. Please check the file.');
3801
+ console.error("File load error", err);
3802
+ res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
3803
+ res.end("Failed to load file. Please check the file.");
3779
3804
  }
3780
3805
  return;
3781
3806
  }
3782
3807
 
3783
- if (req.method === 'GET' && req.url === '/healthz') {
3784
- res.writeHead(200, { 'Content-Type': 'text/plain' });
3785
- res.end('ok');
3808
+ if (req.method === "GET" && req.url === "/healthz") {
3809
+ res.writeHead(200, { "Content-Type": "text/plain" });
3810
+ res.end("ok");
3786
3811
  return;
3787
3812
  }
3788
3813
 
3789
- if (req.method === 'POST' && req.url === '/exit') {
3814
+ if (req.method === "POST" && req.url === "/exit") {
3790
3815
  try {
3791
3816
  const raw = await readBody(req);
3792
3817
  let payload = {};
3793
3818
  if (raw && raw.trim()) {
3794
3819
  payload = JSON.parse(raw);
3795
3820
  }
3796
- res.writeHead(200, { 'Content-Type': 'text/plain' });
3797
- res.end('bye');
3821
+ res.writeHead(200, { "Content-Type": "text/plain" });
3822
+ res.end("bye");
3798
3823
  shutdownServer(payload);
3799
3824
  } catch (err) {
3800
- console.error('payload parse error', err);
3801
- res.writeHead(400, { 'Content-Type': 'text/plain' });
3802
- res.end('bad request');
3825
+ console.error("payload parse error", err);
3826
+ res.writeHead(400, { "Content-Type": "text/plain" });
3827
+ res.end("bad request");
3803
3828
  shutdownServer(null);
3804
3829
  }
3805
3830
  return;
3806
3831
  }
3807
3832
 
3808
- if (req.method === 'GET' && req.url === '/sse') {
3833
+ if (req.method === "GET" && req.url === "/sse") {
3809
3834
  res.writeHead(200, {
3810
- 'Content-Type': 'text/event-stream',
3811
- 'Cache-Control': 'no-cache',
3812
- Connection: 'keep-alive',
3813
- 'X-Accel-Buffering': 'no'
3835
+ "Content-Type": "text/event-stream",
3836
+ "Cache-Control": "no-cache",
3837
+ Connection: "keep-alive",
3838
+ "X-Accel-Buffering": "no",
3814
3839
  });
3815
- res.write('retry: 3000\n\n');
3840
+ res.write("retry: 3000\n\n");
3816
3841
  ctx.sseClients.add(res);
3817
- req.on('close', () => ctx.sseClients.delete(res));
3842
+ req.on("close", () => ctx.sseClients.delete(res));
3818
3843
  return;
3819
3844
  }
3820
3845
 
3821
3846
  // Static file serving for images and other assets
3822
- if (req.method === 'GET') {
3847
+ if (req.method === "GET") {
3823
3848
  const MIME_TYPES = {
3824
- '.png': 'image/png',
3825
- '.jpg': 'image/jpeg',
3826
- '.jpeg': 'image/jpeg',
3827
- '.gif': 'image/gif',
3828
- '.webp': 'image/webp',
3829
- '.svg': 'image/svg+xml',
3830
- '.ico': 'image/x-icon',
3831
- '.css': 'text/css',
3832
- '.js': 'application/javascript',
3833
- '.json': 'application/json',
3834
- '.pdf': 'application/pdf',
3849
+ ".png": "image/png",
3850
+ ".jpg": "image/jpeg",
3851
+ ".jpeg": "image/jpeg",
3852
+ ".gif": "image/gif",
3853
+ ".webp": "image/webp",
3854
+ ".svg": "image/svg+xml",
3855
+ ".ico": "image/x-icon",
3856
+ ".css": "text/css",
3857
+ ".js": "application/javascript",
3858
+ ".json": "application/json",
3859
+ ".pdf": "application/pdf",
3835
3860
  };
3836
3861
  try {
3837
- const urlPath = decodeURIComponent(req.url.split('?')[0]);
3838
- if (urlPath.includes('..')) {
3839
- res.writeHead(403, { 'Content-Type': 'text/plain' });
3840
- res.end('forbidden');
3862
+ const urlPath = decodeURIComponent(req.url.split("?")[0]);
3863
+ if (urlPath.includes("..")) {
3864
+ res.writeHead(403, { "Content-Type": "text/plain" });
3865
+ res.end("forbidden");
3841
3866
  return;
3842
3867
  }
3843
3868
  const staticPath = path.join(baseDir, urlPath);
3844
3869
  if (!staticPath.startsWith(baseDir)) {
3845
- res.writeHead(403, { 'Content-Type': 'text/plain' });
3846
- res.end('forbidden');
3870
+ res.writeHead(403, { "Content-Type": "text/plain" });
3871
+ res.end("forbidden");
3847
3872
  return;
3848
3873
  }
3849
3874
  if (fs.existsSync(staticPath) && fs.statSync(staticPath).isFile()) {
3850
3875
  const ext = path.extname(staticPath).toLowerCase();
3851
- const contentType = MIME_TYPES[ext] || 'application/octet-stream';
3876
+ const contentType = MIME_TYPES[ext] || "application/octet-stream";
3852
3877
  const content = fs.readFileSync(staticPath);
3853
- res.writeHead(200, { 'Content-Type': contentType });
3878
+ res.writeHead(200, { "Content-Type": contentType });
3854
3879
  res.end(content);
3855
3880
  return;
3856
3881
  }
@@ -3859,20 +3884,22 @@ function createFileServer(filePath) {
3859
3884
  }
3860
3885
  }
3861
3886
 
3862
- res.writeHead(404, { 'Content-Type': 'text/plain' });
3863
- res.end('not found');
3887
+ res.writeHead(404, { "Content-Type": "text/plain" });
3888
+ res.end("not found");
3864
3889
  });
3865
3890
 
3866
3891
  function tryListen(attemptPort, attempts = 0) {
3867
3892
  if (attempts >= MAX_PORT_ATTEMPTS) {
3868
- console.error(`Could not find an available port for ${baseName} after ${MAX_PORT_ATTEMPTS} attempts.`);
3893
+ console.error(
3894
+ `Could not find an available port for ${baseName} after ${MAX_PORT_ATTEMPTS} attempts.`,
3895
+ );
3869
3896
  serversRunning--;
3870
3897
  checkAllDone();
3871
3898
  return;
3872
3899
  }
3873
3900
 
3874
- ctx.server.once('error', (err) => {
3875
- if (err.code === 'EADDRINUSE') {
3901
+ ctx.server.once("error", (err) => {
3902
+ if (err.code === "EADDRINUSE") {
3876
3903
  tryListen(attemptPort + 1, attempts + 1);
3877
3904
  } else {
3878
3905
  console.error(`Server error for ${baseName}:`, err);
@@ -3888,11 +3915,19 @@ function createFileServer(filePath) {
3888
3915
  console.log(`Viewer started: http://localhost:${attemptPort} (file: ${baseName})`);
3889
3916
  if (!noOpen) {
3890
3917
  const url = `http://localhost:${attemptPort}`;
3891
- const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
3918
+ const opener =
3919
+ process.platform === "darwin"
3920
+ ? "open"
3921
+ : process.platform === "win32"
3922
+ ? "start"
3923
+ : "xdg-open";
3892
3924
  try {
3893
- spawn(opener, [url], { stdio: 'ignore', detached: true });
3925
+ spawn(opener, [url], { stdio: "ignore", detached: true });
3894
3926
  } catch (err) {
3895
- console.warn('Failed to open browser automatically. Please open this URL manually:', url);
3927
+ console.warn(
3928
+ "Failed to open browser automatically. Please open this URL manually:",
3929
+ url,
3930
+ );
3896
3931
  }
3897
3932
  }
3898
3933
  startWatcher();
@@ -3914,13 +3949,15 @@ function createDiffServer(diffContent) {
3914
3949
  sseClients: new Set(),
3915
3950
  heartbeat: null,
3916
3951
  server: null,
3917
- port: 0
3952
+ port: 0,
3918
3953
  };
3919
3954
 
3920
3955
  function broadcast(data) {
3921
- const payload = typeof data === 'string' ? data : JSON.stringify(data);
3956
+ const payload = typeof data === "string" ? data : JSON.stringify(data);
3922
3957
  ctx.sseClients.forEach((res) => {
3923
- try { res.write(`data: ${payload}\n\n`); } catch (_) {}
3958
+ try {
3959
+ res.write(`data: ${payload}\n\n`);
3960
+ } catch (_) {}
3924
3961
  });
3925
3962
  }
3926
3963
 
@@ -3929,7 +3966,11 @@ function createDiffServer(diffContent) {
3929
3966
  clearInterval(ctx.heartbeat);
3930
3967
  ctx.heartbeat = null;
3931
3968
  }
3932
- ctx.sseClients.forEach((res) => { try { res.end(); } catch (_) {} });
3969
+ ctx.sseClients.forEach((res) => {
3970
+ try {
3971
+ res.end();
3972
+ } catch (_) {}
3973
+ });
3933
3974
  if (ctx.server) {
3934
3975
  ctx.server.close();
3935
3976
  ctx.server = null;
@@ -3941,79 +3982,81 @@ function createDiffServer(diffContent) {
3941
3982
  }
3942
3983
 
3943
3984
  ctx.server = http.createServer(async (req, res) => {
3944
- if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
3985
+ if (req.method === "GET" && (req.url === "/" || req.url === "/index.html")) {
3945
3986
  try {
3946
3987
  const html = diffHtmlTemplate(diffData);
3947
3988
  res.writeHead(200, {
3948
- 'Content-Type': 'text/html; charset=utf-8',
3949
- 'Cache-Control': 'no-store, no-cache, must-revalidate',
3950
- Pragma: 'no-cache',
3951
- Expires: '0'
3989
+ "Content-Type": "text/html; charset=utf-8",
3990
+ "Cache-Control": "no-store, no-cache, must-revalidate",
3991
+ Pragma: "no-cache",
3992
+ Expires: "0",
3952
3993
  });
3953
3994
  res.end(html);
3954
3995
  } catch (err) {
3955
- console.error('Diff render error', err);
3956
- res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
3957
- res.end('Failed to render diff view.');
3996
+ console.error("Diff render error", err);
3997
+ res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
3998
+ res.end("Failed to render diff view.");
3958
3999
  }
3959
4000
  return;
3960
4001
  }
3961
4002
 
3962
- if (req.method === 'GET' && req.url === '/healthz') {
3963
- res.writeHead(200, { 'Content-Type': 'text/plain' });
3964
- res.end('ok');
4003
+ if (req.method === "GET" && req.url === "/healthz") {
4004
+ res.writeHead(200, { "Content-Type": "text/plain" });
4005
+ res.end("ok");
3965
4006
  return;
3966
4007
  }
3967
4008
 
3968
- if (req.method === 'POST' && req.url === '/exit') {
4009
+ if (req.method === "POST" && req.url === "/exit") {
3969
4010
  try {
3970
4011
  const raw = await readBody(req);
3971
4012
  let payload = {};
3972
4013
  if (raw && raw.trim()) {
3973
4014
  payload = JSON.parse(raw);
3974
4015
  }
3975
- res.writeHead(200, { 'Content-Type': 'text/plain' });
3976
- res.end('bye');
4016
+ res.writeHead(200, { "Content-Type": "text/plain" });
4017
+ res.end("bye");
3977
4018
  shutdownServer(payload);
3978
4019
  } catch (err) {
3979
- console.error('payload parse error', err);
3980
- res.writeHead(400, { 'Content-Type': 'text/plain' });
3981
- res.end('bad request');
4020
+ console.error("payload parse error", err);
4021
+ res.writeHead(400, { "Content-Type": "text/plain" });
4022
+ res.end("bad request");
3982
4023
  shutdownServer(null);
3983
4024
  }
3984
4025
  return;
3985
4026
  }
3986
4027
 
3987
- if (req.method === 'GET' && req.url === '/sse') {
4028
+ if (req.method === "GET" && req.url === "/sse") {
3988
4029
  res.writeHead(200, {
3989
- 'Content-Type': 'text/event-stream',
3990
- 'Cache-Control': 'no-cache',
3991
- Connection: 'keep-alive',
3992
- 'X-Accel-Buffering': 'no'
4030
+ "Content-Type": "text/event-stream",
4031
+ "Cache-Control": "no-cache",
4032
+ Connection: "keep-alive",
4033
+ "X-Accel-Buffering": "no",
3993
4034
  });
3994
- res.write('retry: 3000\n\n');
4035
+ res.write("retry: 3000\n\n");
3995
4036
  ctx.sseClients.add(res);
3996
- req.on('close', () => ctx.sseClients.delete(res));
4037
+ req.on("close", () => ctx.sseClients.delete(res));
3997
4038
  return;
3998
4039
  }
3999
4040
 
4000
- res.writeHead(404, { 'Content-Type': 'text/plain' });
4001
- res.end('not found');
4041
+ res.writeHead(404, { "Content-Type": "text/plain" });
4042
+ res.end("not found");
4002
4043
  });
4003
4044
 
4004
4045
  function tryListen(attemptPort, attempts = 0) {
4005
4046
  if (attempts >= MAX_PORT_ATTEMPTS) {
4006
- console.error(`Could not find an available port for diff viewer after ${MAX_PORT_ATTEMPTS} attempts.`);
4047
+ console.error(
4048
+ `Could not find an available port for diff viewer after ${MAX_PORT_ATTEMPTS} attempts.`,
4049
+ );
4007
4050
  serversRunning--;
4008
4051
  checkAllDone();
4009
4052
  return;
4010
4053
  }
4011
4054
 
4012
- ctx.server.once('error', (err) => {
4013
- if (err.code === 'EADDRINUSE') {
4055
+ ctx.server.once("error", (err) => {
4056
+ if (err.code === "EADDRINUSE") {
4014
4057
  tryListen(attemptPort + 1, attempts + 1);
4015
4058
  } else {
4016
- console.error('Diff server error:', err);
4059
+ console.error("Diff server error:", err);
4017
4060
  serversRunning--;
4018
4061
  checkAllDone();
4019
4062
  }
@@ -4021,15 +4064,23 @@ function createDiffServer(diffContent) {
4021
4064
 
4022
4065
  ctx.server.listen(attemptPort, () => {
4023
4066
  ctx.port = attemptPort;
4024
- ctx.heartbeat = setInterval(() => broadcast('ping'), 25000);
4067
+ ctx.heartbeat = setInterval(() => broadcast("ping"), 25000);
4025
4068
  console.log(`Diff viewer started: http://localhost:${attemptPort}`);
4026
4069
  if (!noOpen) {
4027
4070
  const url = `http://localhost:${attemptPort}`;
4028
- const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
4071
+ const opener =
4072
+ process.platform === "darwin"
4073
+ ? "open"
4074
+ : process.platform === "win32"
4075
+ ? "start"
4076
+ : "xdg-open";
4029
4077
  try {
4030
- spawn(opener, [url], { stdio: 'ignore', detached: true });
4078
+ spawn(opener, [url], { stdio: "ignore", detached: true });
4031
4079
  } catch (err) {
4032
- console.warn('Failed to open browser automatically. Please open this URL manually:', url);
4080
+ console.warn(
4081
+ "Failed to open browser automatically. Please open this URL manually:",
4082
+ url,
4083
+ );
4033
4084
  }
4034
4085
  }
4035
4086
  resolve(ctx);
@@ -4051,17 +4102,21 @@ function createDiffServer(diffContent) {
4051
4102
  stdinContent = stdinData;
4052
4103
 
4053
4104
  // Check if it looks like a diff
4054
- if (stdinContent.startsWith('diff --git') || stdinContent.includes('\n+++ ') || stdinContent.includes('\n--- ')) {
4105
+ if (
4106
+ stdinContent.startsWith("diff --git") ||
4107
+ stdinContent.includes("\n+++ ") ||
4108
+ stdinContent.includes("\n--- ")
4109
+ ) {
4055
4110
  diffMode = true;
4056
- console.log('Starting diff viewer from stdin...');
4111
+ console.log("Starting diff viewer from stdin...");
4057
4112
  serversRunning = 1;
4058
4113
  await createDiffServer(stdinContent);
4059
- console.log('Close the browser tab or Submit & Exit to finish.');
4114
+ console.log("Close the browser tab or Submit & Exit to finish.");
4060
4115
  } else {
4061
4116
  // Treat as plain text
4062
- console.log('Starting text viewer from stdin...');
4117
+ console.log("Starting text viewer from stdin...");
4063
4118
  // For now, just show message - could enhance to support any text
4064
- console.error('Non-diff stdin content is not supported yet. Use a file instead.');
4119
+ console.error("Non-diff stdin content is not supported yet. Use a file instead.");
4065
4120
  process.exit(1);
4066
4121
  }
4067
4122
  } else if (resolvedPaths.length > 0) {
@@ -4071,31 +4126,31 @@ function createDiffServer(diffContent) {
4071
4126
  for (const filePath of resolvedPaths) {
4072
4127
  await createFileServer(filePath);
4073
4128
  }
4074
- console.log('Close all browser tabs or Submit & Exit to finish.');
4129
+ console.log("Close all browser tabs or Submit & Exit to finish.");
4075
4130
  } else {
4076
4131
  // No files and no stdin: try auto git diff
4077
- console.log('No files specified. Running git diff HEAD...');
4132
+ console.log("No files specified. Running git diff HEAD...");
4078
4133
  try {
4079
4134
  const gitDiff = await runGitDiff();
4080
- if (gitDiff.trim() === '') {
4081
- console.log('No changes detected (working tree clean).');
4082
- console.log('');
4083
- console.log('Usage: reviw <file...> [options]');
4084
- console.log(' git diff | reviw [options]');
4085
- console.log(' reviw (auto runs git diff HEAD)');
4135
+ if (gitDiff.trim() === "") {
4136
+ console.log("No changes detected (working tree clean).");
4137
+ console.log("");
4138
+ console.log("Usage: reviw <file...> [options]");
4139
+ console.log(" git diff | reviw [options]");
4140
+ console.log(" reviw (auto runs git diff HEAD)");
4086
4141
  process.exit(0);
4087
4142
  }
4088
4143
  diffMode = true;
4089
4144
  stdinContent = gitDiff;
4090
- console.log('Starting diff viewer...');
4145
+ console.log("Starting diff viewer...");
4091
4146
  serversRunning = 1;
4092
4147
  await createDiffServer(gitDiff);
4093
- console.log('Close the browser tab or Submit & Exit to finish.');
4148
+ console.log("Close the browser tab or Submit & Exit to finish.");
4094
4149
  } catch (err) {
4095
4150
  console.error(err.message);
4096
- console.log('');
4097
- console.log('Usage: reviw <file...> [options]');
4098
- console.log(' git diff | reviw [options]');
4151
+ console.log("");
4152
+ console.log("Usage: reviw <file...> [options]");
4153
+ console.log(" git diff | reviw [options]");
4099
4154
  process.exit(1);
4100
4155
  }
4101
4156
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reviw",
3
- "version": "0.9.0",
3
+ "version": "0.9.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": {