hunkdiff 0.1.0 → 0.3.0-beta.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 CHANGED
@@ -1,58 +1,103 @@
1
1
  # hunk
2
2
 
3
- Hunk is a desktop-inspired terminal diff viewer for understanding AI-authored changesets in Bun + TypeScript with OpenTUI.
3
+ Hunk is a terminal diff viewer for reviewing agent-authored changesets with a desktop-style UI.
4
+
5
+ - full-screen multi-file review stream
6
+ - split, stacked, and responsive auto layouts
7
+ - keyboard and mouse navigation
8
+ - optional agent rationale beside annotated hunks
9
+ - Git pager and difftool integration
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm i -g hunkdiff
15
+ ```
4
16
 
5
17
  ## Requirements
6
18
 
7
- - Bun
8
- - Zig
19
+ - Node.js 18+
20
+ - Git for `hunk diff`, `hunk show`, `hunk stash show`, and pager integration
9
21
 
10
- ## Install
22
+ ## Quick start
23
+
24
+ Review your current working tree:
11
25
 
12
26
  ```bash
13
- bun install
27
+ hunk diff
14
28
  ```
15
29
 
16
- ## Run
30
+ Review staged changes:
17
31
 
18
32
  ```bash
19
- bun run src/main.tsx -- diff
33
+ hunk diff --staged
20
34
  ```
21
35
 
22
- ## Standalone binary
36
+ Review a commit:
37
+
38
+ ```bash
39
+ hunk show HEAD~1
40
+ ```
23
41
 
24
- Build a local executable:
42
+ Compare two files directly:
25
43
 
26
44
  ```bash
27
- bun run build:bin
28
- ./dist/hunk diff
45
+ hunk diff before.ts after.ts
29
46
  ```
30
47
 
31
- Install it into `~/.local/bin`:
48
+ Open a patch from stdin:
32
49
 
33
50
  ```bash
