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.
- package/README.md +3 -3
- package/cli.cjs +305 -248
- 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">
|
|
@@ -1975,15 +1978,15 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
1975
1978
|
transform: scale(1.04);
|
|
1976
1979
|
}
|
|
1977
1980
|
.image-container {
|
|
1978
|
-
|
|
1979
|
-
|
|
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
|
-
|
|
1986
|
-
|
|
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
|
-
${
|
|
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">
|
|
@@ -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
|
-
|
|
3629
|
-
clonedImg.style.
|
|
3630
|
-
clonedImg.style.
|
|
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 ===
|
|
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(
|
|
3672
|
+
let data = "";
|
|
3673
|
+
req.on("data", (chunk) => {
|
|
3657
3674
|
data += chunk;
|
|
3658
3675
|
if (data.length > 2 * 1024 * 1024) {
|
|
3659
|
-
reject(new Error(
|
|
3676
|
+
reject(new Error("payload too large"));
|
|
3660
3677
|
req.destroy();
|
|
3661
3678
|
}
|
|
3662
3679
|
});
|
|
3663
|
-
req.on(
|
|
3664
|
-
req.on(
|
|
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(
|
|
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) => {
|
|
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(
|
|
3702
|
-
process.on(
|
|
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 ===
|
|
3744
|
+
const payload = typeof data === "string" ? data : JSON.stringify(data);
|
|
3724
3745
|
ctx.sseClients.forEach((res) => {
|
|
3725
|
-
try {
|
|
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(
|
|
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(
|
|
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) => {
|
|
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 ===
|
|
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
|
-
|
|
3770
|
-
|
|
3771
|
-
Pragma:
|
|
3772
|
-
Expires:
|
|
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(
|
|
3777
|
-
res.writeHead(500, {
|
|
3778
|
-
res.end(
|
|
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 ===
|
|
3784
|
-
res.writeHead(200, {
|
|
3785
|
-
res.end(
|
|
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 ===
|
|
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, {
|
|
3797
|
-
res.end(
|
|
3823
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
3824
|
+
res.end("bye");
|
|
3798
3825
|
shutdownServer(payload);
|
|
3799
3826
|
} catch (err) {
|
|
3800
|
-
console.error(
|
|
3801
|
-
res.writeHead(400, {
|
|
3802
|
-
res.end(
|
|
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 ===
|
|
3835
|
+
if (req.method === "GET" && req.url === "/sse") {
|
|
3809
3836
|
res.writeHead(200, {
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
Connection:
|
|
3813
|
-
|
|
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(
|
|
3842
|
+
res.write("retry: 3000\n\n");
|
|
3816
3843
|
ctx.sseClients.add(res);
|
|
3817
|
-
req.on(
|
|
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 ===
|
|
3849
|
+
if (req.method === "GET") {
|
|
3823
3850
|
const MIME_TYPES = {
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
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(
|
|
3838
|
-
if (urlPath.includes(
|
|
3839
|
-
res.writeHead(403, {
|
|
3840
|
-
res.end(
|
|
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, {
|
|
3846
|
-
res.end(
|
|
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] ||
|
|
3878
|
+
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
3852
3879
|
const content = fs.readFileSync(staticPath);
|
|
3853
|
-
res.writeHead(200, {
|
|
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, {
|
|
3863
|
-
res.end(
|
|
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(
|
|
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(
|
|
3875
|
-
if (err.code ===
|
|
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 =
|
|
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:
|
|
3927
|
+
spawn(opener, [url], { stdio: "ignore", detached: true });
|
|
3894
3928
|
} catch (err) {
|
|
3895
|
-
console.warn(
|
|
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 ===
|
|
3958
|
+
const payload = typeof data === "string" ? data : JSON.stringify(data);
|
|
3922
3959
|
ctx.sseClients.forEach((res) => {
|
|
3923
|
-
try {
|
|
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) => {
|
|
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 ===
|
|
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
|
-
|
|
3949
|
-
|
|
3950
|
-
Pragma:
|
|
3951
|
-
Expires:
|
|
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(
|
|
3956
|
-
res.writeHead(500, {
|
|
3957
|
-
res.end(
|
|
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 ===
|
|
3963
|
-
res.writeHead(200, {
|
|
3964
|
-
res.end(
|
|
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 ===
|
|
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, {
|
|
3976
|
-
res.end(
|
|
4018
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
4019
|
+
res.end("bye");
|
|
3977
4020
|
shutdownServer(payload);
|
|
3978
4021
|
} catch (err) {
|
|
3979
|
-
console.error(
|
|
3980
|
-
res.writeHead(400, {
|
|
3981
|
-
res.end(
|
|
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 ===
|
|
4030
|
+
if (req.method === "GET" && req.url === "/sse") {
|
|
3988
4031
|
res.writeHead(200, {
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
Connection:
|
|
3992
|
-
|
|
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(
|
|
4037
|
+
res.write("retry: 3000\n\n");
|
|
3995
4038
|
ctx.sseClients.add(res);
|
|
3996
|
-
req.on(
|
|
4039
|
+
req.on("close", () => ctx.sseClients.delete(res));
|
|
3997
4040
|
return;
|
|
3998
4041
|
}
|
|
3999
4042
|
|
|
4000
|
-
res.writeHead(404, {
|
|
4001
|
-
res.end(
|
|
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(
|
|
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(
|
|
4013
|
-
if (err.code ===
|
|
4057
|
+
ctx.server.once("error", (err) => {
|
|
4058
|
+
if (err.code === "EADDRINUSE") {
|
|
4014
4059
|
tryListen(attemptPort + 1, attempts + 1);
|
|
4015
4060
|
} else {
|
|
4016
|
-
console.error(
|
|
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(
|
|
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 =
|
|
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:
|
|
4080
|
+
spawn(opener, [url], { stdio: "ignore", detached: true });
|
|
4031
4081
|
} catch (err) {
|
|
4032
|
-
console.warn(
|
|
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 (
|
|
4107
|
+
if (
|
|
4108
|
+
stdinContent.startsWith("diff --git") ||
|
|
4109
|
+
stdinContent.includes("\n+++ ") ||
|
|
4110
|
+
stdinContent.includes("\n--- ")
|
|
4111
|
+
) {
|
|
4055
4112
|
diffMode = true;
|
|
4056
|
-
console.log(
|
|
4113
|
+
console.log("Starting diff viewer from stdin...");
|
|
4057
4114
|
serversRunning = 1;
|
|
4058
4115
|
await createDiffServer(stdinContent);
|
|
4059
|
-
console.log(
|
|
4116
|
+
console.log("Close the browser tab or Submit & Exit to finish.");
|
|
4060
4117
|
} else {
|
|
4061
4118
|
// Treat as plain text
|
|
4062
|
-
console.log(
|
|
4119
|
+
console.log("Starting text viewer from stdin...");
|
|
4063
4120
|
// For now, just show message - could enhance to support any text
|
|
4064
|
-
console.error(
|
|
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(
|
|
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(
|
|
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(
|
|
4082
|
-
console.log(
|
|
4083
|
-
console.log(
|
|
4084
|
-
console.log(
|
|
4085
|
-
console.log(
|
|
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(
|
|
4147
|
+
console.log("Starting diff viewer...");
|
|
4091
4148
|
serversRunning = 1;
|
|
4092
4149
|
await createDiffServer(gitDiff);
|
|
4093
|
-
console.log(
|
|
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(
|
|
4098
|
-
console.log(
|
|
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
|
}
|