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.
- package/.claude/settings.local.json +11 -0
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/development.md +156 -0
- package/package.json +32 -0
- package/packages/cli/build.js +38 -0
- package/packages/cli/package.json +51 -0
- package/packages/cli/src/agent.ts +187 -0
- package/packages/cli/src/db.ts +58 -0
- package/packages/cli/src/index.ts +196 -0
- package/packages/cli/src/review-routes.ts +150 -0
- package/packages/cli/src/server.ts +370 -0
- package/packages/cli/src/session.ts +48 -0
- package/packages/cli/src/threads.ts +238 -0
- package/packages/cli/tsconfig.json +13 -0
- package/packages/git/package.json +24 -0
- package/packages/git/src/commits.ts +28 -0
- package/packages/git/src/diff.ts +97 -0
- package/packages/git/src/exec.ts +35 -0
- package/packages/git/src/index.ts +5 -0
- package/packages/git/src/repo.ts +63 -0
- package/packages/git/src/status.ts +9 -0
- package/packages/git/src/types.ts +12 -0
- package/packages/git/tsconfig.json +9 -0
- package/packages/parser/package.json +26 -0
- package/packages/parser/src/index.ts +12 -0
- package/packages/parser/src/parse.ts +299 -0
- package/packages/parser/src/types.ts +52 -0
- package/packages/parser/src/word-diff.ts +155 -0
- package/packages/parser/tests/fixtures/binary-deleted.diff +4 -0
- package/packages/parser/tests/fixtures/binary-file.diff +4 -0
- package/packages/parser/tests/fixtures/binary-modified.diff +3 -0
- package/packages/parser/tests/fixtures/copied-file.diff +12 -0
- package/packages/parser/tests/fixtures/deleted-file.diff +9 -0
- package/packages/parser/tests/fixtures/empty.diff +0 -0
- package/packages/parser/tests/fixtures/hunk-with-context.diff +12 -0
- package/packages/parser/tests/fixtures/mode-change-with-content.diff +10 -0
- package/packages/parser/tests/fixtures/mode-change.diff +3 -0
- package/packages/parser/tests/fixtures/multi-file.diff +22 -0
- package/packages/parser/tests/fixtures/new-file.diff +9 -0
- package/packages/parser/tests/fixtures/no-newline.diff +10 -0
- package/packages/parser/tests/fixtures/renamed-file.diff +12 -0
- package/packages/parser/tests/fixtures/single-file-additions.diff +11 -0
- package/packages/parser/tests/fixtures/single-file-deletions.diff +11 -0
- package/packages/parser/tests/fixtures/single-file-mixed.diff +15 -0
- package/packages/parser/tests/fixtures/single-file-multi-hunk.diff +22 -0
- package/packages/parser/tests/fixtures/spaces-in-path.diff +9 -0
- package/packages/parser/tests/fixtures/submodule.diff +7 -0
- package/packages/parser/tests/fixtures/unicode-content.diff +11 -0
- package/packages/parser/tests/parse.test.ts +312 -0
- package/packages/parser/tests/word-diff-integration.test.ts +52 -0
- package/packages/parser/tests/word-diff.test.ts +121 -0
- package/packages/parser/tsconfig.json +10 -0
- package/packages/skills/diffity-resolve/SKILL.md +55 -0
- package/packages/skills/diffity-review/SKILL.md +74 -0
- package/packages/skills/diffity-start/SKILL.md +25 -0
- package/packages/ui/index.html +13 -0
- package/packages/ui/package.json +35 -0
- package/packages/ui/public/brand.svg +12 -0
- package/packages/ui/public/favicon.svg +15 -0
- package/packages/ui/src/app.tsx +14 -0
- package/packages/ui/src/components/comment-bubble.tsx +78 -0
- package/packages/ui/src/components/comment-form-row.tsx +58 -0
- package/packages/ui/src/components/comment-form.tsx +78 -0
- package/packages/ui/src/components/comment-line-number.tsx +60 -0
- package/packages/ui/src/components/comment-thread.tsx +209 -0
- package/packages/ui/src/components/commit-list.tsx +100 -0
- package/packages/ui/src/components/dashboard.tsx +84 -0
- package/packages/ui/src/components/diff-line.tsx +90 -0
- package/packages/ui/src/components/diff-page.tsx +332 -0
- package/packages/ui/src/components/diff-stats.tsx +20 -0
- package/packages/ui/src/components/diff-view.tsx +278 -0
- package/packages/ui/src/components/expand-row.tsx +45 -0
- package/packages/ui/src/components/file-block.tsx +536 -0
- package/packages/ui/src/components/file-tree-item.tsx +84 -0
- package/packages/ui/src/components/file-tree.tsx +72 -0
- package/packages/ui/src/components/general-comments.tsx +174 -0
- package/packages/ui/src/components/hunk-block-split.tsx +357 -0
- package/packages/ui/src/components/hunk-block.tsx +161 -0
- package/packages/ui/src/components/hunk-header.tsx +144 -0
- package/packages/ui/src/components/hunk-with-gap.tsx +113 -0
- package/packages/ui/src/components/icons/arrow-down-icon.tsx +7 -0
- package/packages/ui/src/components/icons/arrow-up-icon.tsx +7 -0
- package/packages/ui/src/components/icons/check-circle-icon.tsx +8 -0
- package/packages/ui/src/components/icons/check-icon.tsx +9 -0
- package/packages/ui/src/components/icons/chevron-down-icon.tsx +11 -0
- package/packages/ui/src/components/icons/chevron-icon.tsx +20 -0
- package/packages/ui/src/components/icons/chevron-up-down-icon.tsx +7 -0
- package/packages/ui/src/components/icons/chevron-up-icon.tsx +11 -0
- package/packages/ui/src/components/icons/comment-icon.tsx +9 -0
- package/packages/ui/src/components/icons/copy-icon.tsx +10 -0
- package/packages/ui/src/components/icons/eye-icon.tsx +10 -0
- package/packages/ui/src/components/icons/eye-off-icon.tsx +12 -0
- package/packages/ui/src/components/icons/file-icon.tsx +7 -0
- package/packages/ui/src/components/icons/folder-icon.tsx +19 -0
- package/packages/ui/src/components/icons/git-branch-icon.tsx +13 -0
- package/packages/ui/src/components/icons/keyboard-icon.tsx +13 -0
- package/packages/ui/src/components/icons/moon-icon.tsx +9 -0
- package/packages/ui/src/components/icons/plus-icon.tsx +9 -0
- package/packages/ui/src/components/icons/search-icon.tsx +10 -0
- package/packages/ui/src/components/icons/sidebar-icon.tsx +10 -0
- package/packages/ui/src/components/icons/spinner.tsx +7 -0
- package/packages/ui/src/components/icons/split-view-icon.tsx +10 -0
- package/packages/ui/src/components/icons/sun-icon.tsx +17 -0
- package/packages/ui/src/components/icons/trash-icon.tsx +11 -0
- package/packages/ui/src/components/icons/undo-icon.tsx +9 -0
- package/packages/ui/src/components/icons/unified-view-icon.tsx +12 -0
- package/packages/ui/src/components/icons/x-icon.tsx +10 -0
- package/packages/ui/src/components/line-number-cell.tsx +18 -0
- package/packages/ui/src/components/markdown-content.tsx +139 -0
- package/packages/ui/src/components/orphaned-threads.tsx +80 -0
- package/packages/ui/src/components/overview-file-list.tsx +57 -0
- package/packages/ui/src/components/render-expansion-rows.tsx +47 -0
- package/packages/ui/src/components/shortcut-modal.tsx +93 -0
- package/packages/ui/src/components/sidebar.tsx +80 -0
- package/packages/ui/src/components/skeleton.tsx +9 -0
- package/packages/ui/src/components/stale-diff-banner.tsx +21 -0
- package/packages/ui/src/components/summary-bar.tsx +39 -0
- package/packages/ui/src/components/toolbar.tsx +246 -0
- package/packages/ui/src/components/ui/badge.tsx +17 -0
- package/packages/ui/src/components/ui/confirm-dialog.tsx +52 -0
- package/packages/ui/src/components/ui/icon-button.tsx +23 -0
- package/packages/ui/src/components/ui/status-badge.tsx +57 -0
- package/packages/ui/src/components/ui/thread-badge.tsx +35 -0
- package/packages/ui/src/components/word-diff.tsx +126 -0
- package/packages/ui/src/hooks/use-comment-actions.ts +97 -0
- package/packages/ui/src/hooks/use-commits.ts +12 -0
- package/packages/ui/src/hooks/use-copy.ts +18 -0
- package/packages/ui/src/hooks/use-diff-staleness.ts +58 -0
- package/packages/ui/src/hooks/use-diff.ts +12 -0
- package/packages/ui/src/hooks/use-highlighter.ts +190 -0
- package/packages/ui/src/hooks/use-info.ts +12 -0
- package/packages/ui/src/hooks/use-keyboard.ts +55 -0
- package/packages/ui/src/hooks/use-line-selection.ts +157 -0
- package/packages/ui/src/hooks/use-overview.ts +12 -0
- package/packages/ui/src/hooks/use-review-threads.ts +12 -0
- package/packages/ui/src/hooks/use-search-params.ts +26 -0
- package/packages/ui/src/hooks/use-theme.ts +34 -0
- package/packages/ui/src/hooks/use-thread-navigation.ts +43 -0
- package/packages/ui/src/lib/api.ts +232 -0
- package/packages/ui/src/lib/cn.ts +6 -0
- package/packages/ui/src/lib/context-expansion.ts +122 -0
- package/packages/ui/src/lib/diff-utils.ts +268 -0
- package/packages/ui/src/lib/dom-utils.ts +13 -0
- package/packages/ui/src/lib/file-tree.ts +122 -0
- package/packages/ui/src/lib/query-client.ts +10 -0
- package/packages/ui/src/lib/render-content.tsx +23 -0
- package/packages/ui/src/lib/syntax-token.ts +4 -0
- package/packages/ui/src/main.tsx +14 -0
- package/packages/ui/src/queries/commits.ts +9 -0
- package/packages/ui/src/queries/diff.ts +9 -0
- package/packages/ui/src/queries/file.ts +10 -0
- package/packages/ui/src/queries/info.ts +9 -0
- package/packages/ui/src/queries/overview.ts +9 -0
- package/packages/ui/src/styles/app.css +178 -0
- package/packages/ui/src/types/comment.ts +61 -0
- package/packages/ui/src/vite-env.d.ts +1 -0
- package/packages/ui/tests/context-expansion.test.ts +279 -0
- package/packages/ui/tests/diff-utils.test.ts +409 -0
- package/packages/ui/tsconfig.json +14 -0
- package/packages/ui/vite.config.ts +23 -0
- package/scripts/build-skills.ts +26 -0
- package/scripts/build.ts +15 -0
- package/scripts/dev.ts +32 -0
- package/scripts/lib/transformers/claude-code.ts +11 -0
- package/scripts/lib/transformers/codex.ts +17 -0
- package/scripts/lib/transformers/cursor.ts +17 -0
- package/scripts/lib/transformers/index.ts +3 -0
- package/scripts/lib/utils.ts +70 -0
- package/scripts/link-dev.ts +54 -0
- package/skills/diffity-resolve/SKILL.md +55 -0
- package/skills/diffity-review/SKILL.md +74 -0
- package/skills/diffity-start/SKILL.md +27 -0
- package/tsconfig.json +22 -0
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
|
+
}
|