34
- bun run install:bin
35
- hunk
36
- hunk diff
51
+ git diff --no-color | hunk patch -
37
52
  ```
38
53
 
39
- If you want a different install location, set `HUNK_INSTALL_DIR` before running the install script.
54
+ ## Feature comparison
55
+
56
+ | Capability | hunk | difftastic | delta | diff |
57
+ | --- | --- | --- | --- | --- |
58
+ | Dedicated interactive review UI | ✅ | ❌ | ❌ | ❌ |
59
+ | Multi-file review stream with navigation sidebar | ✅ | ❌ | ❌ | ❌ |
60
+ | Agent / AI rationale sidecar | ✅ | ❌ | ❌ | ❌ |
61
+ | Split diffs | ✅ | ✅ | ✅ | ✅ |
62
+ | Stacked diffs | ✅ | ✅ | ✅ | ✅ |
63
+ | Auto responsive layouts | ✅ | ❌ | ❌ | ❌ |
64
+ | Themes | ✅ | ❌ | ✅ | ❌ |
65
+ | Syntax highlighting | ✅ | ✅ | ✅ | ❌ |
66
+ | Syntax-aware / structural diffing | ❌ | ✅ | ❌ | ❌ |
67
+ | Mouse support inside the diff viewer | ✅ | ❌ | ❌ | ❌ |
68
+ | Runtime toggles for wrapping / line numbers / hunk metadata | ✅ | ❌ | ❌ | ❌ |
69
+ | Pager-compatible mode | ✅ | ✅ | ✅ | ✅ |
70
+
71
+ ## Benchmarks
72
+
73
+ Quick local timing snapshot from one Linux machine on the same 120-line TypeScript file pair. Metric: time until a changed marker first became visible.
74
+
75
+ | Tool | Avg first-visible changed output |
76
+ | --- | ---: |
77
+ | `diff` | ~37 ms |
78
+ | `delta --paging=never` | ~35 ms |
79
+ | `hunk diff` | ~219 ms |
80
+ | `difft --display side-by-side` | ~266 ms |
81
+
82
+ Takeaway:
40
83
 
41
- ## Workflows
84
+ - `diff` and `delta` are fastest here because they print plain diff text and exit.
85
+ - `hunk` spends more startup time on an interactive UI, syntax highlighting, navigation state, and optional agent context.
86
+ - `difftastic` spends more startup time on structural diffing.
42
87
 
43
- - `hunk` — print standard CLI help with the most common commands
44
- - `hunk diff` — review local working tree changes in the full Hunk UI
45
- - `hunk diff --staged` / `hunk diff --cached` review staged changes in the full Hunk UI
88
+ ## Common workflows
89
+
90
+ - `hunk` — print CLI help
91
+ - `hunk diff` — review working tree changes
92
+ - `hunk diff --staged` / `hunk diff --cached` — review staged changes
46
93
  - `hunk diff <ref>` — review changes versus a branch, tag, or commit-ish
47
- - `hunk diff <ref1>..<ref2>` / `hunk diff <ref1>...<ref2>` — review common Git ranges
48
- - `hunk diff -- <pathspec...>` — review only selected paths
49
- - `hunk show [ref]` — review the last commit or a given ref in the full Hunk UI
50
- - `hunk stash show [ref]` — review a stash entry in the full Hunk UI
51
- - `hunk diff <left> <right>` compare two concrete files directly
52
- - `hunk patch [file|-]` — review a patch file or stdin, including pager mode
53
- - `hunk pager` — act as a general Git pager wrapper, opening Hunk for diff-like stdin and falling back to normal text paging otherwise
94
+ - `hunk diff <ref1>..<ref2>` / `hunk diff <ref1>...<ref2>` — review Git ranges
95
+ - `hunk diff -- <pathspec...>` — limit review to selected paths
96
+ - `hunk show [ref]` — review the last commit or a specific ref
97
+ - `hunk stash show [ref]` — review a stash entry
98
+ - `hunk patch [file|-]`review a patch file or stdin
99
+ - `hunk pager` — act as a Git pager wrapper, opening Hunk for diff-like stdin and falling back to plain text paging otherwise
54
100
  - `hunk difftool <left> <right> [path]` — integrate with Git difftool
55
- - `hunk git [range]` — legacy alias for the original Git-style diff entrypoint
56
101
 
57
102
  ## Interaction
58
103
 
@@ -70,6 +115,38 @@ If you want a different install location, set `HUNK_INSTALL_DIR` before running
70
115
  - `tab` cycle focus regions
71
116
  - `q` or `Esc` quit
72
117
 
118
+ ## Git integration
119
+
120
+ Use Hunk directly for full-screen review:
121
+
122
+ ```bash
123
+ hunk diff
124
+ hunk diff --staged
125
+ hunk diff main...feature
126
+ hunk show
127
+ hunk stash show
128
+ ```
129
+
130
+ Use Hunk as a pager for `git diff` and `git show`:
131
+
132
+ ```bash
133
+ git config --global core.pager 'hunk patch -'
134
+ ```
135
+
136
+ Or scope it just to diff/show:
137
+
138
+ ```bash
139
+ git config --global pager.diff 'hunk patch -'
140
+ git config --global pager.show 'hunk patch -'
141
+ ```
142
+
143
+ Use Hunk as a Git difftool:
144
+
145
+ ```bash
146
+ git config --global diff.tool hunk
147
+ git config --global difftool.hunk.cmd 'hunk difftool "$LOCAL" "$REMOTE" "$MERGED"'
148
+ ```
149
+
73
150
  ## Configuration
74
151
 
75
152
  Hunk reads layered TOML config with this precedence:
@@ -77,12 +154,10 @@ Hunk reads layered TOML config with this precedence:
77
154
  1. built-in defaults
78
155
  2. global config: `$XDG_CONFIG_HOME/hunk/config.toml` or `~/.config/hunk/config.toml`
79
156
  3. repo-local config: `.hunk/config.toml`
80
- 4. command-specific sections like `[git]`, `[diff]`, `[show]`, `[stash-show]`, `[patch]`, `[difftool]`
157
+ 4. command-specific sections like `[diff]`, `[show]`, `[stash-show]`, `[patch]`, `[difftool]`
81
158
  5. `[pager]` when Hunk is running in pager mode
82
159
  6. explicit CLI flags
83
160
 
84
- When you change persistent view settings inside Hunk, it writes them back to `.hunk/config.toml` in the current repo when possible, or to the global config file outside a repo.
85
-
86
161
  Example:
87
162
 
88
163
  ```toml
