difftab 0.1.2 → 0.2.0

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 CHANGED
@@ -1,6 +1,9 @@
1
1
  # difftab
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/difftab)](https://www.npmjs.com/package/difftab)
4
+ [![node](https://img.shields.io/node/v/difftab)](https://nodejs.org)
5
+ [![dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](#never-writes-to-your-repository)
6
+ [![license](https://img.shields.io/npm/l/difftab)](LICENSE)
4
7
 
5
8
  **See what your AI coding agent changed — in one tab.**
6
9
 
@@ -10,20 +13,16 @@ while the agent keeps writing. Close the tab and the process exits.
10
13
 
11
14
  [中文文档](README.zh-CN.md)
12
15
 
13
- ## Install
16
+ ## Quick start
14
17
 
15
18
  ```bash
16
- npx difftab # or install it once: npm i -g difftab
19
+ cd /path/to/your/repo
20
+ npx difftab
17
21
  ```
18
22
 
19
- Requires **Node.js 22 or newer** on macOS, Windows or Linux. Zero dependencies.
20
-
21
- ## Usage
23
+ Or install it once — `npm i -g difftab` — and just run `difftab`.
22
24
 
23
- ```bash
24
- cd /path/to/your/repo
25
- difftab
26
- ```
25
+ Requires **Node.js 22 or newer** on macOS, Windows or Linux. Zero dependencies.
27
26
 
28
27
  - `--no-open` — print the URL instead of opening a browser
29
28
  - `-v`, `--version` — print the version and exit
@@ -38,14 +37,21 @@ Running it again in the same repository reuses the running instance.
38
37
  - **Diffs, lazily** — click a file to load its patch, rendered by
39
38
  [diff2html](https://diff2html.xyz/) + highlight.js in a VS Code-like theme, side by side
40
39
  until the pane gets too narrow.
40
+ - **File tree** — a second sidebar tab browses the whole repository rather than only what
41
+ changed: tracked, untracked and ignored files (the ignored ones dimmed), loaded one
42
+ directory at a time. Click any file to read it, highlighted by the same theme.
41
43
  - **Branch status** — branch name, ahead/behind counts, "no upstream", detached HEAD, and
42
44
  an in-progress rebase / merge / cherry-pick / revert / bisect / `git am`.
43
45
  - **Auto-refresh** — a file watcher pushes changes over SSE, falling back to polling where
44
46
  recursive watching would exhaust the inotify quota (and saying so in the UI).
45
47
  - **Exits on its own** — 45 seconds after the last tab closes.
48
+ - **Cheap to run** — about 40 ms from launch to a listening server, well inside the 300 ms
49
+ budget CI enforces on every commit across nine Node × OS combinations; the JS bundle is
50
+ 71 KB gzipped.
46
51
 
47
52
  Empty repositories, interrupted rebases, linked worktrees, submodules, binary and >5 MB
48
53
  files, renames, and paths with spaces, quotes, CJK or emoji are handled explicitly.
54
+
49
55
  difftab is deliberately a viewer: no editing, history, blame or review workflow
50
56
  ([out-of-scope list](CONTRIBUTING.md#the-read-only-promise-is-not-negotiable)).
51
57
 
@@ -56,6 +62,19 @@ discard, pull, push, branch or stash. Two gates enforce it on every change: a `G
56
62
  allowlist over every git invocation, and a byte-for-byte comparison of `.git` before and
57
63
  after. And `dist/server/main.js` ships unminified, so you can audit it by hand.
58
64
 
65
+ ## Nothing leaves your machine
66
+
67
+ The server binds `127.0.0.1` on a port the kernel picks, and the only HTTP request difftab
68
+ makes is to localhost, to see whether an instance is already running for this repository.
69
+ No telemetry, no account, no cloud.
70
+
71
+ Each session gets a random token, handed to the browser once through the URL and then kept
72
+ in an `HttpOnly; SameSite=Strict` cookie while the URL is redirected clean. Every request
73
+ is checked against the `Host` header — the actual defense against DNS rebinding, rather
74
+ than the token alone — and against `Origin`, and the page runs under a `default-src 'none'`
75
+ CSP that also blocks framing, `<base>` rewriting and form submission. There is no
76
+ development escape hatch: no environment variable relaxes any of those checks.
77
+
59
78
  ## Development
60
79
 
61
80
  ```bash
package/README.zh-CN.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # difftab
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/difftab)](https://www.npmjs.com/package/difftab)
4
+ [![node](https://img.shields.io/node/v/difftab)](https://nodejs.org)
5
+ [![dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](#全程零写操作)
6
+ [![license](https://img.shields.io/npm/l/difftab)](LICENSE)
4
7
 
5
8
  **一个标签页,看懂 AI 编码 Agent 改了哪些代码。**
6
9
 
@@ -8,20 +11,16 @@
8
11
 
9
12
  [English](README.md)
10
13
 
11
- ## 安装
14
+ ## 快速开始
12
15
 
13
16
  ```bash
14
- npx difftab # 或者装一次:npm i -g difftab
17
+ cd /path/to/your/repo
18
+ npx difftab
15
19
  ```
16
20
 
17
- 要求 **Node.js 22 或更高**,macOS / Windows / Linux 三端均支持,零依赖。
18
-
19
- ## 使用
21
+ 也可以装一次——`npm i -g difftab`——之后直接敲 `difftab`。
20
22
 
21
- ```bash
22
- cd /path/to/your/repo
23
- difftab
24
- ```
23
+ 要求 **Node.js 22 或更高**,macOS / Windows / Linux 三端均支持,零依赖。
25
24
 
26
25
  - `--no-open`——只打印 URL,不拉起浏览器
27
26
  - `-v`, `--version`——打印版本号并退出
@@ -33,17 +32,27 @@ difftab
33
32
 
34
33
  - **变更列表**——已暂存、未暂存、未跟踪、冲突四组,未跟踪的目录展开到文件粒度。
35
34
  - **按文件懒加载的 diff**——点开某个文件才取它的补丁,渲染用 [diff2html](https://diff2html.xyz/) + highlight.js,配色仿 VS Code,面板窄到放不下两列时自动切成逐行。
35
+ - **文件树**——侧栏第二个 tab 浏览整个仓库,而不只是改动过的那些:已跟踪、未跟踪与被忽略的文件(后者灰显),按目录懒加载。点任一文件在右侧只读查看,高亮复用同一套配色。
36
36
  - **分支状态**——当前分支、ahead/behind 计数、无上游、游离 HEAD,以及进行中的 rebase /
37
37
  merge / cherry-pick / revert / bisect / `git am`。
38
38
  - **自动刷新**——文件监听经 SSE 推到前端;递归监听会耗尽 inotify 配额的场合改用轮询,并在界面上标注。
39
39
  - **自己退出**——最后一个标签页关掉 45 秒后自动退。
40
+ - **跑起来很轻**——从启动到端口就绪约 40ms,远在 CI 每次提交都验的 300ms 预算之内(九个 Node × 平台组合各跑一遍);打包后的 JS gzip 71KB。
41
+
42
+ 空仓库、停在中途的 rebase、linked worktree、submodule、二进制与超过 5MB 的文件、重命名,以及带空格、引号、中日韩文字或 emoji 的路径,都是被显式处理的。
40
43
 
41
- 空仓库、停在中途的 rebase、linked worktree、submodule、二进制与超过 5MB 的文件、重命名,以及带空格、引号、中日韩文字或 emoji 的路径,都是被显式处理的。difftab 刻意只是个查看器:不编辑、不看历史、不做 blame、不承载评审流程(完整的不做清单见 [CONTRIBUTING.md](CONTRIBUTING.md#the-read-only-promise-is-not-negotiable))。
44
+ difftab 刻意只是个查看器:不编辑、不看历史、不做 blame、不承载评审流程(完整的不做清单见 [CONTRIBUTING.md](CONTRIBUTING.md#the-read-only-promise-is-not-negotiable))。
42
45
 
43
46
  ## 全程零写操作
44
47
 
45
48
  不是「尽量」:difftab 只发只读的 git 命令——不 stage、不 commit、不 discard、不 pull / push、不建分支、不 stash。两道门禁在每次改动上守着:一道是覆盖每一次 git 调用的 `GIT_TRACE` 命令白名单,另一道是 `.git` 前后逐字节比对。后端也支持手工审计:`dist/server/main.js` 不压缩不混淆发布。
46
49
 
50
+ ## 全程不外传
51
+
52
+ 服务只绑 `127.0.0.1`,端口由内核随机分配,而 difftab 发出的唯一一个 HTTP 请求是打到 localhost 上的,用来确认这个仓库是不是已经有实例在跑。无遥测、无账号、无云端。
53
+
54
+ 每次会话生成一个随机 token,先经 URL 交给浏览器,随后落进 `HttpOnly; SameSite=Strict` 的 cookie,并把 URL 上的 query 重定向掉。每个请求都校验 `Host` 头与 `Origin`——`Host` 那道才是 DNS rebinding 的正面防御,光靠 token 挡不住;页面跑在 `default-src 'none'` 的 CSP 之下,被嵌 iframe、`<base>` 改写相对 URL 与表单外发也一并挡掉。后端没有任何开发用的后门:不存在放宽这几道校验的环境变量。
55
+
47
56
  ## 开发
48
57
 
49
58
  ```bash
package/bin/difftab.js CHANGED
@@ -1,51 +1,43 @@
1
1
  #!/usr/bin/env node
2
- // difftab CLI 入口 —— 版本守卫。
2
+ // difftab CLI 入口——版本守卫。
3
3
  //
4
- // 本文件手写维护,不参与 TypeScript 编译、不作为任何打包入口。
5
- // 一旦它进了构建管线,就可能被注入超出 Node 22 的语法或被合并进主模块,
6
- // 低于下限的用户拿到的将是解析期 SyntaxError —— 守卫在解析期即失效。
4
+ // 本文件手写维护,不参与 TypeScript 编译、不作为任何打包入口:一旦它进了构建管线,就可能被
5
+ // 注入超出 Node 22 的语法或被合并进主模块,低于下限的用户拿到的将是解析期 SyntaxError——
6
+ // 守卫在解析期即失效。因此这里只用「保守语法」:ES2015 之内,不使用可选链 / 空值合并 /
7
+ // 顶层 await / class 字段等更晚的语法。守卫通过后,才动态 import 主模块。
7
8
  //
8
- // 因此这里只用「保守语法」:ES2015 之内,不使用可选链 / 空值合并 /
9
- // 顶层 await / class 字段等更晚的语法。守卫通过后,才动态 import 主模块。
10
- //
11
- // `node:fs` 是**内建模块**,静态 import 它不违反上面那条 —— 守卫要防的是「解析期就
12
- // 炸掉」与「把主模块的新语法拖进来」,而内建模块两条都不沾,且在 Node 12 起就支持
13
- // `node:` 前缀。写成静态 import 而不是在分支里动态 import:那条路要么 `await`
14
- // (顶层 await 正是禁项),要么把报错塞进 `.then()` 回调里,而回调里的 `process.exit()`
15
- // 与本文件要修的问题是同一类。
9
+ // `node:fs` 是**内建模块**,静态 import 它不违反上面那条——守卫要防的是解析期就炸掉与把
10
+ // 主模块的新语法拖进来,而内建模块两条都不沾。写成静态 import 是因为动态那条要么 `await`
11
+ // (顶层 await 正是禁项)、要么把报错塞进 `.then()` 回调里。
16
12
 
17
13
  import { writeSync } from 'node:fs';
18
14
 
19
15
  /**
20
- * 退出前的报错**一律 `writeSync(2, …)`**(红线)。
21
- *
22
- * `process.stderr.write(…)` + `process.exit()` 在 Windows 上写**管道**时是异步的,
23
- * 整条消息会被丢掉 —— 症状是 stderr 全空、只剩一个退出码。而**文件重定向看不出
24
- * 区别**(那条是同步写),所以门禁那侧必须经管道取回 stderr,见 CI 的 old-node-guard。
16
+ * 退出前的报错**一律 `writeSync(2, …)`**。`process.stderr.write(…)` + `process.exit()` 在
17
+ * Windows 上写**管道**时是异步的,整条消息会被丢掉——症状是 stderr 全空、只剩一个退出码,
18
+ * 而**文件重定向看不出区别**(那条是同步写),所以门禁那侧必须经管道取回 stderr。
25
19
  *
26
- * try/catch 是因为读端可能先走(`difftab | head -1`):此刻要报的是别的事,
27
- * 不该被一个 EPIPE 顶掉。
20
+ * try/catch 是因为读端可能先走(`difftab | head -1`):此刻要报的是别的事,不该被 EPIPE 顶掉。
28
21
  */
29
22
  function writeStderr(message) {
30
23
  try {
31
24
  writeSync(2, message);
32
25
  } catch (_cause) {
33
- // 报不出来就算了 —— 退出码仍然是对的。
34
- // 绑定写成 `_cause` 而不是省略:可选 catch 绑定是 ES2019,本文件只用 ES2015
26
+ // 报不出来就算了——退出码仍然是对的。
27
+ // 绑定写成 `_cause` 而不是省略:可选 catch 绑定是 ES2019,本文件只用 ES2015
35
28
  }
36
29
  }
37
30
 
38
- // 下限只写一次。写成两个常量时,把 MIN_MAJOR 提到 24 却忘了改 MIN_VERSION,
39
- // 结果是守卫正确地拒掉 Node 22、却告诉用户「requires Node.js 22.0.0 or newer,
40
- // but this is v22.5.0」—— 没有任何门禁会因此变红。
41
- // 与 package.json 的 engines.node 对齐由冒烟测试断言。
31
+ // 下限只写一次。写成两个常量时,把 MIN_MAJOR 提到 24 却忘了改 MIN_VERSION,结果是守卫正确
32
+ // 地拒掉 Node 22、却告诉用户「requires Node.js 22.0.0 or newer, but this is v22.5.0」——
33
+ // 没有任何门禁会因此变红。与 package.json 的 engines.node 对齐由冒烟测试断言
42
34
  const MIN_MAJOR = 22;
43
35
  const MIN_VERSION = `${MIN_MAJOR}.0.0`;
44
36
 
45
37
  const raw = process.versions.node;
46
38
  const major = parseInt(raw.split('.')[0], 10);
47
39
 
48
- // 写成 !(major >= MIN_MAJOR) 而非 major < MIN_MAJOR:解析失败得到 NaN 时同样报错。
40
+ // 写成 !(major >= MIN_MAJOR) 而非 major < MIN_MAJOR:解析失败得到 NaN 时同样报错。
49
41
  if (!(major >= MIN_MAJOR)) {
50
42
  writeStderr(
51
43
  'difftab: requires Node.js ' +