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.
- package/README.md +3 -3
- package/cli.cjs +296 -241
- 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
|
|
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
|
|
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:
|
|
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
|
|
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(
|
|
15
|
-
const http = require(
|
|
16
|
-
const path = require(
|
|
17
|
-
const { spawn } = require(
|
|
18
|
-
const chardet = require(
|
|
19
|
-
const iconv = require(
|
|
20
|
-
const marked = require(
|
|
21
|
-
const yaml = require(
|
|
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 =
|
|
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 ===
|
|
36
|
+
if (arg === "--port" && args[i + 1]) {
|
|
37
37
|
basePort = Number(args[i + 1]);
|
|
38
38
|
i += 1;
|
|
39
|
-
} else if ((arg ===
|
|
39
|
+
} else if ((arg === "--encoding" || arg === "-e") && args[i + 1]) {
|
|
40
40
|
encodingOpt = args[i + 1];
|
|
41
41
|
i += 1;
|
|
42
|
-
} else if (arg ===
|
|
42
|
+
} else if (arg === "--no-open") {
|
|
43
43
|
noOpen = true;
|
|
44
|
-
} else if (arg ===
|
|
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:
|
|
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(
|
|
83
|
-
process.stdin.on(
|
|
82
|
+
process.stdin.setEncoding("utf8");
|
|
83
|
+
process.stdin.on("data", (chunk) => {
|
|
84
84
|
data += chunk;
|
|
85
85
|
});
|
|
86
|
-
process.stdin.on(
|
|
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(
|
|
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(
|
|
106
|
+
const { execSync } = require("child_process");
|
|
107
107
|
try {
|
|
108
108
|
// Check if we're in a git repo
|
|
109
|
-
execSync(
|
|
109
|
+
execSync("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
|
|
110
110
|
// Run git diff HEAD
|
|
111
|
-
const diff = execSync(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
159
|
+
if (line.startsWith("new file mode")) {
|
|
160
160
|
currentFile.isNew = true;
|
|
161
161
|
continue;
|
|
162
162
|
}
|
|
163
|
-
if (line.startsWith(
|
|
163
|
+
if (line.startsWith("deleted file mode")) {
|
|
164
164
|
currentFile.isDeleted = true;
|
|
165
165
|
continue;
|
|
166
166
|
}
|
|
167
|
-
if (line.startsWith(
|
|
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(
|
|
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:
|
|
196
|
-
} else if (line.startsWith(
|
|
197
|
-
hunk.lines.push({ type:
|
|
198
|
-
} else if (line.startsWith(
|
|
199
|
-
hunk.lines.push({ type:
|
|
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 +=
|
|
239
|
-
if (file.isDeleted) label +=
|
|
240
|
-
if (file.isBinary) label +=
|
|
238
|
+
if (file.isNew) label += " (new)";
|
|
239
|
+
if (file.isDeleted) label += " (deleted)";
|
|
240
|
+
if (file.isBinary) label += " (binary)";
|
|
241
241
|
rows.push({
|
|
242
|
-
type:
|
|
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:
|
|
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:
|
|
278
|
-
mode:
|
|
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 ===
|
|
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 ===
|
|
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
|
-
|
|
339
|
-
utf8:
|
|
340
|
-
|
|
341
|
-
sjis:
|
|
342
|
-
|
|
343
|
-
cp932:
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
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) ||
|
|
361
|
-
if (encoding !==
|
|
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(
|
|
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 ===
|
|
378
|
-
if (!csvText.includes(
|
|
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(
|
|
422
|
+
const frontmatterText = frontmatterLines.join("\n");
|
|
423
423
|
|
|
424
424
|
try {
|
|
425
425
|
const frontmatter = yaml.load(frontmatterText);
|
|
426
|
-
if (frontmatter && typeof frontmatter ===
|
|
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 +=
|
|
431
|
+
frontmatterHtml += "<tbody>";
|
|
432
432
|
|
|
433
433
|
function renderValue(val) {
|
|
434
434
|
if (Array.isArray(val)) {
|
|
435
|
-
return val
|
|
435
|
+
return val
|
|
436
|
+
.map((v) => '<span class="fm-tag">' + escapeHtmlChars(String(v)) + "</span>")
|
|
437
|
+
.join(" ");
|
|
436
438
|
}
|
|
437
|
-
if (typeof val ===
|
|
438
|
-
return
|
|
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 +=
|
|
446
|
+
frontmatterHtml +=
|
|
447
|
+
"<tr><th>" + escapeHtmlChars(key) + "</th><td>" + renderValue(val) + "</td></tr>";
|
|
445
448
|
}
|
|
446
449
|
|
|
447
|
-
frontmatterHtml +=
|
|
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(
|
|
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,
|
|
471
|
-
.replace(/</g,
|
|
472
|
-
.replace(/>/g,
|
|
473
|
-
.replace(/"/g,
|
|
473
|
+
.replace(/&/g, "&")
|
|
474
|
+
.replace(/</g, "<")
|
|
475
|
+
.replace(/>/g, ">")
|
|
476
|
+
.replace(/"/g, """);
|
|
474
477
|
}
|
|
475
478
|
|
|
476
479
|
function loadData(filePath) {
|
|
477
480
|
const ext = path.extname(filePath).toLowerCase();
|
|
478
|
-
if (ext ===
|
|
481
|
+
if (ext === ".csv" || ext === ".tsv") {
|
|
479
482
|
const data = loadCsv(filePath);
|
|
480
|
-
return { ...data, mode:
|
|
483
|
+
return { ...data, mode: "csv" };
|
|
481
484
|
}
|
|
482
|
-
if (ext ===
|
|
485
|
+
if (ext === ".md" || ext === ".markdown") {
|
|
483
486
|
const data = loadMarkdown(filePath);
|
|
484
|
-
return { ...data, mode:
|
|
487
|
+
return { ...data, mode: "markdown" };
|
|
485
488
|
}
|
|
486
|
-
if (ext ===
|
|
487
|
-
const content = fs.readFileSync(filePath,
|
|
489
|
+
if (ext === ".diff" || ext === ".patch") {
|
|
490
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
488
491
|
const data = loadDiff(content);
|
|
489
|
-
return { ...data, mode:
|
|
492
|
+
return { ...data, mode: "diff" };
|
|
490
493
|
}
|
|
491
494
|
// default text
|
|
492
495
|
const data = loadText(filePath);
|
|
493
|
-
return { ...data, mode:
|
|
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,
|
|
502
|
-
.replace(/>/g,
|
|
503
|
-
.replace(/\u2028/g,
|
|
504
|
-
.replace(/\u2029/g,
|
|
505
|
-
.replace(/`/g,
|
|
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 ===
|
|
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 ?
|
|
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
|
-
${
|
|
2222
|
-
|
|
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 })
|
|
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 })
|
|
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 ===
|
|
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(
|
|
3670
|
+
let data = "";
|
|
3671
|
+
req.on("data", (chunk) => {
|
|
3657
3672
|
data += chunk;
|
|
3658
3673
|
if (data.length > 2 * 1024 * 1024) {
|
|
3659
|
-
reject(new Error(
|
|
3674
|
+
reject(new Error("payload too large"));
|
|
3660
3675
|
req.destroy();
|
|
3661
3676
|
}
|
|
3662
3677
|
});
|
|
3663
|
-
req.on(
|
|
3664
|
-
req.on(
|
|
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(
|
|
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) => {
|
|
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(
|
|
3702
|
-
process.on(
|
|
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 ===
|
|
3742
|
+
const payload = typeof data === "string" ? data : JSON.stringify(data);
|
|
3724
3743
|
ctx.sseClients.forEach((res) => {
|
|
3725
|
-
try {
|
|
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(
|
|
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(
|
|
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) => {
|
|
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 ===
|
|
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
|
-
|
|
3770
|
-
|
|
3771
|
-
Pragma:
|
|
3772
|
-
Expires:
|
|
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(
|
|
3777
|
-
res.writeHead(500, {
|
|
3778
|
-
res.end(
|
|
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 ===
|
|
3784
|
-
res.writeHead(200, {
|
|
3785
|
-
res.end(
|
|
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 ===
|
|
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, {
|
|
3797
|
-
res.end(
|
|
3821
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
3822
|
+
res.end("bye");
|
|
3798
3823
|
shutdownServer(payload);
|
|
3799
3824
|
} catch (err) {
|
|
3800
|
-
console.error(
|
|
3801
|
-
res.writeHead(400, {
|
|
3802
|
-
res.end(
|
|
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 ===
|
|
3833
|
+
if (req.method === "GET" && req.url === "/sse") {
|
|
3809
3834
|
res.writeHead(200, {
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
Connection:
|
|
3813
|
-
|
|
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(
|
|
3840
|
+
res.write("retry: 3000\n\n");
|
|
3816
3841
|
ctx.sseClients.add(res);
|
|
3817
|
-
req.on(
|
|
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 ===
|
|
3847
|
+
if (req.method === "GET") {
|
|
3823
3848
|
const MIME_TYPES = {
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
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(
|
|
3838
|
-
if (urlPath.includes(
|
|
3839
|
-
res.writeHead(403, {
|
|
3840
|
-
res.end(
|
|
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, {
|
|
3846
|
-
res.end(
|
|
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] ||
|
|
3876
|
+
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
3852
3877
|
const content = fs.readFileSync(staticPath);
|
|
3853
|
-
res.writeHead(200, {
|
|
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, {
|
|
3863
|
-
res.end(
|
|
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(
|
|
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(
|
|
3875
|
-
if (err.code ===
|
|
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 =
|
|
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:
|
|
3925
|
+
spawn(opener, [url], { stdio: "ignore", detached: true });
|
|
3894
3926
|
} catch (err) {
|
|
3895
|
-
console.warn(
|
|
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 ===
|
|
3956
|
+
const payload = typeof data === "string" ? data : JSON.stringify(data);
|
|
3922
3957
|
ctx.sseClients.forEach((res) => {
|
|
3923
|
-
try {
|
|
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) => {
|
|
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 ===
|
|
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
|
-
|
|
3949
|
-
|
|
3950
|
-
Pragma:
|
|
3951
|
-
Expires:
|
|
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(
|
|
3956
|
-
res.writeHead(500, {
|
|
3957
|
-
res.end(
|
|
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 ===
|
|
3963
|
-
res.writeHead(200, {
|
|
3964
|
-
res.end(
|
|
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 ===
|
|
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, {
|
|
3976
|
-
res.end(
|
|
4016
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
4017
|
+
res.end("bye");
|
|
3977
4018
|
shutdownServer(payload);
|
|
3978
4019
|
} catch (err) {
|
|
3979
|
-
console.error(
|
|
3980
|
-
res.writeHead(400, {
|
|
3981
|
-
res.end(
|
|
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 ===
|
|
4028
|
+
if (req.method === "GET" && req.url === "/sse") {
|
|
3988
4029
|
res.writeHead(200, {
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
Connection:
|
|
3992
|
-
|
|
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(
|
|
4035
|
+
res.write("retry: 3000\n\n");
|
|
3995
4036
|
ctx.sseClients.add(res);
|
|
3996
|
-
req.on(
|
|
4037
|
+
req.on("close", () => ctx.sseClients.delete(res));
|
|
3997
4038
|
return;
|
|
3998
4039
|
}
|
|
3999
4040
|
|
|
4000
|
-
res.writeHead(404, {
|
|
4001
|
-
res.end(
|
|
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(
|
|
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(
|
|
4013
|
-
if (err.code ===
|
|
4055
|
+
ctx.server.once("error", (err) => {
|
|
4056
|
+
if (err.code === "EADDRINUSE") {
|
|
4014
4057
|
tryListen(attemptPort + 1, attempts + 1);
|
|
4015
4058
|
} else {
|
|
4016
|
-
console.error(
|
|
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(
|
|
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 =
|
|
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:
|
|
4078
|
+
spawn(opener, [url], { stdio: "ignore", detached: true });
|
|
4031
4079
|
} catch (err) {
|
|
4032
|
-
console.warn(
|
|
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 (
|
|
4105
|
+
if (
|
|
4106
|
+
stdinContent.startsWith("diff --git") ||
|
|
4107
|
+
stdinContent.includes("\n+++ ") ||
|
|
4108
|
+
stdinContent.includes("\n--- ")
|
|
4109
|
+
) {
|
|
4055
4110
|
diffMode = true;
|
|
4056
|
-
console.log(
|
|
4111
|
+
console.log("Starting diff viewer from stdin...");
|
|
4057
4112
|
serversRunning = 1;
|
|
4058
4113
|
await createDiffServer(stdinContent);
|
|
4059
|
-
console.log(
|
|
4114
|
+
console.log("Close the browser tab or Submit & Exit to finish.");
|
|
4060
4115
|
} else {
|
|
4061
4116
|
// Treat as plain text
|
|
4062
|
-
console.log(
|
|
4117
|
+
console.log("Starting text viewer from stdin...");
|
|
4063
4118
|
// For now, just show message - could enhance to support any text
|
|
4064
|
-
console.error(
|
|
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(
|
|
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(
|
|
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(
|
|
4082
|
-
console.log(
|
|
4083
|
-
console.log(
|
|
4084
|
-
console.log(
|
|
4085
|
-
console.log(
|
|
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(
|
|
4145
|
+
console.log("Starting diff viewer...");
|
|
4091
4146
|
serversRunning = 1;
|
|
4092
4147
|
await createDiffServer(gitDiff);
|
|
4093
|
-
console.log(
|
|
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(
|
|
4098
|
-
console.log(
|
|
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
|
}
|