@@ -101,16 +176,7 @@ line_numbers = false
101
176
  mode = "split"
102
177
  ```
103
178
 
104
- CLI overrides are available when you want one-off or pager-specific behavior:
105
-
106
- ```bash
107
- hunk diff --mode split --line-numbers
108
- hunk show HEAD~1 --theme paper
109
- hunk patch - --mode stack --no-line-numbers
110
- hunk diff before.ts after.ts --theme paper --wrap
111
- ```
112
-
113
- Supported persistent CLI overrides:
179
+ Supported one-off CLI overrides:
114
180
 
115
181
  - `--mode <auto|split|stack>`
116
182
  - `--theme <theme>`
@@ -119,11 +185,11 @@ Supported persistent CLI overrides:
119
185
  - `--hunk-headers` / `--no-hunk-headers`
120
186
  - `--agent-notes` / `--no-agent-notes`
121
187
 
122
- ## Agent sidecar format
188
+ ## Agent context sidecar
123
189
 
124
190
  Use `--agent-context <file>` to load a JSON sidecar and show agent rationale next to the diff.
125
191
 
126
- The order of `files` in the sidecar is significant. Hunk uses that order for the sidebar and main review stream so an agent can tell a story instead of relying on raw patch order.
192
+ The order of `files` in the sidecar is significant. Hunk uses that order for the sidebar and the main review stream so an agent can present a review narrative instead of raw patch order.
127
193
 
128
194
  ```json
129
195
  {
@@ -142,130 +208,59 @@ The order of `files` in the sidecar is significant. Hunk uses that order for the
142
208
  "confidence": "high"
143
209
  }
144
210
  ]
145
- },
146
- {
147
- "path": "src/ui/App.tsx",
148
- "summary": "Presents the new workflow after the loader changes.",
149
- "annotations": [
150
- {
151
- "newRange": [90, 136],
152
- "summary": "Uses the normalized model in the review shell.",
153
- "rationale": "The reader should inspect this after understanding the loader changes.",
154
- "tags": ["ui"],
155
- "confidence": "medium"
156
- }
157
- ]
158
211
  }
159
212
  ]
160
213
  }
161
214
  ```
162
215
 
163
- Files omitted from the sidecar keep their original diff order and appear after the explicitly ordered files.
164
-
165
- ## Codex workflow
166
-
167
- For Codex-driven changes, keep a transient sidecar at `.hunk/latest.json` and load it during review:
216
+ For local agent-driven review, keep a transient sidecar at `.hunk/latest.json` and load it with:
168
217
 
169
218
  ```bash
170
219
  hunk diff --agent-context .hunk/latest.json
