diffx-cli 0.7.1 → 0.8.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/dist/cli.mjs +46 -25
- package/package.json +2 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,15 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
4
|
+
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
6
|
import getPort from "get-port";
|
|
7
|
-
import {
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
8
|
import { parseSync } from "editorconfig";
|
|
9
9
|
import { readFile } from "node:fs/promises";
|
|
10
10
|
import { Hono } from "hono";
|
|
11
11
|
import { serve } from "@hono/node-server";
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
|
+
//#region src/path.ts
|
|
14
|
+
function decodeAndNormalize(p) {
|
|
15
|
+
let decoded = p;
|
|
16
|
+
try {
|
|
17
|
+
decoded = decodeURIComponent(p);
|
|
18
|
+
} catch {}
|
|
19
|
+
return decoded.replace(/\\/g, "/");
|
|
20
|
+
}
|
|
21
|
+
function isSafePath(relativePath, baseDir) {
|
|
22
|
+
const normalized = decodeAndNormalize(relativePath);
|
|
23
|
+
if (normalized.includes("..") || normalized.includes("\0") || isAbsolute(normalized)) return false;
|
|
24
|
+
const resolved = resolve(baseDir, normalized);
|
|
25
|
+
return resolved.startsWith(resolve(baseDir) + "/") || resolved === resolve(baseDir);
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
13
28
|
//#region src/git.ts
|
|
14
29
|
function isBinaryFile(absolutePath) {
|
|
15
30
|
try {
|
|
@@ -23,57 +38,59 @@ function isBinaryFile(absolutePath) {
|
|
|
23
38
|
}
|
|
24
39
|
function getFileContent(filePath, version) {
|
|
25
40
|
const root = getRepoRoot();
|
|
41
|
+
if (!isSafePath(filePath, root)) return null;
|
|
42
|
+
const resolved = resolve(root, filePath);
|
|
26
43
|
if (version === "new") try {
|
|
27
|
-
return readFileSync(
|
|
44
|
+
return readFileSync(resolved);
|
|
28
45
|
} catch {
|
|
29
46
|
return null;
|
|
30
47
|
}
|
|
31
48
|
try {
|
|
32
|
-
return
|
|
49
|
+
return execFileSync("git", ["show", `HEAD:${filePath}`], { maxBuffer: 50 * 1024 * 1024 });
|
|
33
50
|
} catch {
|
|
34
51
|
return null;
|
|
35
52
|
}
|
|
36
53
|
}
|
|
37
54
|
function isGitRepo() {
|
|
38
55
|
try {
|
|
39
|
-
|
|
56
|
+
execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
|
|
40
57
|
return true;
|
|
41
58
|
} catch {
|
|
42
59
|
return false;
|
|
43
60
|
}
|
|
44
61
|
}
|
|
45
62
|
function getRepoRoot() {
|
|
46
|
-
return
|
|
63
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf-8" }).trim();
|
|
47
64
|
}
|
|
48
65
|
function getRepoName() {
|
|
49
66
|
return basename(getRepoRoot());
|
|
50
67
|
}
|
|
51
68
|
function getBranchName() {
|
|
52
69
|
try {
|
|
53
|
-
return
|
|
70
|
+
return execFileSync("git", [
|
|
71
|
+
"rev-parse",
|
|
72
|
+
"--abbrev-ref",
|
|
73
|
+
"HEAD"
|
|
74
|
+
], { encoding: "utf-8" }).trim();
|
|
54
75
|
} catch {
|
|
55
76
|
return "";
|
|
56
77
|
}
|
|
57
78
|
}
|
|
58
79
|
function getCustomGitDiff(args) {
|
|
59
|
-
return
|
|
60
|
-
"git",
|
|
61
|
-
"diff",
|
|
62
|
-
...args
|
|
63
|
-
].join(" "), {
|
|
80
|
+
return execFileSync("git", ["diff", ...args], {
|
|
64
81
|
encoding: "utf-8",
|
|
65
82
|
maxBuffer: 50 * 1024 * 1024
|
|
66
83
|
});
|
|
67
84
|
}
|
|
68
85
|
function getGitDiff(options = {}) {
|
|
69
86
|
const parts = [];
|
|
70
|
-
const unstaged =
|
|
87
|
+
const unstaged = execFileSync("git", ["diff"], {
|
|
71
88
|
encoding: "utf-8",
|
|
72
89
|
maxBuffer: 50 * 1024 * 1024
|
|
73
90
|
});
|
|
74
91
|
if (unstaged) parts.push(unstaged);
|
|
75
92
|
if (options.staged) {
|
|
76
|
-
const staged =
|
|
93
|
+
const staged = execFileSync("git", ["diff", "--staged"], {
|
|
77
94
|
encoding: "utf-8",
|
|
78
95
|
maxBuffer: 50 * 1024 * 1024
|
|
79
96
|
});
|
|
@@ -98,7 +115,11 @@ function getTabSizeForFiles(filePaths) {
|
|
|
98
115
|
}
|
|
99
116
|
function getUntrackedFilesDiff() {
|
|
100
117
|
const root = getRepoRoot();
|
|
101
|
-
const output =
|
|
118
|
+
const output = execFileSync("git", [
|
|
119
|
+
"ls-files",
|
|
120
|
+
"--others",
|
|
121
|
+
"--exclude-standard"
|
|
122
|
+
], {
|
|
102
123
|
encoding: "utf-8",
|
|
103
124
|
maxBuffer: 50 * 1024 * 1024
|
|
104
125
|
}).trim();
|
|
@@ -333,7 +354,9 @@ function createApp(clientDir, customDiffArgs, commentStore) {
|
|
|
333
354
|
app.get("/*", async (c) => {
|
|
334
355
|
let filePath = c.req.path;
|
|
335
356
|
if (filePath === "/") filePath = "/index.html";
|
|
336
|
-
const
|
|
357
|
+
const relativePath = filePath.slice(1);
|
|
358
|
+
if (!isSafePath(relativePath, clientDir)) return c.text("Forbidden", 403);
|
|
359
|
+
const fullPath = resolve(clientDir, relativePath);
|
|
337
360
|
try {
|
|
338
361
|
const content = await readFile(fullPath);
|
|
339
362
|
const contentType = MIME_TYPES[extname(fullPath)] || "application/octet-stream";
|
|
@@ -350,7 +373,8 @@ function startServer(options) {
|
|
|
350
373
|
return new Promise((resolve) => {
|
|
351
374
|
serve({
|
|
352
375
|
fetch: app.fetch,
|
|
353
|
-
port: options.port
|
|
376
|
+
port: options.port,
|
|
377
|
+
hostname: "127.0.0.1"
|
|
354
378
|
}, (info) => {
|
|
355
379
|
resolve({ port: info.port });
|
|
356
380
|
});
|
|
@@ -382,7 +406,7 @@ if (values.help) {
|
|
|
382
406
|
Usage: diffx [options] [-- <git diff args>]
|
|
383
407
|
|
|
384
408
|
Options:
|
|
385
|
-
-p, --port <port> Port to run the server on (default:
|
|
409
|
+
-p, --port <port> Port to run the server on (default: random available port)
|
|
386
410
|
--no-open Don't open the browser automatically
|
|
387
411
|
-v, --version Show version number
|
|
388
412
|
-h, --help Show this help message
|
|
@@ -405,7 +429,7 @@ if (!isGitRepo()) {
|
|
|
405
429
|
console.error("Error: not inside a git repository");
|
|
406
430
|
process.exit(1);
|
|
407
431
|
}
|
|
408
|
-
const port = await getPort(
|
|
432
|
+
const port = await getPort(values.port ? { port: parseInt(values.port, 10) } : void 0);
|
|
409
433
|
const clientDir = resolve(dirname(fileURLToPath(import.meta.url)), "client");
|
|
410
434
|
const { existsSync } = await import("node:fs");
|
|
411
435
|
const { port: actualPort } = await startServer({
|
|
@@ -413,12 +437,9 @@ const { port: actualPort } = await startServer({
|
|
|
413
437
|
clientDir: existsSync(clientDir) ? clientDir : resolve(process.cwd(), "dist/client"),
|
|
414
438
|
customDiffArgs
|
|
415
439
|
});
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const url = `http://localhost:${actualPort}`;
|
|
420
|
-
openModule.default(url);
|
|
421
|
-
}
|
|
440
|
+
const localUrl = `http://127.0.0.1:${actualPort}`;
|
|
441
|
+
console.log(`diffx server running at ${localUrl}`);
|
|
442
|
+
if (!values["no-open"]) (await import("open")).default(localUrl);
|
|
422
443
|
process.on("SIGINT", () => {
|
|
423
444
|
console.log("\nShutting down...");
|
|
424
445
|
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "diffx-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Local code review tool for git diffs with a GitHub PR-like web UI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"open": "^11.0.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
+
"@changesets/changelog-git": "^0.2.1",
|
|
36
37
|
"@changesets/cli": "^2.30.0",
|
|
37
38
|
"@pierre/diffs": "^1.1.11",
|
|
38
39
|
"@types/react": "^19.1.2",
|