diffity 0.1.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.
Files changed (174) hide show
  1. package/.claude/settings.local.json +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +71 -0
  4. package/development.md +156 -0
  5. package/package.json +32 -0
  6. package/packages/cli/build.js +38 -0
  7. package/packages/cli/package.json +51 -0
  8. package/packages/cli/src/agent.ts +187 -0
  9. package/packages/cli/src/db.ts +58 -0
  10. package/packages/cli/src/index.ts +196 -0
  11. package/packages/cli/src/review-routes.ts +150 -0
  12. package/packages/cli/src/server.ts +370 -0
  13. package/packages/cli/src/session.ts +48 -0
  14. package/packages/cli/src/threads.ts +238 -0
  15. package/packages/cli/tsconfig.json +13 -0
  16. package/packages/git/package.json +24 -0
  17. package/packages/git/src/commits.ts +28 -0
  18. package/packages/git/src/diff.ts +97 -0
  19. package/packages/git/src/exec.ts +35 -0
  20. package/packages/git/src/index.ts +5 -0
  21. package/packages/git/src/repo.ts +63 -0
  22. package/packages/git/src/status.ts +9 -0
  23. package/packages/git/src/types.ts +12 -0
  24. package/packages/git/tsconfig.json +9 -0
  25. package/packages/parser/package.json +26 -0
  26. package/packages/parser/src/index.ts +12 -0
  27. package/packages/parser/src/parse.ts +299 -0
  28. package/packages/parser/src/types.ts +52 -0
  29. package/packages/parser/src/word-diff.ts +155 -0
  30. package/packages/parser/tests/fixtures/binary-deleted.diff +4 -0
  31. package/packages/parser/tests/fixtures/binary-file.diff +4 -0
  32. package/packages/parser/tests/fixtures/binary-modified.diff +3 -0
  33. package/packages/parser/tests/fixtures/copied-file.diff +12 -0
  34. package/packages/parser/tests/fixtures/deleted-file.diff +9 -0
  35. package/packages/parser/tests/fixtures/empty.diff +0 -0
  36. package/packages/parser/tests/fixtures/hunk-with-context.diff +12 -0
  37. package/packages/parser/tests/fixtures/mode-change-with-content.diff +10 -0
  38. package/packages/parser/tests/fixtures/mode-change.diff +3 -0
  39. package/packages/parser/tests/fixtures/multi-file.diff +22 -0
  40. package/packages/parser/tests/fixtures/new-file.diff +9 -0
  41. package/packages/parser/tests/fixtures/no-newline.diff +10 -0
  42. package/packages/parser/tests/fixtures/renamed-file.diff +12 -0
  43. package/packages/parser/tests/fixtures/single-file-additions.diff +11 -0
  44. package/packages/parser/tests/fixtures/single-file-deletions.diff +11 -0
  45. package/packages/parser/tests/fixtures/single-file-mixed.diff +15 -0
  46. package/packages/parser/tests/fixtures/single-file-multi-hunk.diff +22 -0
  47. package/packages/parser/tests/fixtures/spaces-in-path.diff +9 -0
  48. package/packages/parser/tests/fixtures/submodule.diff +7 -0
  49. package/packages/parser/tests/fixtures/unicode-content.diff +11 -0
  50. package/packages/parser/tests/parse.test.ts +312 -0
  51. package/packages/parser/tests/word-diff-integration.test.ts +52 -0
  52. package/packages/parser/tests/word-diff.test.ts +121 -0
  53. package/packages/parser/tsconfig.json +10 -0
  54. package/packages/skills/diffity-resolve/SKILL.md +55 -0
  55. package/packages/skills/diffity-review/SKILL.md +74 -0
  56. package/packages/skills/diffity-start/SKILL.md +25 -0
  57. package/packages/ui/index.html +13 -0
  58. package/packages/ui/package.json +35 -0
  59. package/packages/ui/public/brand.svg +12 -0
  60. package/packages/ui/public/favicon.svg +15 -0
  61. package/packages/ui/src/app.tsx +14 -0
  62. package/packages/ui/src/components/comment-bubble.tsx +78 -0
  63. package/packages/ui/src/components/comment-form-row.tsx +58 -0
  64. package/packages/ui/src/components/comment-form.tsx +78 -0
  65. package/packages/ui/src/components/comment-line-number.tsx +60 -0
  66. package/packages/ui/src/components/comment-thread.tsx +209 -0
  67. package/packages/ui/src/components/commit-list.tsx +100 -0
  68. package/packages/ui/src/components/dashboard.tsx +84 -0
  69. package/packages/ui/src/components/diff-line.tsx +90 -0
  70. package/packages/ui/src/components/diff-page.tsx +332 -0
  71. package/packages/ui/src/components/diff-stats.tsx +20 -0
  72. package/packages/ui/src/components/diff-view.tsx +278 -0
  73. package/packages/ui/src/components/expand-row.tsx +45 -0
  74. package/packages/ui/src/components/file-block.tsx +536 -0
  75. package/packages/ui/src/components/file-tree-item.tsx +84 -0
  76. package/packages/ui/src/components/file-tree.tsx +72 -0
  77. package/packages/ui/src/components/general-comments.tsx +174 -0
  78. package/packages/ui/src/components/hunk-block-split.tsx +357 -0
  79. package/packages/ui/src/components/hunk-block.tsx +161 -0
  80. package/packages/ui/src/components/hunk-header.tsx +144 -0
  81. package/packages/ui/src/components/hunk-with-gap.tsx +113 -0
  82. package/packages/ui/src/components/icons/arrow-down-icon.tsx +7 -0
  83. package/packages/ui/src/components/icons/arrow-up-icon.tsx +7 -0
  84. package/packages/ui/src/components/icons/check-circle-icon.tsx +8 -0
  85. package/packages/ui/src/components/icons/check-icon.tsx +9 -0
  86. package/packages/ui/src/components/icons/chevron-down-icon.tsx +11 -0
  87. package/packages/ui/src/components/icons/chevron-icon.tsx +20 -0
  88. package/packages/ui/src/components/icons/chevron-up-down-icon.tsx +7 -0
  89. package/packages/ui/src/components/icons/chevron-up-icon.tsx +11 -0
  90. package/packages/ui/src/components/icons/comment-icon.tsx +9 -0
  91. package/packages/ui/src/components/icons/copy-icon.tsx +10 -0
  92. package/packages/ui/src/components/icons/eye-icon.tsx +10 -0
  93. package/packages/ui/src/components/icons/eye-off-icon.tsx +12 -0
  94. package/packages/ui/src/components/icons/file-icon.tsx +7 -0
  95. package/packages/ui/src/components/icons/folder-icon.tsx +19 -0
  96. package/packages/ui/src/components/icons/git-branch-icon.tsx +13 -0
  97. package/packages/ui/src/components/icons/keyboard-icon.tsx +13 -0
  98. package/packages/ui/src/components/icons/moon-icon.tsx +9 -0
  99. package/packages/ui/src/components/icons/plus-icon.tsx +9 -0
  100. package/packages/ui/src/components/icons/search-icon.tsx +10 -0
  101. package/packages/ui/src/components/icons/sidebar-icon.tsx +10 -0
  102. package/packages/ui/src/components/icons/spinner.tsx +7 -0
  103. package/packages/ui/src/components/icons/split-view-icon.tsx +10 -0
  104. package/packages/ui/src/components/icons/sun-icon.tsx +17 -0
  105. package/packages/ui/src/components/icons/trash-icon.tsx +11 -0
  106. package/packages/ui/src/components/icons/undo-icon.tsx +9 -0
  107. package/packages/ui/src/components/icons/unified-view-icon.tsx +12 -0
  108. package/packages/ui/src/components/icons/x-icon.tsx +10 -0
  109. package/packages/ui/src/components/line-number-cell.tsx +18 -0
  110. package/packages/ui/src/components/markdown-content.tsx +139 -0
  111. package/packages/ui/src/components/orphaned-threads.tsx +80 -0
  112. package/packages/ui/src/components/overview-file-list.tsx +57 -0
  113. package/packages/ui/src/components/render-expansion-rows.tsx +47 -0
  114. package/packages/ui/src/components/shortcut-modal.tsx +93 -0
  115. package/packages/ui/src/components/sidebar.tsx +80 -0
  116. package/packages/ui/src/components/skeleton.tsx +9 -0
  117. package/packages/ui/src/components/stale-diff-banner.tsx +21 -0
  118. package/packages/ui/src/components/summary-bar.tsx +39 -0
  119. package/packages/ui/src/components/toolbar.tsx +246 -0
  120. package/packages/ui/src/components/ui/badge.tsx +17 -0
  121. package/packages/ui/src/components/ui/confirm-dialog.tsx +52 -0
  122. package/packages/ui/src/components/ui/icon-button.tsx +23 -0
  123. package/packages/ui/src/components/ui/status-badge.tsx +57 -0
  124. package/packages/ui/src/components/ui/thread-badge.tsx +35 -0
  125. package/packages/ui/src/components/word-diff.tsx +126 -0
  126. package/packages/ui/src/hooks/use-comment-actions.ts +97 -0
  127. package/packages/ui/src/hooks/use-commits.ts +12 -0
  128. package/packages/ui/src/hooks/use-copy.ts +18 -0
  129. package/packages/ui/src/hooks/use-diff-staleness.ts +58 -0
  130. package/packages/ui/src/hooks/use-diff.ts +12 -0
  131. package/packages/ui/src/hooks/use-highlighter.ts +190 -0
  132. package/packages/ui/src/hooks/use-info.ts +12 -0
  133. package/packages/ui/src/hooks/use-keyboard.ts +55 -0
  134. package/packages/ui/src/hooks/use-line-selection.ts +157 -0
  135. package/packages/ui/src/hooks/use-overview.ts +12 -0
  136. package/packages/ui/src/hooks/use-review-threads.ts +12 -0
  137. package/packages/ui/src/hooks/use-search-params.ts +26 -0
  138. package/packages/ui/src/hooks/use-theme.ts +34 -0
  139. package/packages/ui/src/hooks/use-thread-navigation.ts +43 -0
  140. package/packages/ui/src/lib/api.ts +232 -0
  141. package/packages/ui/src/lib/cn.ts +6 -0
  142. package/packages/ui/src/lib/context-expansion.ts +122 -0
  143. package/packages/ui/src/lib/diff-utils.ts +268 -0
  144. package/packages/ui/src/lib/dom-utils.ts +13 -0
  145. package/packages/ui/src/lib/file-tree.ts +122 -0
  146. package/packages/ui/src/lib/query-client.ts +10 -0
  147. package/packages/ui/src/lib/render-content.tsx +23 -0
  148. package/packages/ui/src/lib/syntax-token.ts +4 -0
  149. package/packages/ui/src/main.tsx +14 -0
  150. package/packages/ui/src/queries/commits.ts +9 -0
  151. package/packages/ui/src/queries/diff.ts +9 -0
  152. package/packages/ui/src/queries/file.ts +10 -0
  153. package/packages/ui/src/queries/info.ts +9 -0
  154. package/packages/ui/src/queries/overview.ts +9 -0
  155. package/packages/ui/src/styles/app.css +178 -0
  156. package/packages/ui/src/types/comment.ts +61 -0
  157. package/packages/ui/src/vite-env.d.ts +1 -0
  158. package/packages/ui/tests/context-expansion.test.ts +279 -0
  159. package/packages/ui/tests/diff-utils.test.ts +409 -0
  160. package/packages/ui/tsconfig.json +14 -0
  161. package/packages/ui/vite.config.ts +23 -0
  162. package/scripts/build-skills.ts +26 -0
  163. package/scripts/build.ts +15 -0
  164. package/scripts/dev.ts +32 -0
  165. package/scripts/lib/transformers/claude-code.ts +11 -0
  166. package/scripts/lib/transformers/codex.ts +17 -0
  167. package/scripts/lib/transformers/cursor.ts +17 -0
  168. package/scripts/lib/transformers/index.ts +3 -0
  169. package/scripts/lib/utils.ts +70 -0
  170. package/scripts/link-dev.ts +54 -0
  171. package/skills/diffity-resolve/SKILL.md +55 -0
  172. package/skills/diffity-review/SKILL.md +74 -0
  173. package/skills/diffity-start/SKILL.md +27 -0
  174. package/tsconfig.json +22 -0