171
220
  ```
172
221
 
173
- Suggested pattern:
174
-
175
- - Codex makes code changes.
176
- - Codex refreshes `.hunk/latest.json` with a concise changeset summary, file summaries, and hunk-level rationale.
177
- - You open `hunk diff`, `hunk diff --staged`, or `hunk show <ref>` with that sidecar.
178
-
179
- Keep the sidecar concise. It should explain why a hunk exists, what risk to review, and how the files fit together. It should not narrate obvious syntax edits line by line.
180
-
181
- ## Comparison
182
-
183
- ### Feature comparison
184
-
185
- | Capability | hunk | difftastic | delta | diff |
186
- | --- | --- | --- | --- | --- |
187
- | Dedicated interactive review UI | ✅ | ❌ | ❌ | ❌ |
188
- | Multi-file review stream with navigation sidebar | ✅ | ❌ | ❌ | ❌ |
189
- | Agent / AI rationale sidecar | ✅ | ❌ | ❌ | ❌ |
190
- | Split diffs | ✅ | ✅ | ✅ | ✅ |
191
- | Stacked diffs | ✅ | ✅ | ✅ | ✅ |
192
- | Auto responsive layouts | ✅ | ❌ | ❌ | ❌ |
193
- | Themes | ✅ | ❌ | ✅ | ❌ |
194
- | Syntax highlighting | ✅ | ✅ | ✅ | ❌ |
195
- | Syntax-aware / structural diffing | ❌ | ✅ | ❌ | ❌ |
196
- | Mouse support inside the diff viewer | ✅ | ❌ | ❌ | ❌ |
197
- | Runtime toggles for wrapping / line numbers / hunk metadata | ✅ | ❌ | ❌ | ❌ |
198
- | Pager-compatible mode | ✅ | ✅ | ✅ | ✅ |
199
-
200
- ### Local timing snapshot
222
+ ## Development
201
223
 
202
- These numbers are **not a universal benchmark**. They are a quick local comparison from one Linux machine using tmux panes, measuring **time until a changed marker first became visible** on the same 120-line TypeScript file pair.
203
-
204
- Commands used:
205
-
206
- - `hunk diff before.ts after.ts`
207
- - `difft --display side-by-side before.ts after.ts`
208
- - `delta --paging=never before.ts after.ts`
209
- - `diff -u before.ts after.ts`
210
-
211
- | Tool | Avg first-visible changed output |
212
- | --- | ---: |
213
- | `diff` | ~37 ms |
214
- | `delta --paging=never` | ~35 ms |
215
- | `hunk diff` | ~219 ms |
216
- | `difft --display side-by-side` | ~266 ms |
217
-
218
- Interpretation:
219
-
220
- - `diff` and `delta` are fastest here because they emit plain diff text and exit.
221
- - `hunk` pays extra startup cost for an interactive terminal UI, syntax highlighting, navigation state, and optional agent context.
222
- - `difftastic` pays extra cost for syntax-aware / structural diffing.
223
- - For larger review sessions, Hunk is optimized for **navigating and understanding** a changeset, not just dumping the quickest possible patch text.
224
-
225
- ## Git integration
226
-
227
- For full-screen review, you can invoke Hunk directly with Git-shaped commands:
224
+ Install dependencies:
228
225
 
229
226
  ```bash
230
- hunk diff
231
- hunk diff --staged
232
- hunk diff main...feature
233
- hunk show
234
- hunk show HEAD~1
235
- hunk stash show
227
+ bun install
236
228
  ```
237
229
 
238
- Use Hunk as the default Git pager when you want it to behave like a normal pager under `git diff` / `git show`:
230
+ Validate a change:
239
231
 
240
232
  ```bash
241
- git config --global core.pager 'hunk patch -'
233
+ bun run typecheck
234
+ bun test
235
+ bun run test:tty-smoke
242
236
  ```
243
237
 
244
- Or scope it just to `git diff` and `git show`:
238
+ Build the npm runtime bundle used for publishing:
245
239
 
246
240
  ```bash
247
- git config --global pager.diff 'hunk patch -'
248
- git config --global pager.show 'hunk patch -'
241
+ bun run build:npm
242
+ bun run check:pack
249
243
  ```
250
244
 
251
- When Hunk reads a patch from stdin, it automatically switches to pager-style chrome, strips Git's color escape sequences before parsing, and binds keyboard input to the controlling terminal so it works correctly as a Git pager.
252
-
253
- Then:
245
+ Stage the prebuilt npm packages for the current host and smoke test the install path without Bun on `PATH`:
254
246
 
255
247
  ```bash
