reviw 0.9.0 → 0.9.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.
Files changed (3) hide show
  1. package/README.md +3 -3
  2. package/cli.cjs +305 -248
  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">
@@ -1975,15 +1978,15 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
1975
1978
  transform: scale(1.04);
1976
1979
  }
1977
1980
  .image-container {
1978
- max-width: 90vw;
1979
- max-height: 90vh;
1981
+ width: 90vw;
1982
+ height: 90vh;
1980
1983
  display: flex;
1981
1984
  justify-content: center;
1982
1985
  align-items: center;
1983
1986
  }
1984
1987
  .image-container img {
1985
- max-width: 100%;
1986
- max-height: 90vh;
1988
+ width: 100%;
1989
+ height: 100%;
1987
1990
  object-fit: contain;
1988
1991
  border-radius: 8px;
1989
1992
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
@@ -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">
@@ -3625,9 +3640,11 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
3625
3640
 
3626
3641
  imageContainer.innerHTML = '';
3627
3642
  const clonedImg = img.cloneNode(true);
3628
- clonedImg.style.maxWidth = '90vw';
3629
- clonedImg.style.maxHeight = '90vh';
3630
- clonedImg.style.objectFit = 'contain';
3643
+ // CSSで制御するためインラインスタイルはリセット
3644
+ clonedImg.style.width = '';
3645
+ clonedImg.style.height = '';
3646
+ clonedImg.style.maxWidth = '';
3647
+ clonedImg.style.maxHeight = '';
3631
3648
  clonedImg.style.cursor = 'default';
3632
3649
  imageContainer.appendChild(clonedImg);
3633
3650
 
@@ -3642,7 +3659,7 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
3642
3659
 
3643
3660
  function buildHtml(filePath) {
3644
3661
  const data = loadData(filePath);
3645
- if (data.mode === 'diff') {
3662
+ if (data.mode === "diff") {
3646
3663
  return diffHtmlTemplate(data);
3647
3664
  }
3648
3665
  const { rows, cols, title, mode, preview } = data;
@@ -3652,16 +3669,16 @@ function buildHtml(filePath) {
3652
3669
  // --- HTTP Server -----------------------------------------------------------
3653
3670
  function readBody(req) {
3654
3671
  return new Promise((resolve, reject) => {
3655
- let data = '';
3656
- req.on('data', (chunk) => {
3672
+ let data = "";
3673
+ req.on("data", (chunk) => {
3657
3674
  data += chunk;
3658
3675
  if (data.length > 2 * 1024 * 1024) {
3659
- reject(new Error('payload too large'));
3676
+ reject(new Error("payload too large"));
3660
3677
  req.destroy();
3661
3678
  }
3662
3679
  });
3663
- req.on('end', () => resolve(data));
3664
- req.on('error', reject);
3680
+ req.on("end", () => resolve(data));
3681
+ req.on("error", reject);
3665
3682
  });
3666
3683
  }
3667
3684
 
@@ -3669,7 +3686,7 @@ const MAX_PORT_ATTEMPTS = 100;
3669
3686
  const activeServers = new Map();
3670
3687
 
3671
3688
  function outputAllResults() {
3672
- console.log('=== All comments received ===');
3689
+ console.log("=== All comments received ===");
3673
3690
  if (allResults.length === 1) {
3674
3691
  const yamlOut = yaml.dump(allResults[0], { noRefs: true, lineWidth: 120 });
3675
3692
  console.log(yamlOut.trim());
@@ -3691,15 +3708,19 @@ function shutdownAll() {
3691
3708
  for (const ctx of activeServers.values()) {
3692
3709
  if (ctx.watcher) ctx.watcher.close();
3693
3710
  if (ctx.heartbeat) clearInterval(ctx.heartbeat);
3694
- ctx.sseClients.forEach((res) => { try { res.end(); } catch (_) {} });
3711
+ ctx.sseClients.forEach((res) => {
3712
+ try {
3713
+ res.end();
3714
+ } catch (_) {}
3715
+ });
3695
3716
  if (ctx.server) ctx.server.close();
3696
3717
  }
3697
3718
  outputAllResults();
3698
3719
  setTimeout(() => process.exit(0), 500).unref();
3699
3720
  }
3700
3721
 
3701
- process.on('SIGINT', shutdownAll);
3702
- process.on('SIGTERM', shutdownAll);
3722
+ process.on("SIGINT", shutdownAll);
3723
+ process.on("SIGTERM", shutdownAll);
3703
3724
 
3704
3725
  function createFileServer(filePath) {
3705
3726
  return new Promise((resolve) => {
@@ -3716,19 +3737,21 @@ function createFileServer(filePath) {
3716
3737
  reloadTimer: null,
3717
3738
  server: null,
3718
3739
  opened: false,
3719
- port: 0
3740
+ port: 0,
3720
3741
  };
3721
3742
 
3722
3743
  function broadcast(data) {
3723
- const payload = typeof data === 'string' ? data : JSON.stringify(data);
3744
+ const payload = typeof data === "string" ? data : JSON.stringify(data);
3724
3745
  ctx.sseClients.forEach((res) => {
3725
- try { res.write(`data: ${payload}\n\n`); } catch (_) {}
3746
+ try {
3747
+ res.write(`data: ${payload}\n\n`);
3748
+ } catch (_) {}
3726
3749
  });
3727
3750
  }
3728
3751
 
3729
3752
  function notifyReload() {
3730
3753
  clearTimeout(ctx.reloadTimer);
3731
- ctx.reloadTimer = setTimeout(() => broadcast('reload'), 150);
3754
+ ctx.reloadTimer = setTimeout(() => broadcast("reload"), 150);
3732
3755
  }
3733
3756
 
3734
3757
  function startWatcher() {
@@ -3737,7 +3760,7 @@ function createFileServer(filePath) {
3737
3760
  } catch (err) {
3738
3761
  console.warn(`Failed to start file watcher for ${baseName}:`, err);
3739
3762
  }
3740
- ctx.heartbeat = setInterval(() => broadcast('ping'), 25000);
3763
+ ctx.heartbeat = setInterval(() => broadcast("ping"), 25000);
3741
3764
  }
3742
3765
 
3743
3766
  function shutdownServer(result) {
@@ -3749,7 +3772,11 @@ function createFileServer(filePath) {
3749
3772
  clearInterval(ctx.heartbeat);
3750
3773
  ctx.heartbeat = null;
3751
3774
  }
3752
- ctx.sseClients.forEach((res) => { try { res.end(); } catch (_) {} });
3775
+ ctx.sseClients.forEach((res) => {
3776
+ try {
3777
+ res.end();
3778
+ } catch (_) {}
3779
+ });
3753
3780
  if (ctx.server) {
3754
3781
  ctx.server.close();
3755
3782
  ctx.server = null;
@@ -3762,95 +3789,95 @@ function createFileServer(filePath) {
3762
3789
  }
3763
3790
 
3764
3791
  ctx.server = http.createServer(async (req, res) => {
3765
- if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
3792
+ if (req.method === "GET" && (req.url === "/" || req.url === "/index.html")) {
3766
3793
  try {
3767
3794
  const html = buildHtml(filePath);
3768
3795
  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'
3796
+ "Content-Type": "text/html; charset=utf-8",
3797
+ "Cache-Control": "no-store, no-cache, must-revalidate",
3798
+ Pragma: "no-cache",
3799
+ Expires: "0",
3773
3800
  });
3774
3801
  res.end(html);
3775
3802
  } 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.');
3803
+ console.error("File load error", err);
3804
+ res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
3805
+ res.end("Failed to load file. Please check the file.");
3779
3806
  }
3780
3807
  return;
3781
3808
  }
3782
3809
 
3783
- if (req.method === 'GET' && req.url === '/healthz') {
3784
- res.writeHead(200, { 'Content-Type': 'text/plain' });
3785
- res.end('ok');
3810
+ if (req.method === "GET" && req.url === "/healthz") {
3811
+ res.writeHead(200, { "Content-Type": "text/plain" });
3812
+ res.end("ok");
3786
3813
  return;
3787
3814
  }
3788
3815
 
3789
- if (req.method === 'POST' && req.url === '/exit') {
3816
+ if (req.method === "POST" && req.url === "/exit") {
3790
3817
  try {
3791
3818
  const raw = await readBody(req);
3792
3819
  let payload = {};
3793
3820
  if (raw && raw.trim()) {
3794
3821
  payload = JSON.parse(raw);
3795
3822
  }
3796
- res.writeHead(200, { 'Content-Type': 'text/plain' });
3797
- res.end('bye');
3823
+ res.writeHead(200, { "Content-Type": "text/plain" });
3824
+ res.end("bye");
3798
3825
  shutdownServer(payload);
3799
3826
  } catch (err) {
3800
- console.error('payload parse error', err);
3801
- res.writeHead(400, { 'Content-Type': 'text/plain' });
3802
- res.end('bad request');
3827
+ console.error("payload parse error", err);
3828
+ res.writeHead(400, { "Content-Type": "text/plain" });
3829
+ res.end("bad request");
3803
3830
  shutdownServer(null);
3804
3831
  }
3805
3832
  return;
3806
3833
  }
3807
3834
 
3808
- if (req.method === 'GET' && req.url === '/sse') {
3835
+ if (req.method === "GET" && req.url === "/sse") {
3809
3836
  res.writeHead(200, {
3810
- 'Content-Type': 'text/event-stream',
3811
- 'Cache-Control': 'no-cache',
3812
- Connection: 'keep-alive',
3813
- 'X-Accel-Buffering': 'no'
3837
+ "Content-Type": "text/event-stream",
3838
+ "Cache-Control": "no-cache",
3839
+ Connection: "keep-alive",
3840
+ "X-Accel-Buffering": "no",
3814
3841
  });
3815
- res.write('retry: 3000\n\n');
3842
+ res.write("retry: 3000\n\n");
3816
3843
  ctx.sseClients.add(res);
3817
- req.on('close', () => ctx.sseClients.delete(res));
3844
+ req.on("close", () => ctx.sseClients.delete(res));
3818
3845
  return;
3819
3846
  }
3820
3847
 
3821
3848
  // Static file serving for images and other assets
3822
- if (req.method === 'GET') {
3849
+ if (req.method === "GET") {
3823
3850
  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',
3851
+ ".png": "image/png",
3852
+ ".jpg": "image/jpeg",
3853
+ ".jpeg": "image/jpeg",
3854
+ ".gif": "image/gif",
3855
+ ".webp": "image/webp",
3856
+ ".svg": "image/svg+xml",
3857
+ ".ico": "image/x-icon",
3858
+ ".css": "text/css",
3859
+ ".js": "application/javascript",
3860
+ ".json": "application/json",
3861
+ ".pdf": "application/pdf",
3835
3862
  };
3836
3863
  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');
3864
+ const urlPath = decodeURIComponent(req.url.split("?")[0]);
3865
+ if (urlPath.includes("..")) {
3866
+ res.writeHead(403, { "Content-Type": "text/plain" });
3867
+ res.end("forbidden");
3841
3868
  return;
3842
3869
  }
3843
3870
  const staticPath = path.join(baseDir, urlPath);
3844
3871
  if (!staticPath.startsWith(baseDir)) {
3845
- res.writeHead(403, { 'Content-Type': 'text/plain' });
3846
- res.end('forbidden');
3872
+ res.writeHead(403, { "Content-Type": "text/plain" });
3873
+ res.end("forbidden");
3847
3874
  return;
3848
3875
  }
3849
3876
  if (fs.existsSync(staticPath) && fs.statSync(staticPath).isFile()) {
3850
3877
  const ext = path.extname(staticPath).toLowerCase();
3851
- const contentType = MIME_TYPES[ext] || 'application/octet-stream';
3878
+ const contentType = MIME_TYPES[ext] || "application/octet-stream";
3852
3879
  const content = fs.readFileSync(staticPath);
3853
- res.writeHead(200, { 'Content-Type': contentType });
3880
+ res.writeHead(200, { "Content-Type": contentType });
3854
3881
  res.end(content);
3855
3882
  return;
3856
3883
  }
@@ -3859,20 +3886,22 @@ function createFileServer(filePath) {
3859
3886
  }
3860
3887
  }
3861
3888
 
3862
- res.writeHead(404, { 'Content-Type': 'text/plain' });
3863
- res.end('not found');
3889
+ res.writeHead(404, { "Content-Type": "text/plain" });
3890
+ res.end("not found");
3864
3891
  });
3865
3892
 
3866
3893
  function tryListen(attemptPort, attempts = 0) {
3867
3894
  if (attempts >= MAX_PORT_ATTEMPTS) {
3868
- console.error(`Could not find an available port for ${baseName} after ${MAX_PORT_ATTEMPTS} attempts.`);
3895
+ console.error(
3896
+ `Could not find an available port for ${baseName} after ${MAX_PORT_ATTEMPTS} attempts.`,
3897
+ );
3869
3898
  serversRunning--;
3870
3899
  checkAllDone();
3871
3900
  return;
3872
3901
  }
3873
3902
 
3874
- ctx.server.once('error', (err) => {
3875
- if (err.code === 'EADDRINUSE') {
3903
+ ctx.server.once("error", (err) => {
3904
+ if (err.code === "EADDRINUSE") {
3876
3905
  tryListen(attemptPort + 1, attempts + 1);
3877
3906
  } else {
3878
3907
  console.error(`Server error for ${baseName}:`, err);
@@ -3888,11 +3917,19 @@ function createFileServer(filePath) {
3888
3917
  console.log(`Viewer started: http://localhost:${attemptPort} (file: ${baseName})`);
3889
3918
  if (!noOpen) {
3890
3919
  const url = `http://localhost:${attemptPort}`;
3891
- const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
3920
+ const opener =
3921
+ process.platform === "darwin"
3922
+ ? "open"
3923
+ : process.platform === "win32"
3924
+ ? "start"
3925
+ : "xdg-open";
3892
3926
  try {
3893
- spawn(opener, [url], { stdio: 'ignore', detached: true });
3927
+ spawn(opener, [url], { stdio: "ignore", detached: true });
3894
3928
  } catch (err) {
3895
- console.warn('Failed to open browser automatically. Please open this URL manually:', url);
3929
+ console.warn(
3930
+ "Failed to open browser automatically. Please open this URL manually:",
3931
+ url,
3932
+ );
3896
3933
  }
3897
3934
  }
3898
3935
  startWatcher();
@@ -3914,13 +3951,15 @@ function createDiffServer(diffContent) {
3914
3951
  sseClients: new Set(),
3915
3952
  heartbeat: null,
3916
3953
  server: null,
3917
- port: 0
3954
+ port: 0,
3918
3955
  };
3919
3956
 
3920
3957
  function broadcast(data) {
3921
- const payload = typeof data === 'string' ? data : JSON.stringify(data);
3958
+ const payload = typeof data === "string" ? data : JSON.stringify(data);
3922
3959
  ctx.sseClients.forEach((res) => {
3923
- try { res.write(`data: ${payload}\n\n`); } catch (_) {}
3960
+ try {
3961
+ res.write(`data: ${payload}\n\n`);
3962
+ } catch (_) {}
3924
3963
  });
3925
3964
  }
3926
3965
 
@@ -3929,7 +3968,11 @@ function createDiffServer(diffContent) {
3929
3968
  clearInterval(ctx.heartbeat);
3930
3969
  ctx.heartbeat = null;
3931
3970
  }
3932
- ctx.sseClients.forEach((res) => { try { res.end(); } catch (_) {} });
3971
+ ctx.sseClients.forEach((res) => {
3972
+ try {
3973
+ res.end();
3974
+ } catch (_) {}
3975
+ });
3933
3976
  if (ctx.server) {
3934
3977
  ctx.server.close();
3935
3978
  ctx.server = null;
@@ -3941,79 +3984,81 @@ function createDiffServer(diffContent) {
3941
3984
  }
3942
3985
 
3943
3986
  ctx.server = http.createServer(async (req, res) => {
3944
- if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
3987
+ if (req.method === "GET" && (req.url === "/" || req.url === "/index.html")) {
3945
3988
  try {
3946
3989
  const html = diffHtmlTemplate(diffData);
3947
3990
  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'
3991
+ "Content-Type": "text/html; charset=utf-8",
3992
+ "Cache-Control": "no-store, no-cache, must-revalidate",
3993
+ Pragma: "no-cache",
3994
+ Expires: "0",
3952
3995
  });
3953
3996
  res.end(html);
3954
3997
  } 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.');
3998
+ console.error("Diff render error", err);
3999
+ res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
4000
+ res.end("Failed to render diff view.");
3958
4001
  }
3959
4002
  return;
3960
4003
  }
3961
4004
 
3962
- if (req.method === 'GET' && req.url === '/healthz') {
3963
- res.writeHead(200, { 'Content-Type': 'text/plain' });
3964
- res.end('ok');
4005
+ if (req.method === "GET" && req.url === "/healthz") {
4006
+ res.writeHead(200, { "Content-Type": "text/plain" });
4007
+ res.end("ok");
3965
4008
  return;
3966
4009
  }
3967
4010
 
3968
- if (req.method === 'POST' && req.url === '/exit') {
4011
+ if (req.method === "POST" && req.url === "/exit") {
3969
4012
  try {
3970
4013
  const raw = await readBody(req);
3971
4014
  let payload = {};
3972
4015
  if (raw && raw.trim()) {
3973
4016
  payload = JSON.parse(raw);
3974
4017
  }
3975
- res.writeHead(200, { 'Content-Type': 'text/plain' });
3976
- res.end('bye');
4018
+ res.writeHead(200, { "Content-Type": "text/plain" });
4019
+ res.end("bye");
3977
4020
  shutdownServer(payload);
3978
4021
  } catch (err) {
3979
- console.error('payload parse error', err);
3980
- res.writeHead(400, { 'Content-Type': 'text/plain' });
3981
- res.end('bad request');
4022
+ console.error("payload parse error", err);
4023
+ res.writeHead(400, { "Content-Type": "text/plain" });
4024
+ res.end("bad request");
3982
4025
  shutdownServer(null);
3983
4026
  }
3984
4027
  return;
3985
4028
  }
3986
4029
 
3987
- if (req.method === 'GET' && req.url === '/sse') {
4030
+ if (req.method === "GET" && req.url === "/sse") {
3988
4031
  res.writeHead(200, {
3989
- 'Content-Type': 'text/event-stream',
3990
- 'Cache-Control': 'no-cache',
3991
- Connection: 'keep-alive',
3992
- 'X-Accel-Buffering': 'no'
4032
+ "Content-Type": "text/event-stream",
4033
+ "Cache-Control": "no-cache",
4034
+ Connection: "keep-alive",
4035
+ "X-Accel-Buffering": "no",
3993
4036
  });
3994
- res.write('retry: 3000\n\n');
4037
+ res.write("retry: 3000\n\n");
3995
4038
  ctx.sseClients.add(res);
3996
- req.on('close', () => ctx.sseClients.delete(res));
4039
+ req.on("close", () => ctx.sseClients.delete(res));
3997
4040
  return;
3998
4041
  }
3999
4042
 
4000
- res.writeHead(404, { 'Content-Type': 'text/plain' });
4001
- res.end('not found');
4043
+ res.writeHead(404, { "Content-Type": "text/plain" });
4044
+ res.end("not found");
4002
4045
  });
4003
4046
 
4004
4047
  function tryListen(attemptPort, attempts = 0) {
4005
4048
  if (attempts >= MAX_PORT_ATTEMPTS) {
4006
- console.error(`Could not find an available port for diff viewer after ${MAX_PORT_ATTEMPTS} attempts.`);
4049
+ console.error(
4050
+ `Could not find an available port for diff viewer after ${MAX_PORT_ATTEMPTS} attempts.`,
4051
+ );
4007
4052
  serversRunning--;
4008
4053
  checkAllDone();
4009
4054
  return;
4010
4055
  }
4011
4056
 
4012
- ctx.server.once('error', (err) => {
4013
- if (err.code === 'EADDRINUSE') {
4057
+ ctx.server.once("error", (err) => {
4058
+ if (err.code === "EADDRINUSE") {
4014
4059
  tryListen(attemptPort + 1, attempts + 1);
4015
4060
  } else {
4016
- console.error('Diff server error:', err);
4061
+ console.error("Diff server error:", err);
4017
4062
  serversRunning--;
4018
4063
  checkAllDone();
4019
4064
  }
@@ -4021,15 +4066,23 @@ function createDiffServer(diffContent) {
4021
4066
 
4022
4067
  ctx.server.listen(attemptPort, () => {
4023
4068
  ctx.port = attemptPort;
4024
- ctx.heartbeat = setInterval(() => broadcast('ping'), 25000);
4069
+ ctx.heartbeat = setInterval(() => broadcast("ping"), 25000);
4025
4070
  console.log(`Diff viewer started: http://localhost:${attemptPort}`);
4026
4071
  if (!noOpen) {
4027
4072
  const url = `http://localhost:${attemptPort}`;
4028
- const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
4073
+ const opener =
4074
+ process.platform === "darwin"
4075
+ ? "open"
4076
+ : process.platform === "win32"
4077
+ ? "start"
4078
+ : "xdg-open";
4029
4079
  try {
4030
- spawn(opener, [url], { stdio: 'ignore', detached: true });
4080
+ spawn(opener, [url], { stdio: "ignore", detached: true });
4031
4081
  } catch (err) {
4032
- console.warn('Failed to open browser automatically. Please open this URL manually:', url);
4082
+ console.warn(
4083
+ "Failed to open browser automatically. Please open this URL manually:",
4084
+ url,
4085
+ );
4033
4086
  }
4034
4087
  }
4035
4088
  resolve(ctx);
@@ -4051,17 +4104,21 @@ function createDiffServer(diffContent) {
4051
4104
  stdinContent = stdinData;
4052
4105
 
4053
4106
  // Check if it looks like a diff
4054
- if (stdinContent.startsWith('diff --git') || stdinContent.includes('\n+++ ') || stdinContent.includes('\n--- ')) {
4107
+ if (
4108
+ stdinContent.startsWith("diff --git") ||
4109
+ stdinContent.includes("\n+++ ") ||
4110
+ stdinContent.includes("\n--- ")
4111
+ ) {
4055
4112
  diffMode = true;
4056
- console.log('Starting diff viewer from stdin...');
4113
+ console.log("Starting diff viewer from stdin...");
4057
4114
  serversRunning = 1;
4058
4115
  await createDiffServer(stdinContent);
4059
- console.log('Close the browser tab or Submit & Exit to finish.');
4116
+ console.log("Close the browser tab or Submit & Exit to finish.");
4060
4117
  } else {
4061
4118
  // Treat as plain text
4062
- console.log('Starting text viewer from stdin...');
4119
+ console.log("Starting text viewer from stdin...");
4063
4120
  // 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.');
4121
+ console.error("Non-diff stdin content is not supported yet. Use a file instead.");
4065
4122
  process.exit(1);
4066
4123
  }
4067
4124
  } else if (resolvedPaths.length > 0) {
@@ -4071,31 +4128,31 @@ function createDiffServer(diffContent) {
4071
4128
  for (const filePath of resolvedPaths) {
4072
4129
  await createFileServer(filePath);
4073
4130
  }
4074
- console.log('Close all browser tabs or Submit & Exit to finish.');
4131
+ console.log("Close all browser tabs or Submit & Exit to finish.");
4075
4132
  } else {
4076
4133
  // No files and no stdin: try auto git diff
4077
- console.log('No files specified. Running git diff HEAD...');
4134
+ console.log("No files specified. Running git diff HEAD...");
4078
4135
  try {
4079
4136
  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)');
4137
+ if (gitDiff.trim() === "") {
4138
+ console.log("No changes detected (working tree clean).");
4139
+ console.log("");
4140
+ console.log("Usage: reviw <file...> [options]");
4141
+ console.log(" git diff | reviw [options]");
4142
+ console.log(" reviw (auto runs git diff HEAD)");
4086
4143
  process.exit(0);
4087
4144
  }
4088
4145
  diffMode = true;
4089
4146
  stdinContent = gitDiff;
4090
- console.log('Starting diff viewer...');
4147
+ console.log("Starting diff viewer...");
4091
4148
  serversRunning = 1;
4092
4149
  await createDiffServer(gitDiff);
4093
- console.log('Close the browser tab or Submit & Exit to finish.');
4150
+ console.log("Close the browser tab or Submit & Exit to finish.");
4094
4151
  } catch (err) {
4095
4152
  console.error(err.message);
4096
- console.log('');
4097
- console.log('Usage: reviw <file...> [options]');
4098
- console.log(' git diff | reviw [options]');
4153
+ console.log("");
4154
+ console.log("Usage: reviw <file...> [options]");
4155
+ console.log(" git diff | reviw [options]");
4099
4156
  process.exit(1);
4100
4157
  }
4101
4158
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reviw",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Lightweight file reviewer with in-browser comments for CSV, TSV, Markdown, and Git diffs.",
5
5
  "type": "module",
6
6
  "bin": {