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.
Files changed (2) hide show
  1. package/dist/cli.mjs +46 -25
  2. 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 { execSync } from "node:child_process";
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(join(root, filePath));
44
+ return readFileSync(resolved);
28
45
  } catch {
29
46
  return null;
30
47
  }
31
48
  try {
32
- return execSync(`git show HEAD:${filePath}`, { maxBuffer: 50 * 1024 * 1024 });
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
- execSync("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
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 execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
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 execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8" }).trim();
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 execSync([
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 = execSync("git diff", {
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 = execSync("git diff --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 = execSync("git ls-files --others --exclude-standard", {
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 fullPath = join(clientDir, filePath);
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: 3433)
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({ port: values.port ? parseInt(values.port, 10) : 3433 });
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
- console.log(`diffx server running at http://localhost:${actualPort}`);
417
- if (!values["no-open"]) {
418
- const openModule = await import("open");
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.7.1",
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",