256
- git diff
257
- git show HEAD
248
+ bun run build:prebuilt:npm
249
+ bun run check:prebuilt-pack
250
+ bun run smoke:prebuilt-install
258
251
  ```
259
252
 
260
- If you want Git to launch Hunk as a difftool for file-to-file comparisons:
253
+ Prepare the multi-platform release directories from downloaded build artifacts and dry-run the publish order:
261
254
 
262
255
  ```bash
263
- git config --global diff.tool hunk
264
- git config --global difftool.hunk.cmd 'hunk difftool "$LOCAL" "$REMOTE" "$MERGED"'
256
+ bun run build:prebuilt:artifact
257
+ bun run stage:prebuilt:release
258
+ bun run check:prebuilt-pack
259
+ bun run publish:prebuilt:npm -- --dry-run
265
260
  ```
266
- e comparisons:
267
261
 
268
- ```bash
269
- git config --global diff.tool hunk
270
- git config --global difftool.hunk.cmd 'hunk difftool "$LOCAL" "$REMOTE" "$MERGED"'
271
- ```
262
+ The automated tag/manual release workflow lives in `.github/workflows/release-prebuilt-npm.yml`.
263
+
264
+ ## License
265
+
266
+ [MIT](LICENSE)
package/bin/hunk.cjs ADDED
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+
3
+ const childProcess = require("node:child_process");
4
+ const fs = require("node:fs");
5
+ const os = require("node:os");
6
+ const path = require("node:path");
7
+
8
+ function run(target, args) {
9
+ const result = childProcess.spawnSync(target, args, {
10
+ stdio: "inherit",
11
+ env: process.env,
12
+ });
13
+
14
+ if (result.error) {
15
+ console.error(result.error.message);
16
+ process.exit(1);
17
+ }
18
+
19
+ process.exit(typeof result.status === "number" ? result.status : 1);
20
+ }
21
+
22
+ function hostCandidates() {
23
+ const platformMap = {
24
+ darwin: "darwin",
25
+ linux: "linux",
26
+ win32: "windows",
27
+ };
28
+ const archMap = {
29
+ x64: "x64",
30
+ arm64: "arm64",
31
+ };
32
+
33
+ const platform = platformMap[os.platform()] || os.platform();
34
+ const arch = archMap[os.arch()] || os.arch();
35
+ const binary = platform === "windows" ? "hunk.exe" : "hunk";
36
+
37
+ if (platform === "darwin") {
38
+ if (arch === "arm64") return [{ packageName: "hunkdiff-darwin-arm64", binary }];
39
+ if (arch === "x64") return [{ packageName: "hunkdiff-darwin-x64", binary }];
40
+ }
41
+
42
+ if (platform === "linux") {
43
+ if (arch === "arm64") return [{ packageName: "hunkdiff-linux-arm64", binary }];
44
+ if (arch === "x64") return [{ packageName: "hunkdiff-linux-x64", binary }];
45
+ }
46
+
47
+ return [];
48
+ }
49
+
50
+ function findInstalledBinary(startDir) {
51
+ let current = startDir;
52
+
53
+ for (;;) {
54
+ const modulesDir = path.join(current, "node_modules");
55
+ if (fs.existsSync(modulesDir)) {
56
+ for (const candidate of hostCandidates()) {
57
+ const resolved = path.join(modulesDir, candidate.packageName, "bin", candidate.binary);
58
+ if (fs.existsSync(resolved)) {
59
+ return resolved;
60
+ }
61
+ }
62
+ }
63
+
64
+ const parent = path.dirname(current);
65
+ if (parent === current) {
66
+ return null;
67
+ }
68
+ current = parent;
69
+ }
70
+ }
71
+
72
+ function bundledBunRuntime() {
73
+ try {
74
+ return require.resolve("bun/bin/bun.exe");
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ const overrideBinary = process.env.HUNK_BIN_PATH;
81
+ if (overrideBinary) {
82
+ run(overrideBinary, process.argv.slice(2));
83
+ }
84
+
85
+ const scriptDir = path.dirname(fs.realpathSync(__filename));
86
+ const prebuiltBinary = findInstalledBinary(scriptDir);
87
+ if (prebuiltBinary) {
88
+ run(prebuiltBinary, process.argv.slice(2));
89
+ }
90
+
91
+ const bunBinary = bundledBunRuntime();
92
+ if (bunBinary) {
93
+ const entrypoint = path.join(__dirname, "..", "dist", "npm", "main.js");
94
+ run(bunBinary, [entrypoint, ...process.argv.slice(2)]);
95
+ }
96
+
97
+ const printablePackages = hostCandidates().map((candidate) => `"${candidate.packageName}"`).join(" or ");
98
+ console.error(
99
+ printablePackages.length > 0
100
+ ? `Failed to locate a matching prebuilt Hunk binary. Try reinstalling hunkdiff or manually installing ${printablePackages}.`
101
+ : `Unsupported platform for prebuilt Hunk binaries: ${os.platform()} ${os.arch()}`,
102
+ );
103
+ process.exit(1);
package/package.json CHANGED
@@ -1,34 +1,15 @@
1
1
  {
2
2
  "name": "hunkdiff",
3
- "version": "0.1.0",
3
+ "version": "0.3.0-beta.1",
4
4
  "description": "Desktop-inspired terminal diff viewer for understanding agent-authored changesets.",
5
- "type": "module",
6
- "packageManager": "bun@1.3.10",
7
5
  "bin": {
8
- "hunk": "dist/npm/main.js"
6
+ "hunk": "./bin/hunk.cjs"
9
7
  },
10
8
  "files": [
11
- "dist/npm",
9
+ "bin",
12
10
  "README.md",
13
- "LICENSE",
14
- "CONTRIBUTING.md",
15
- "SECURITY.md"
11
+ "LICENSE"
16
12
  ],
17
- "scripts": {
18
- "start": "bun run src/main.tsx",
19
- "dev": "bun --watch src/main.tsx",
20
- "build:npm": "bash ./scripts/build-npm.sh",
21
- "build:bin": "bash ./scripts/build-bin.sh",
22
- "install:bin": "bash ./scripts/install-bin.sh",
23
- "typecheck": "tsc --noEmit",
24
- "test": "bun test",
25
- "test:tty-smoke": "bun test test/tty-render-smoke.test.ts",
26
- "check:pack": "bun run ./scripts/check-pack.ts",
27
- "prepack": "bun run build:npm",
28
- "bench:bootstrap-load": "bun run test/bootstrap-load-benchmark.ts",
29
- "bench:highlight-prefetch": "bun run test/adjacent-highlight-prefetch-benchmark.ts",
30
- "bench:large-stream": "bun run test/large-stream-windowing-benchmark.ts"
31
- },
32
13
  "keywords": [
33
14
  "diff",
34
15
  "git",
@@ -46,24 +27,15 @@
46
27
  "url": "https://github.com/modem-dev/hunk/issues"
47
28
  },
48
29
  "engines": {
49
- "bun": ">=1.3.10"
30
+ "node": ">=18"
31
+ },
32
+ "optionalDependencies": {
33
+ "hunkdiff-darwin-arm64": "0.3.0-beta.1",
34
+ "hunkdiff-darwin-x64": "0.3.0-beta.1",
35
+ "hunkdiff-linux-x64": "0.3.0-beta.1"
50
36
  },
37
+ "license": "MIT",
51
38
  "publishConfig": {
52
39
  "access": "public"
53
- },
54
- "devDependencies": {
55
- "@types/bun": "latest",
56
- "@types/react": "^19.2.14",
57
- "typescript": "^5.9.3"
58
- },
59
- "dependencies": {
60
- "@opentui/core": "^0.1.88",
61
- "@opentui/react": "^0.1.88",
62
- "@pierre/diffs": "^1.1.0",
63
- "commander": "^14.0.3",
64
- "diff": "^8.0.3",
65
- "parse-diff": "^0.11.1",
66
- "react": "^19.2.4"
67
- },
68
- "license": "MIT"
40
+ }
69
41
  }