@@ -0,0 +1,11 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npm run:*)",
5
+ "mcp__playwright__browser_navigate",
6
+ "mcp__playwright__browser_take_screenshot",
7
+ "mcp__playwright__browser_evaluate",
8
+ "mcp__playwright__browser_snapshot"
9
+ ]
10
+ }
11
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kamran Ahmed
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,71 @@
1
+ <img src="./packages/ui/public/brand.svg" width="80" />
2
+
3
+ # diffity
4
+
5
+ [Diffity](https://diffity.com) is an agent-agnostic, GitHub-style diff viewer and code review tool.
6
+
7
+ ```bash
8
+ npm install -g diffity
9
+ ```
10
+
11
+ It works with Claude Code, Cursor, Codex, and any AI coding agent.
12
+
13
+ ## See your diffs
14
+
15
+ Run `diffity` inside any git repo — your browser opens with a GitHub-style, syntax-highlighted diff.
16
+
17
+ ```bash
18
+ diffity # working tree changes
19
+ diffity HEAD~1 # last commit
20
+ diffity HEAD~3 # last 3 commits
21
+ diffity main..feature # compare branches
22
+ diffity v1.0.0..v2.0.0 # compare tags
23
+ ```
24
+
25
+ For the working tree, you can leave comments, copy them into your agent with a button and ask it to resolve them. Alternatively, use the skills below to avoid this manual step and let your agent auto-solve them.
26
+
27
+ ## AI code review
28
+
29
+ Install the skills for your coding agent (Claude Code, Cursor, Codex, etc.):
30
+
31
+ ```bash
32
+ npx skills add kamranahmedse/diffity
33
+ ```
34
+
35
+ Then use the slash commands:
36
+
37
+ ```
38
+ # use this skill to open the browser with diff viewer
39
+ # you can review the code yourself and leave comments
40
+ /diffity-start
41
+
42
+ # once done, you can come back to the agent and use the
43
+ # below skill to ask agent to resolve your comments.
44
+ /diffity-resolve
45
+
46
+ # you can use this to have AI review your uncommitted
47
+ # changes and leave comments in the diff viewer
48
+ /diffity-review
49
+ ```
50
+
51
+ The review uses severity tags so you know what matters:
52
+ - `[must-fix]` — Bugs, security issues
53
+ - `[suggestion]` — Meaningful improvements
54
+ - `[nit]` — Style preferences
55
+ - `[question]` — Needs clarification
56
+
57
+ You can focus the review on what you care about: `/diffity-review security` or `/diffity-review performance`
58
+
59
+ ## Options
60
+
61
+ ```
62
+ --port <port> Custom port (default: 5391)
63
+ --no-open Don't open browser
64
+ --dark Dark mode
65
+ --unified Unified view (default: split)
66
+ --quiet Minimal terminal output
67
+ ```
68
+
69
+ ## License
70
+
71
+ MIT
package/development.md ADDED
@@ -0,0 +1,156 @@
1
+ # Development Guide
2
+
3
+ ## Prerequisites
4
+
5
+ - Node.js (v22+ recommended)
6
+ - npm
7
+ - Git
8
+
9
+ ## Initial Setup
10
+
11
+ ```bash
12
+ # Install dependencies
13
+ npm install
14
+
15
+ # Start all watchers (run this in the diffity repo)
16
+ npm run dev
17
+ ```
18
+
19
+ This automatically creates the `diffity-dev` binary, builds skills, adds `.bin` to your PATH (in `~/.zshrc` or `~/.bashrc`), and starts five concurrent processes:
20
+
21
+ | Process | What it does |
22
+ |---------|-------------|
23
+ | **parser** | `tsc --watch` on `@diffity/parser` |
24
+ | **git** | `tsc --watch` on `@diffity/git` |
25
+ | **cli** | `tsc --watch` on the CLI package |
26
+ | **ui** | `vite build --watch` on `@diffity/ui` |
27
+ | **skills** | Rebuilds Claude skills on change |
28
+
29
+ If this is your first time, source your shell profile to pick up the PATH change:
30
+
31
+ ```bash
32
+ source ~/.zshrc # or ~/.bashrc
33
+ ```
34
+
35
+ Then, in any git repository:
36
+
37
+ ```bash
38
+ diffity-dev
39
+ ```
40
+
41
+ This opens a diff viewer for that repo's working tree changes.
42
+
43
+ ## How the Dev Loop Works
44
+
45
+ ### UI changes
46
+
47
+ The UI uses `vite build --watch` instead of `vite dev`. This is intentional — `vite dev` serves files from memory and never writes to disk, but the CLI server serves static files from `packages/cli/dist/ui/`. Using `vite build --watch` rebuilds the output on every change so the CLI can serve it. Refresh the browser to see changes.
48
+
49
+ ### Server changes (CLI, git, parser)
50
+
51
+ `tsc --watch` recompiles TypeScript to `dist/` on save. The `diffity-dev` binary uses `node --watch-path=packages/cli/dist` which auto-restarts the Node process when any file in `dist/` changes. The port is persisted across restarts — the server retries the same port if it's briefly held by the old process.
52
+
53
+ ### How `diffity-dev` works
54
+
55
+ `diffity-dev` is a shell script (not a symlink) created by `scripts/link-dev.ts`. It runs:
56
+
57
+ ```bash
58
+ node --watch-path=<dist-dir> <cli-entry> --no-open "$@"
59
+ ```
60
+
61
+ - **Shell script, not symlink** — a symlink to `dist/index.js` would load the CLI once and never pick up server changes. The shell script wraps it with `node --watch-path` so it restarts on recompilation.
62
+ - **`--watch-path`** — restarts the process when `tsc --watch` writes new files to `dist/`.
63
+ - **`--no-open`** — prevents opening a new browser tab on every restart. Open the URL manually on first run.
64
+
65
+ ## Project Structure
66
+
67
+ ```
68
+ diffity/
69
+ ├── packages/
70
+ │ ├── cli/ # CLI server and entry point
71
+ │ │ ├── src/
72
+ │ │ └── dist/
73
+ │ │ ├── index.js # CLI binary
74
+ │ │ └── ui/ # Built UI (served as static files)
75
+ │ ├── git/ # Git operations (execSync wrappers)
76
+ │ ├── parser/ # Diff parsing library
77
+ │ └── ui/ # React frontend (Vite + Tailwind)
78
+ ├── scripts/
79
+ │ ├── dev.ts # Starts all watchers concurrently
80
+ │ ├── link-dev.ts # Creates the diffity-dev shell script
81
+ │ ├── build.ts # Production build (all packages in order)
82
+ │ └── build-skills.ts
83
+ └── .bin/
84
+ └── diffity-dev # Generated shell script for development
85
+ ```
86
+
87
+ ### Package dependencies
88
+
89
+ ```
90
+ @diffity/ui ──► @diffity/parser
91
+
92
+ @diffity/cli ────────┤
93
+
94
+ @diffity/git
95
+ ```
96
+
97
+ The UI builds into `packages/cli/dist/ui/` so the CLI can serve it as static files. In production, everything ships as a single `diffity` npm package.
98
+
99
+ ## Build Commands
100
+
101
+ ```bash
102
+ # Full production build (all packages in dependency order)
103
+ npm run build
104
+
105
+ # Build a single package
106
+ npm run build -w @diffity/parser
107
+ npm run build -w @diffity/git
108
+ npm run build -w @diffity/ui
109
+ npm run build -w diffity
110
+ ```
111
+
112
+ ## Testing
113
+
114
+ ```bash
115
+ # Run all tests
116
+ npm run test
117
+
118
+ # Run tests for a specific package
119
+ npm run test -w @diffity/parser
120
+ npm run test -w @diffity/ui
121
+
122
+ # Watch mode
123
+ npm run test:watch -w @diffity/parser
124
+ npm run test:watch -w @diffity/ui
125
+ ```
126
+
127
+ ## CLI Usage (for reference while developing)
128
+
129
+ ```bash
130
+ diffity-dev # Working tree changes
131
+ diffity-dev HEAD~1 # Last commit vs working tree
132
+ diffity-dev HEAD~3 # Last 3 commits vs working tree
133
+ diffity-dev main..feature # Compare branches
134
+ diffity-dev --port 3000 # Custom port
135
+ ```
136
+
137
+ ## Troubleshooting
138
+
139
+ ### Port already in use
140
+
141
+ If `diffity-dev` fails with `EADDRINUSE`, a previous process is still running:
142
+
143
+ ```bash
144
+ # Find and kill the process
145
+ lsof -i :5391
146
+ kill <PID>
147
+ ```
148
+
149
+ The server retries the same port up to 30 times (15 seconds) on startup to handle the brief overlap during `--watch` restarts. But if a completely separate process holds the port, you need to kill it manually.
150
+
151
+ ### Changes not showing up
152
+
153
+ 1. Make sure `npm run dev` is running in the diffity repo
154
+ 2. Check that the relevant watcher (ui/cli/parser/git) isn't showing errors
155
+ 3. Refresh the browser — there's no HMR in this setup
156
+ 4. For server changes, wait for the `--watch` restart (you'll see the diffity banner re-print in the terminal where `diffity-dev` is running)
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "diffity",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "GitHub-style git diff viewer in the browser",
6
+ "workspaces": [
7
+ "packages/cli",
8
+ "packages/git",
9
+ "packages/parser",
10
+ "packages/ui"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsx scripts/build.ts",
14
+ "build:skills": "tsx scripts/build-skills.ts",
15
+ "test": "npm run test -w @diffity/parser && npm run test -w @diffity/ui",
16
+ "link-dev": "tsx scripts/link-dev.ts",
17
+ "dev": "tsx scripts/dev.ts"
18
+ },
19
+ "keywords": [
20
+ "git",
21
+ "diff",
22
+ "viewer",
23
+ "cli"
24
+ ],
25
+ "author": "",
26
+ "license": "MIT",
27
+ "devDependencies": {
28
+ "concurrently": "^9.2.1",
29
+ "gray-matter": "^4.0.3",
30
+ "tsx": "^4.21.0"
31
+ }
32
+ }
@@ -0,0 +1,38 @@
1
+ import { build } from 'esbuild';
2
+ import { rmSync, readdirSync, statSync } from 'node:fs';
3
+ import { join, dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const distDir = join(__dirname, 'dist');
8
+
9
+ // Clean all previous build output except the ui/ directory (built separately by vite)
10
+ for (const entry of readdirSync(distDir)) {
11
+ if (entry === 'ui') {
12
+ continue;
13
+ }
14
+ const fullPath = join(distDir, entry);
15
+ const stat = statSync(fullPath);
16
+ rmSync(fullPath, { recursive: stat.isDirectory(), force: true });
17
+ }
18
+
19
+ await build({
20
+ entryPoints: [join(__dirname, 'src/index.ts')],
21
+ bundle: true,
22
+ platform: 'node',
23
+ target: 'node18',
24
+ format: 'esm',
25
+ outfile: join(distDir, 'index.js'),
26
+ banner: {
27
+ js: '#!/usr/bin/env node',
28
+ },
29
+ external: [
30
+ 'better-sqlite3',
31
+ 'commander',
32
+ 'open',
33
+ 'picocolors',
34
+ ],
35
+ sourcemap: false,
36
+ minifySyntax: true,
37
+ treeShaking: true,
38
+ });
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "diffity",
3
+ "version": "0.1.0",
4
+ "description": "GitHub-style git diff viewer in the browser",
5
+ "type": "module",
6
+ "bin": {
7
+ "diffity": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "node build.js",
11
+ "dev": "tsx src/index.ts",
12
+ "dev:watch": "tsc --watch"
13
+ },
14
+ "dependencies": {
15
+ "better-sqlite3": "^12.8.0",
16
+ "commander": "latest",
17
+ "open": "latest",
18
+ "picocolors": "latest"
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "keywords": [
24
+ "git",
25
+ "diff",
26
+ "viewer",
27
+ "cli",
28
+ "code-review",
29
+ "github"
30
+ ],
31
+ "author": "Kamran Ahmed <kamranahmed.se@gmail.com> (https://kamranahmed.se)",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/kamranahmedse/diffity.git"
36
+ },
37
+ "homepage": "https://diffity.com",
38
+ "bugs": {
39
+ "url": "https://github.com/kamranahmedse/diffity/issues"
40
+ },
41
+ "engines": {
42
+ "node": ">=18"
43
+ },
44
+ "devDependencies": {
45
+ "@types/better-sqlite3": "^7.6.13",
46
+ "@types/node": "^25.5.0",
47
+ "esbuild": "^0.27.0",
48
+ "tsx": "^4.21.0",
49
+ "typescript": "^5.9.3"
50
+ }
51
+ }
@@ -0,0 +1,187 @@
1
+ import type { Command } from 'commander';
2
+ import pc from 'picocolors';
3
+ import { isGitRepo } from '@diffity/git';
4
+ import { getCurrentSession } from './session.js';
5
+ import {
6
+ createThread,
7
+ getThreadsForSession,
8
+ getThread,
9
+ addReply,
10
+ updateThreadStatus,
11
+ type ThreadStatus,
12
+ type Thread,
13
+ } from './threads.js';
14
+
15
+ function requireSession() {
16
+ if (!isGitRepo()) {
17
+ console.error(pc.red('Error: Not a git repository'));
18
+ process.exit(1);
19
+ }
20
+
21
+ const session = getCurrentSession();
22
+ if (!session) {
23
+ console.error(pc.red('Error: No active review session.'));
24
+ console.error(pc.dim('Start diffity first to create a session.'));
25
+ process.exit(1);
26
+ }
27
+ return session;
28
+ }
29
+
30
+ function resolveThreadId(shortId: string, sessionId: string): Thread {
31
+ const thread = getThread(shortId);
32
+ if (!thread) {
33
+ console.error(pc.red(`Error: Thread not found: ${shortId}`));
34
+ process.exit(1);
35
+ }
36
+ if (thread.sessionId !== sessionId) {
37
+ console.error(pc.red(`Error: Thread ${shortId} does not belong to current session`));
38
+ process.exit(1);
39
+ }
40
+ return thread;
41
+ }
42
+
43
+ function formatThreadLine(thread: Thread): string {
44
+ const shortId = thread.id.slice(0, 8);
45
+ const isGeneral = thread.filePath === '__general__';
46
+ const statusColor = thread.status === 'open' ? pc.yellow : thread.status === 'resolved' ? pc.green : thread.status === 'dismissed' ? pc.dim : pc.cyan;
47
+ const statusLabel = statusColor(`[${thread.status}]`);
48
+ const firstComment = thread.comments[0]?.body || '';
49
+ const truncated = firstComment.length > 80 ? firstComment.slice(0, 77) + '...' : firstComment;
50
+
51
+ if (isGeneral) {
52
+ return `${statusLabel.padEnd(22)} ${pc.dim(shortId)} ${pc.bold('General comment')}\n${''.padEnd(15)}${pc.dim('"')}${truncated}${pc.dim('"')}`;
53
+ }
54
+
55
+ const lineRange = thread.startLine === thread.endLine
56
+ ? `${thread.startLine}`
57
+ : `${thread.startLine}-${thread.endLine}`;
58
+ const location = `${thread.filePath}:${lineRange}`;
59
+ const sideLabel = thread.side === 'old' ? '(old)' : '(new)';
60
+
61
+ return `${statusLabel.padEnd(22)} ${pc.dim(shortId)} ${location} ${pc.dim(sideLabel)}\n${''.padEnd(15)}${pc.dim('"')}${truncated}${pc.dim('"')}`;
62
+ }
63
+
64
+ export function registerAgentCommands(program: Command): void {
65
+ const agent = program
66
+ .command('agent')
67
+ .description('Agent commands for interacting with review comments')
68
+ .addHelpText('after', `
69
+ Examples:
70
+ $ diffity agent list --status open --json
71
+ $ diffity agent comment --file src/app.ts --line 42 --body "Missing null check"
72
+ $ diffity agent resolve abc123 --summary "Added null check"
73
+ $ diffity agent reply abc123 --body "Good catch, fixed"
74
+ $ diffity agent general-comment --body "Overall this looks good, just a few nits"`);
75
+
76
+ agent
77
+ .command('list')
78
+ .description('List comment threads in the current session (use --json for full details)')
79
+ .option('--status <status>', 'Filter by status (open, resolved, dismissed)')
80
+ .option('--json', 'Output as JSON')
81
+ .action((opts) => {
82
+ const validStatuses = ['open', 'resolved', 'dismissed'];
83
+ if (opts.status && !validStatuses.includes(opts.status)) {
84
+ console.error(pc.red(`Error: Invalid status "${opts.status}". Must be one of: ${validStatuses.join(', ')}`));
85
+ process.exit(1);
86
+ }
87
+ const session = requireSession();
88
+ const threads = getThreadsForSession(session.id, opts.status as ThreadStatus | undefined);
89
+
90
+ if (opts.json) {
91
+ console.log(JSON.stringify(threads, null, 2));
92
+ return;
93
+ }
94
+
95
+ if (threads.length === 0) {
96
+ console.log(pc.dim('No threads found.'));
97
+ return;
98
+ }
99
+
100
+ for (const thread of threads) {
101
+ console.log(formatThreadLine(thread));
102
+ }
103
+ });
104
+
105
+ agent
106
+ .command('comment')
107
+ .description('Create a new comment thread')
108
+ .requiredOption('--file <path>', 'File path (relative to repo root)')
109
+ .requiredOption('--line <n>', 'Line number (1-indexed)', parseInt)
110
+ .option('--end-line <n>', 'End line for multi-line comments (1-indexed)', parseInt)
111
+ .option('--side <side>', 'Which side of the diff (new or old)', 'new')
112
+ .requiredOption('--body <text>', 'Comment body')
113
+ .action((opts) => {
114
+ if (opts.side !== 'new' && opts.side !== 'old') {
115
+ console.error(pc.red(`Error: Invalid side "${opts.side}". Must be "new" or "old"`));
116
+ process.exit(1);
117
+ }
118
+ const session = requireSession();
119
+ const endLine = opts.endLine ?? opts.line;
120
+ const thread = createThread(
121
+ session.id,
122
+ opts.file,
123
+ opts.side,
124
+ opts.line,
125
+ endLine,
126
+ opts.body,
127
+ { name: 'Agent', type: 'agent' },
128
+ );
129
+ console.log(pc.green(`Created thread ${thread.id.slice(0, 8)}`));
130
+ });
131
+
132
+ agent
133
+ .command('resolve')
134
+ .description('Resolve a thread (marks as fixed)')
135
+ .argument('<thread-id>', 'Thread ID (or 8-char prefix)')
136
+ .option('--summary <text>', 'What was done to resolve it')
137
+ .action((id: string, opts) => {
138
+ const session = requireSession();
139
+ const thread = resolveThreadId(id, session.id);
140
+ const author = opts.summary ? { name: 'Agent', type: 'agent' as const } : undefined;
141
+ updateThreadStatus(thread.id, 'resolved', opts.summary, author);
142
+ console.log(pc.green(`Resolved thread ${thread.id.slice(0, 8)}`));
143
+ });
144
+
145
+ agent
146
+ .command('dismiss')
147
+ .description('Dismiss a thread (marks as won\'t fix)')
148
+ .argument('<thread-id>', 'Thread ID (or 8-char prefix)')
149
+ .option('--reason <text>', 'Why the thread is being dismissed')
150
+ .action((id: string, opts) => {
151
+ const session = requireSession();
152
+ const thread = resolveThreadId(id, session.id);
153
+ const author = opts.reason ? { name: 'Agent', type: 'agent' as const } : undefined;
154
+ updateThreadStatus(thread.id, 'dismissed', opts.reason, author);
155
+ console.log(pc.green(`Dismissed thread ${thread.id.slice(0, 8)}`));
156
+ });
157
+
158
+ agent
159
+ .command('reply')
160
+ .description('Reply to a comment thread')
161
+ .argument('<thread-id>', 'Thread ID (or 8-char prefix)')
162
+ .requiredOption('--body <text>', 'Reply body')
163
+ .action((id: string, opts) => {
164
+ const session = requireSession();
165
+ const thread = resolveThreadId(id, session.id);
166
+ addReply(thread.id, opts.body, { name: 'Agent', type: 'agent' });
167
+ console.log(pc.green(`Replied to thread ${thread.id.slice(0, 8)}`));
168
+ });
169
+
170
+ agent
171
+ .command('general-comment')
172
+ .description('Create a general comment on the entire diff (not tied to a specific file or line)')
173
+ .requiredOption('--body <text>', 'Comment body')
174
+ .action((opts) => {
175
+ const session = requireSession();
176
+ const thread = createThread(
177
+ session.id,
178
+ '__general__',
179
+ 'new',
180
+ 0,
181
+ 0,
182
+ opts.body,
183
+ { name: 'Agent', type: 'agent' },
184
+ );
185
+ console.log(pc.green(`Created general comment ${thread.id.slice(0, 8)}`));
186
+ });
187
+ }
@@ -0,0 +1,58 @@
1
+ import Database from 'better-sqlite3';
2
+ import { join } from 'node:path';
3
+ import { getDiffityDir } from '@diffity/git';
4
+
5
+ let db: Database.Database | null = null;
6
+
7
+ export function getDb(): Database.Database {
8
+ if (db) {
9
+ return db;
10
+ }
11
+
12
+ const dbPath = join(getDiffityDir(), 'reviews.db');
13
+ db = new Database(dbPath);
14
+ db.pragma('journal_mode = WAL');
15
+ db.pragma('foreign_keys = ON');
16
+ migrateDb(db);
17
+ return db;
18
+ }
19
+
20
+ function migrateDb(db: Database.Database): void {
21
+ db.exec(`
22
+ CREATE TABLE IF NOT EXISTS review_sessions (
23
+ id TEXT PRIMARY KEY,
24
+ ref TEXT NOT NULL,
25
+ head_hash TEXT NOT NULL,
26
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
27
+ );
28
+
29
+ CREATE TABLE IF NOT EXISTS comment_threads (
30
+ id TEXT PRIMARY KEY,
31
+ session_id TEXT NOT NULL REFERENCES review_sessions(id),
32
+ file_path TEXT NOT NULL,
33
+ side TEXT NOT NULL,
34
+ start_line INTEGER NOT NULL,
35
+ end_line INTEGER NOT NULL,
36
+ status TEXT NOT NULL DEFAULT 'open',
37
+ anchor_content TEXT,
38
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
39
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
40
+ );
41
+
42
+ CREATE TABLE IF NOT EXISTS comments (
43
+ id TEXT PRIMARY KEY,
44
+ thread_id TEXT NOT NULL REFERENCES comment_threads(id) ON DELETE CASCADE,
45
+ author_name TEXT NOT NULL,
46
+ author_type TEXT NOT NULL,
47
+ body TEXT NOT NULL,
48
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
49
+ );
50
+ `);
51
+ }
52
+
53
+ export function closeDb(): void {
54
+ if (db) {
55
+ db.close();
56
+ db = null;
57
+ }
58
+ }