difit 2.0.10 → 2.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/README.ja.md +192 -0
- package/README.ko.md +192 -0
- package/README.md +61 -44
- package/README.zh.md +192 -0
- package/dist/cli/index.js +50 -0
- package/dist/cli/index.test.js +141 -12
- package/dist/cli/utils.js +18 -3
- package/dist/cli/utils.test.js +10 -1
- package/dist/client/assets/index-BtavrLIu.css +1 -0
- package/dist/client/assets/index-Bx2n4Aep.js +210 -0
- package/dist/client/assets/{prism-csharp-68c6WkNx.js → prism-csharp-BTkEzOdP.js} +1 -1
- package/dist/client/assets/{prism-java-C8EIlB8E.js → prism-java-B6gV82l4.js} +1 -1
- package/dist/client/assets/{prism-php-DHZyM8JV.js → prism-php-gnpy0VQF.js} +1 -1
- package/dist/client/assets/prism-protobuf-DiQ_z8B5.js +1 -0
- package/dist/client/assets/{prism-ruby-MnFNFfyf.js → prism-ruby-CMkpRodx.js} +1 -1
- package/dist/client/assets/{prism-solidity-CIeB0O-m.js → prism-solidity-BDXCWkss.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/file-watcher.d.ts +23 -0
- package/dist/server/file-watcher.js +236 -0
- package/dist/server/file-watcher.test.d.ts +1 -0
- package/dist/server/file-watcher.test.js +225 -0
- package/dist/server/git-diff.d.ts +2 -0
- package/dist/server/git-diff.js +47 -4
- package/dist/server/git-diff.test.js +209 -0
- package/dist/server/server.d.ts +5 -2
- package/dist/server/server.js +66 -16
- package/dist/server/server.test.js +3 -3
- package/dist/types/watch.d.ts +30 -0
- package/dist/types/watch.js +8 -0
- package/package.json +3 -2
- package/dist/client/assets/index-DMBW6MaM.css +0 -1
- package/dist/client/assets/index-EuLpHPLj.js +0 -200
package/README.zh.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
<h1 align="center">
|
|
2
|
+
<img src="public/logo.png" alt="difit" width="260">
|
|
3
|
+
</h1>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="./README.md">English</a> | <a href="./README.ja.md">日本語</a> | 简体中文 | <a href="./README.ko.md">한국어</a>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
**difit** 是一个让你使用 GitHub 风格查看器查看和审查本地 git 差异的 CLI 工具。除了清晰的视觉效果外,评论还可以作为 AI 提示进行复制。AI 时代的本地代码审查工具!
|
|
10
|
+
|
|
11
|
+
## ✨ 功能
|
|
12
|
+
|
|
13
|
+
- ⚡ **零配置**:只需运行 `npx difit` 即可使用
|
|
14
|
+
- 💬 **本地审查**:为差异添加评论,并将其与文件路径和行号一起复制给 AI
|
|
15
|
+
- 🖥️ **WebUI/终端UI**:在浏览器中使用 Web UI,或使用 `--tui` 保持在终端中
|
|
16
|
+
|
|
17
|
+
## ⚡ 快速开始
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx difit # 在 WebUI 中查看最新提交的差异
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 🚀 使用方法
|
|
24
|
+
|
|
25
|
+
### 基本用法
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx difit <target> # 查看单个提交差异
|
|
29
|
+
npx difit <target> [compare-with] # 比较两个提交/分支
|
|
30
|
+
npx difit --pr <github-pr-url> # 审查 GitHub 拉取请求
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 单个提交审查
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx difit # HEAD(最新)提交
|
|
37
|
+
npx difit 6f4a9b7 # 特定提交
|
|
38
|
+
npx difit feature # feature 分支上的最新提交
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 比较两个提交
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx difit @ main # 与 main 分支比较(@ 是 HEAD 的别名)
|
|
45
|
+
npx difit feature main # 比较分支
|
|
46
|
+
npx difit . origin/main # 比较工作目录与远程 main
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 特殊参数
|
|
50
|
+
|
|
51
|
+
difit 支持常见差异场景的特殊关键字:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npx difit . # 所有未提交的更改(暂存区 + 未暂存)
|
|
55
|
+
npx difit staged # 暂存区更改
|
|
56
|
+
npx difit working # 仅未暂存的更改
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### GitHub PR
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npx difit --pr https://github.com/owner/repo/pull/123
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
difit 使用以下方式自动处理 GitHub 认证:
|
|
66
|
+
|
|
67
|
+
1. **GitHub CLI**(推荐):如果您已使用 `gh auth login` 登录,difit 将使用您现有的凭据
|
|
68
|
+
2. **环境变量**:设置 `GITHUB_TOKEN` 环境变量
|
|
69
|
+
3. **无认证**:公共仓库无需认证即可工作(有速率限制)
|
|
70
|
+
|
|
71
|
+
#### GitHub Enterprise Server
|
|
72
|
+
|
|
73
|
+
对于 Enterprise Server PR,您必须设置在您的 Enterprise Server 实例上生成的令牌:
|
|
74
|
+
|
|
75
|
+
1. 转到 `https://YOUR-ENTERPRISE-SERVER/settings/tokens`
|
|
76
|
+
2. 生成具有适当范围的个人访问令牌
|
|
77
|
+
3. 将其设置为 `GITHUB_TOKEN` 环境变量
|
|
78
|
+
|
|
79
|
+
### 标准输入
|
|
80
|
+
|
|
81
|
+
通过使用管道通过标准输入传递统一差异,您可以使用 difit 查看来自任何工具的差异。
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# 查看来自其他工具的差异
|
|
85
|
+
diff -u file1.txt file2.txt | npx difit
|
|
86
|
+
|
|
87
|
+
# 审查保存的补丁
|
|
88
|
+
cat changes.patch | npx difit
|
|
89
|
+
|
|
90
|
+
# 与合并基础比较
|
|
91
|
+
git diff --merge-base main feature | npx difit
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## ⚙️ CLI 选项
|
|
95
|
+
|
|
96
|
+
| 标志 | 默认值 | 描述 |
|
|
97
|
+
| ---------------- | ------------ | ---------------------------------------------------------------------- |
|
|
98
|
+
| `<target>` | HEAD | 提交哈希、标签、HEAD~n、分支或特殊参数 |
|
|
99
|
+
| `[compare-with]` | - | 要比较的可选第二个提交(显示两者之间的差异) |
|
|
100
|
+
| `--pr <url>` | - | 要审查的 GitHub PR URL(例如:https://github.com/owner/repo/pull/123) |
|
|
101
|
+
| `--port` | 4966 | 首选端口;如果被占用则回退到 +1 |
|
|
102
|
+
| `--host` | 127.0.0.1 | 绑定服务器的主机地址(使用 0.0.0.0 进行外部访问) |
|
|
103
|
+
| `--no-open` | false | 不自动打开浏览器 |
|
|
104
|
+
| `--mode` | side-by-side | 显示模式:`inline` 或 `side-by-side` |
|
|
105
|
+
| `--tui` | false | 使用终端 UI 模式而不是 WebUI |
|
|
106
|
+
| `--clean` | false | 启动时清除所有现有评论 |
|
|
107
|
+
|
|
108
|
+
## 💬 评论系统
|
|
109
|
+
|
|
110
|
+
difit 包含一个审查评论系统,便于向 AI 编码代理提供反馈:
|
|
111
|
+
|
|
112
|
+
1. **添加评论**:单击任何差异行上的评论按钮或拖动选择范围
|
|
113
|
+
2. **编辑评论**:使用编辑按钮编辑现有评论
|
|
114
|
+
3. **生成提示**:评论包含"复制提示"按钮,可为 AI 编码代理格式化上下文
|
|
115
|
+
4. **复制全部**:使用"复制所有提示"以结构化格式复制所有评论
|
|
116
|
+
5. **持久存储**:评论按每个提交保存在浏览器 localStorage 中
|
|
117
|
+
|
|
118
|
+
### 评论提示格式
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
src/components/Button.tsx:L42 # 此行自动添加
|
|
122
|
+
使此变量名更具描述性
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
对于范围选择:
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
src/components/Button.tsx:L42-L48 # 此行自动添加
|
|
129
|
+
此部分是不必要的
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## 🎨 语法高亮语言
|
|
133
|
+
|
|
134
|
+
- **JavaScript/TypeScript**:`.js`、`.jsx`、`.ts`、`.tsx`
|
|
135
|
+
- **Web 技术**:HTML、CSS、JSON、XML、Markdown
|
|
136
|
+
- **Shell 脚本**:`.sh`、`.bash`、`.zsh`、`.fish`
|
|
137
|
+
- **后端语言**:PHP、SQL、Ruby、Java、Scala
|
|
138
|
+
- **系统语言**:C、C++、C#、Rust、Go
|
|
139
|
+
- **移动语言**:Swift、Kotlin、Dart
|
|
140
|
+
- **其他**:Python、YAML、Solidity、Vim 脚本
|
|
141
|
+
|
|
142
|
+
## 🛠️ 开发
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# 安装依赖
|
|
146
|
+
pnpm install
|
|
147
|
+
|
|
148
|
+
# 启动开发服务器(带热重载)
|
|
149
|
+
# 这会同时运行 Vite 开发服务器和 CLI,NODE_ENV=development
|
|
150
|
+
pnpm run dev
|
|
151
|
+
|
|
152
|
+
# 构建并启动生产服务器
|
|
153
|
+
pnpm run start <target>
|
|
154
|
+
|
|
155
|
+
# 构建生产版本
|
|
156
|
+
pnpm run build
|
|
157
|
+
|
|
158
|
+
# 运行测试
|
|
159
|
+
pnpm test
|
|
160
|
+
|
|
161
|
+
# 代码检查和格式化
|
|
162
|
+
pnpm run lint
|
|
163
|
+
pnpm run format
|
|
164
|
+
pnpm run typecheck
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### 开发工作流程
|
|
168
|
+
|
|
169
|
+
- **`pnpm run dev`**:同时启动 Vite 开发服务器(带热重载)和 CLI 服务器
|
|
170
|
+
- **`pnpm run start <target>`**:构建所有内容并启动生产服务器(用于测试最终构建)
|
|
171
|
+
- **开发模式**:使用 Vite 的开发服务器进行热重载和快速开发
|
|
172
|
+
- **生产模式**:提供构建的静态文件(供 npx 和生产构建使用)
|
|
173
|
+
|
|
174
|
+
## 🏗️ 架构
|
|
175
|
+
|
|
176
|
+
- **CLI**:使用 Commander.js 进行参数解析,具有全面的验证
|
|
177
|
+
- **后端**:Express 服务器配合 simple-git 进行差异处理
|
|
178
|
+
- **GitHub 集成**:Octokit 用于 GitHub API,具有自动认证(GitHub CLI + 环境变量)
|
|
179
|
+
- **前端**:React 18 + TypeScript + Vite
|
|
180
|
+
- **样式**:Tailwind CSS v4,带有类似 GitHub 的深色主题
|
|
181
|
+
- **语法高亮**:Prism.js 带动态语言加载
|
|
182
|
+
- **测试**:Vitest 用于单元测试,测试文件与源代码放在一起
|
|
183
|
+
- **质量**:ESLint、Prettier、lefthook 预提交钩子
|
|
184
|
+
|
|
185
|
+
## 📋 要求
|
|
186
|
+
|
|
187
|
+
- Node.js ≥ 21.0.0
|
|
188
|
+
- 包含要审查的提交的 Git 仓库
|
|
189
|
+
|
|
190
|
+
## 📄 许可证
|
|
191
|
+
|
|
192
|
+
MIT
|
package/dist/cli/index.js
CHANGED
|
@@ -4,10 +4,28 @@ import React from 'react';
|
|
|
4
4
|
import { simpleGit } from 'simple-git';
|
|
5
5
|
import pkg from '../../package.json' with { type: 'json' };
|
|
6
6
|
import { startServer } from '../server/server.js';
|
|
7
|
+
import { DiffMode } from '../types/watch.js';
|
|
7
8
|
import { findUntrackedFiles, markFilesIntentToAdd, promptUser, validateDiffArguments, resolvePrCommits, } from './utils.js';
|
|
8
9
|
function isSpecialArg(arg) {
|
|
9
10
|
return arg === 'working' || arg === 'staged' || arg === '.';
|
|
10
11
|
}
|
|
12
|
+
function determineDiffMode(targetCommitish, compareWith) {
|
|
13
|
+
// If comparing specific commits/branches (not involving HEAD), no watching needed
|
|
14
|
+
if (compareWith && targetCommitish !== 'HEAD') {
|
|
15
|
+
return DiffMode.SPECIFIC;
|
|
16
|
+
}
|
|
17
|
+
if (targetCommitish === 'working') {
|
|
18
|
+
return DiffMode.WORKING;
|
|
19
|
+
}
|
|
20
|
+
if (targetCommitish === 'staged') {
|
|
21
|
+
return DiffMode.STAGED;
|
|
22
|
+
}
|
|
23
|
+
if (targetCommitish === '.') {
|
|
24
|
+
return DiffMode.DOT;
|
|
25
|
+
}
|
|
26
|
+
// Default mode: HEAD^ vs HEAD or HEAD vs other commits (watch for HEAD changes)
|
|
27
|
+
return DiffMode.DEFAULT;
|
|
28
|
+
}
|
|
11
29
|
const program = new Command();
|
|
12
30
|
program
|
|
13
31
|
.name('difit')
|
|
@@ -24,6 +42,29 @@ program
|
|
|
24
42
|
.option('--clean', 'start with a clean slate by clearing all existing comments')
|
|
25
43
|
.action(async (commitish, compareWith, options) => {
|
|
26
44
|
try {
|
|
45
|
+
// Check if we should read from stdin
|
|
46
|
+
const shouldReadStdin = !process.stdin.isTTY || commitish === '-';
|
|
47
|
+
if (shouldReadStdin) {
|
|
48
|
+
// Read unified diff from stdin
|
|
49
|
+
const diffContent = await readStdin();
|
|
50
|
+
if (!diffContent.trim()) {
|
|
51
|
+
console.error('Error: No diff content received from stdin');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
// Start server with stdin diff
|
|
55
|
+
const { url } = await startServer({
|
|
56
|
+
stdinDiff: diffContent,
|
|
57
|
+
preferredPort: options.port,
|
|
58
|
+
host: options.host,
|
|
59
|
+
openBrowser: options.open,
|
|
60
|
+
mode: options.mode,
|
|
61
|
+
clearComments: options.clean,
|
|
62
|
+
});
|
|
63
|
+
console.log(`\n🚀 difit server started on ${url}`);
|
|
64
|
+
console.log(`📋 Reviewing: diff from stdin`);
|
|
65
|
+
console.log('\nPress Ctrl+C to stop the server');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
27
68
|
// Determine target and base commitish
|
|
28
69
|
let targetCommitish = commitish;
|
|
29
70
|
let baseCommitish;
|
|
@@ -88,6 +129,7 @@ program
|
|
|
88
129
|
process.exit(1);
|
|
89
130
|
}
|
|
90
131
|
}
|
|
132
|
+
const diffMode = determineDiffMode(targetCommitish, compareWith);
|
|
91
133
|
const { url, port, isEmpty } = await startServer({
|
|
92
134
|
targetCommitish,
|
|
93
135
|
baseCommitish,
|
|
@@ -96,6 +138,7 @@ program
|
|
|
96
138
|
openBrowser: options.open,
|
|
97
139
|
mode: options.mode,
|
|
98
140
|
clearComments: options.clean,
|
|
141
|
+
diffMode,
|
|
99
142
|
});
|
|
100
143
|
console.log(`\n🚀 difit server started on ${url}`);
|
|
101
144
|
console.log(`📋 Reviewing: ${targetCommitish}`);
|
|
@@ -137,6 +180,13 @@ program
|
|
|
137
180
|
});
|
|
138
181
|
program.parse();
|
|
139
182
|
// Check for untracked files and prompt user to add them for diff visibility
|
|
183
|
+
async function readStdin() {
|
|
184
|
+
const chunks = [];
|
|
185
|
+
for await (const chunk of process.stdin) {
|
|
186
|
+
chunks.push(chunk);
|
|
187
|
+
}
|
|
188
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
189
|
+
}
|
|
140
190
|
async function handleUntrackedFiles(git) {
|
|
141
191
|
const files = await findUntrackedFiles(git);
|
|
142
192
|
if (files.length === 0) {
|
package/dist/cli/index.test.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
4
|
+
import { DiffMode } from '../types/watch.js';
|
|
4
5
|
// Mock all external dependencies
|
|
5
6
|
vi.mock('simple-git');
|
|
6
7
|
vi.mock('../server/server.js');
|
|
@@ -37,8 +38,8 @@ describe('CLI index.ts', () => {
|
|
|
37
38
|
vi.mocked(simpleGit).mockReturnValue(mockGit);
|
|
38
39
|
mockStartServer = vi.mocked(startServer);
|
|
39
40
|
mockStartServer.mockResolvedValue({
|
|
40
|
-
port:
|
|
41
|
-
url: 'http://localhost:
|
|
41
|
+
port: 4966,
|
|
42
|
+
url: 'http://localhost:4966',
|
|
42
43
|
isEmpty: false,
|
|
43
44
|
});
|
|
44
45
|
mockPromptUser = vi.mocked(promptUser);
|
|
@@ -421,8 +422,8 @@ describe('CLI index.ts', () => {
|
|
|
421
422
|
it('displays clean message when flag is used', async () => {
|
|
422
423
|
mockFindUntrackedFiles.mockResolvedValue([]);
|
|
423
424
|
mockStartServer.mockResolvedValue({
|
|
424
|
-
port:
|
|
425
|
-
url: 'http://localhost:
|
|
425
|
+
port: 4966,
|
|
426
|
+
url: 'http://localhost:4966',
|
|
426
427
|
isEmpty: false,
|
|
427
428
|
});
|
|
428
429
|
const program = new Command();
|
|
@@ -461,8 +462,8 @@ describe('CLI index.ts', () => {
|
|
|
461
462
|
it('does not display clean message when flag is not used', async () => {
|
|
462
463
|
mockFindUntrackedFiles.mockResolvedValue([]);
|
|
463
464
|
mockStartServer.mockResolvedValue({
|
|
464
|
-
port:
|
|
465
|
-
url: 'http://localhost:
|
|
465
|
+
port: 4966,
|
|
466
|
+
url: 'http://localhost:4966',
|
|
466
467
|
isEmpty: false,
|
|
467
468
|
});
|
|
468
469
|
const program = new Command();
|
|
@@ -503,8 +504,8 @@ describe('CLI index.ts', () => {
|
|
|
503
504
|
it('displays server startup message with correct URL', async () => {
|
|
504
505
|
mockFindUntrackedFiles.mockResolvedValue([]);
|
|
505
506
|
mockStartServer.mockResolvedValue({
|
|
506
|
-
port:
|
|
507
|
-
url: 'http://localhost:
|
|
507
|
+
port: 4966,
|
|
508
|
+
url: 'http://localhost:4966',
|
|
508
509
|
isEmpty: false,
|
|
509
510
|
});
|
|
510
511
|
const program = new Command();
|
|
@@ -540,14 +541,14 @@ describe('CLI index.ts', () => {
|
|
|
540
541
|
}
|
|
541
542
|
});
|
|
542
543
|
await program.parseAsync([], { from: 'user' });
|
|
543
|
-
expect(console.log).toHaveBeenCalledWith('\n🚀 difit server started on http://localhost:
|
|
544
|
+
expect(console.log).toHaveBeenCalledWith('\n🚀 difit server started on http://localhost:4966');
|
|
544
545
|
expect(console.log).toHaveBeenCalledWith('📋 Reviewing: HEAD');
|
|
545
546
|
});
|
|
546
547
|
it('displays correct message when no differences found', async () => {
|
|
547
548
|
mockFindUntrackedFiles.mockResolvedValue([]);
|
|
548
549
|
mockStartServer.mockResolvedValue({
|
|
549
|
-
port:
|
|
550
|
-
url: 'http://localhost:
|
|
550
|
+
port: 4966,
|
|
551
|
+
url: 'http://localhost:4966',
|
|
551
552
|
isEmpty: true,
|
|
552
553
|
});
|
|
553
554
|
const program = new Command();
|
|
@@ -578,7 +579,7 @@ describe('CLI index.ts', () => {
|
|
|
578
579
|
});
|
|
579
580
|
await program.parseAsync([], { from: 'user' });
|
|
580
581
|
expect(console.log).toHaveBeenCalledWith('\n! No differences found. Browser will not open automatically.');
|
|
581
|
-
expect(console.log).toHaveBeenCalledWith(' Server is running at http://localhost:
|
|
582
|
+
expect(console.log).toHaveBeenCalledWith(' Server is running at http://localhost:4966 if you want to check manually.\n');
|
|
582
583
|
});
|
|
583
584
|
});
|
|
584
585
|
describe('Server mode option handling', () => {
|
|
@@ -812,4 +813,132 @@ describe('CLI index.ts', () => {
|
|
|
812
813
|
expect(process.exit).toHaveBeenCalledWith(1);
|
|
813
814
|
});
|
|
814
815
|
});
|
|
816
|
+
describe('Diff mode determination', () => {
|
|
817
|
+
const testCases = [
|
|
818
|
+
{
|
|
819
|
+
name: 'determines DEFAULT mode for HEAD',
|
|
820
|
+
args: ['HEAD'],
|
|
821
|
+
expectedMode: 'default',
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
name: 'determines WORKING mode for working',
|
|
825
|
+
args: ['working'],
|
|
826
|
+
expectedMode: 'working',
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
name: 'determines STAGED mode for staged',
|
|
830
|
+
args: ['staged'],
|
|
831
|
+
expectedMode: 'staged',
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
name: 'determines DOT mode for dot argument',
|
|
835
|
+
args: ['.'],
|
|
836
|
+
expectedMode: 'dot',
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
name: 'determines SPECIFIC mode for commit comparison',
|
|
840
|
+
args: ['abc123', 'def456'],
|
|
841
|
+
expectedMode: 'specific',
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
name: 'determines DEFAULT mode for custom commit',
|
|
845
|
+
args: ['main'],
|
|
846
|
+
expectedMode: 'default',
|
|
847
|
+
},
|
|
848
|
+
];
|
|
849
|
+
testCases.forEach(({ name, args, expectedMode }) => {
|
|
850
|
+
it(name, async () => {
|
|
851
|
+
mockFindUntrackedFiles.mockResolvedValue([]);
|
|
852
|
+
const program = new Command();
|
|
853
|
+
program
|
|
854
|
+
.argument('[commit-ish]', 'commit-ish', 'HEAD')
|
|
855
|
+
.argument('[compare-with]', 'compare-with')
|
|
856
|
+
.option('--port <port>', 'port', parseInt)
|
|
857
|
+
.option('--host <host>', 'host', '')
|
|
858
|
+
.option('--no-open', 'no-open')
|
|
859
|
+
.option('--mode <mode>', 'mode', 'side-by-side')
|
|
860
|
+
.option('--tui', 'tui')
|
|
861
|
+
.option('--pr <url>', 'pr')
|
|
862
|
+
.action(async (commitish, compareWith, options) => {
|
|
863
|
+
// Simulate determineDiffMode function behavior
|
|
864
|
+
let diffMode;
|
|
865
|
+
if (compareWith && commitish !== 'HEAD') {
|
|
866
|
+
diffMode = DiffMode.SPECIFIC;
|
|
867
|
+
}
|
|
868
|
+
else if (commitish === 'working') {
|
|
869
|
+
diffMode = DiffMode.WORKING;
|
|
870
|
+
}
|
|
871
|
+
else if (commitish === 'staged') {
|
|
872
|
+
diffMode = DiffMode.STAGED;
|
|
873
|
+
}
|
|
874
|
+
else if (commitish === '.') {
|
|
875
|
+
diffMode = DiffMode.DOT;
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
diffMode = DiffMode.DEFAULT;
|
|
879
|
+
}
|
|
880
|
+
await startServer({
|
|
881
|
+
targetCommitish: commitish,
|
|
882
|
+
baseCommitish: compareWith || commitish + '^',
|
|
883
|
+
preferredPort: options.port,
|
|
884
|
+
host: options.host,
|
|
885
|
+
openBrowser: options.open,
|
|
886
|
+
mode: options.mode,
|
|
887
|
+
diffMode,
|
|
888
|
+
});
|
|
889
|
+
});
|
|
890
|
+
await program.parseAsync(args, { from: 'user' });
|
|
891
|
+
expect(mockStartServer).toHaveBeenCalledWith(expect.objectContaining({
|
|
892
|
+
diffMode: expectedMode,
|
|
893
|
+
}));
|
|
894
|
+
});
|
|
895
|
+
});
|
|
896
|
+
it('handles HEAD comparison with different commit', async () => {
|
|
897
|
+
mockFindUntrackedFiles.mockResolvedValue([]);
|
|
898
|
+
const program = new Command();
|
|
899
|
+
program
|
|
900
|
+
.argument('[commit-ish]', 'commit-ish', 'HEAD')
|
|
901
|
+
.argument('[compare-with]', 'compare-with')
|
|
902
|
+
.option('--port <port>', 'port', parseInt)
|
|
903
|
+
.option('--host <host>', 'host', '')
|
|
904
|
+
.option('--no-open', 'no-open')
|
|
905
|
+
.option('--mode <mode>', 'mode', 'side-by-side')
|
|
906
|
+
.option('--tui', 'tui')
|
|
907
|
+
.option('--pr <url>', 'pr')
|
|
908
|
+
.action(async (commitish, compareWith, options) => {
|
|
909
|
+
// Simulate determineDiffMode function behavior
|
|
910
|
+
let diffMode;
|
|
911
|
+
if (compareWith && commitish !== 'HEAD') {
|
|
912
|
+
diffMode = DiffMode.SPECIFIC;
|
|
913
|
+
}
|
|
914
|
+
else if (commitish === 'working') {
|
|
915
|
+
diffMode = DiffMode.WORKING;
|
|
916
|
+
}
|
|
917
|
+
else if (commitish === 'staged') {
|
|
918
|
+
diffMode = DiffMode.STAGED;
|
|
919
|
+
}
|
|
920
|
+
else if (commitish === '.') {
|
|
921
|
+
diffMode = DiffMode.DOT;
|
|
922
|
+
}
|
|
923
|
+
else {
|
|
924
|
+
diffMode = DiffMode.DEFAULT;
|
|
925
|
+
}
|
|
926
|
+
await startServer({
|
|
927
|
+
targetCommitish: commitish,
|
|
928
|
+
baseCommitish: compareWith || commitish + '^',
|
|
929
|
+
preferredPort: options.port,
|
|
930
|
+
host: options.host,
|
|
931
|
+
openBrowser: options.open,
|
|
932
|
+
mode: options.mode,
|
|
933
|
+
diffMode,
|
|
934
|
+
});
|
|
935
|
+
});
|
|
936
|
+
await program.parseAsync(['HEAD', 'main'], { from: 'user' });
|
|
937
|
+
expect(mockStartServer).toHaveBeenCalledWith(expect.objectContaining({
|
|
938
|
+
diffMode: 'default', // HEAD with comparison is still DEFAULT mode
|
|
939
|
+
targetCommitish: 'HEAD',
|
|
940
|
+
baseCommitish: 'main',
|
|
941
|
+
}));
|
|
942
|
+
});
|
|
943
|
+
});
|
|
815
944
|
});
|
package/dist/cli/utils.js
CHANGED
|
@@ -21,6 +21,7 @@ export function validateCommitish(commitish) {
|
|
|
21
21
|
/^[a-f0-9]{4,40}\^+$/i, // SHA hashes with ^ suffix (parent references)
|
|
22
22
|
/^[a-f0-9]{4,40}~\d+$/i, // SHA hashes with ~N suffix (ancestor references)
|
|
23
23
|
/^HEAD(~\d+|\^\d*)*$/, // HEAD, HEAD~1, HEAD^, HEAD^2, etc.
|
|
24
|
+
/^@(~\d+|\^\d*)*$/, // @, @~1, @^, @^2, etc. (@ is Git alias for HEAD)
|
|
24
25
|
];
|
|
25
26
|
// Check if it matches any specific patterns first
|
|
26
27
|
if (validPatterns.some((pattern) => pattern.test(trimmed))) {
|
|
@@ -35,8 +36,7 @@ function isValidBranchName(name) {
|
|
|
35
36
|
return false; // Cannot start with dash
|
|
36
37
|
if (name.endsWith('.'))
|
|
37
38
|
return false; // Cannot end with dot
|
|
38
|
-
|
|
39
|
-
return false; // Cannot be just @
|
|
39
|
+
// @ is a valid Git alias for HEAD, so we should allow it
|
|
40
40
|
if (name.includes('..'))
|
|
41
41
|
return false; // No consecutive dots
|
|
42
42
|
if (name.includes('@{'))
|
|
@@ -130,7 +130,22 @@ export async function fetchPrDetails(prInfo) {
|
|
|
130
130
|
}
|
|
131
131
|
catch (error) {
|
|
132
132
|
if (error instanceof Error) {
|
|
133
|
-
|
|
133
|
+
let authHint = '';
|
|
134
|
+
// Provide more specific error messages for authentication issues
|
|
135
|
+
if (error.message.includes('Bad credentials')) {
|
|
136
|
+
if (prInfo.hostname !== 'github.com') {
|
|
137
|
+
authHint = `\n\nFor GitHub Enterprise Server (${prInfo.hostname}):
|
|
138
|
+
1. Generate a token on YOUR Enterprise Server: https://${prInfo.hostname}/settings/tokens
|
|
139
|
+
2. Set it as GITHUB_TOKEN environment variable
|
|
140
|
+
3. Tokens from github.com will NOT work on Enterprise servers`;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
authHint = '\n\nTry: gh auth login or set GITHUB_TOKEN environment variable';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else if (!token) {
|
|
147
|
+
authHint = ' (Try: gh auth login or set GITHUB_TOKEN environment variable)';
|
|
148
|
+
}
|
|
134
149
|
throw new Error(`Failed to fetch PR details: ${error.message}${authHint}`);
|
|
135
150
|
}
|
|
136
151
|
throw new Error('Failed to fetch PR details: Unknown error');
|
package/dist/cli/utils.test.js
CHANGED
|
@@ -27,6 +27,15 @@ describe('CLI Utils', () => {
|
|
|
27
27
|
expect(validateCommitish('HEAD^2')).toBe(true);
|
|
28
28
|
expect(validateCommitish('HEAD~2^1')).toBe(true);
|
|
29
29
|
});
|
|
30
|
+
it('should validate @ references (Git alias for HEAD)', () => {
|
|
31
|
+
expect(validateCommitish('@')).toBe(true);
|
|
32
|
+
expect(validateCommitish('@~1')).toBe(true);
|
|
33
|
+
expect(validateCommitish('@~10')).toBe(true);
|
|
34
|
+
expect(validateCommitish('@^')).toBe(true);
|
|
35
|
+
expect(validateCommitish('@^1')).toBe(true);
|
|
36
|
+
expect(validateCommitish('@^2')).toBe(true);
|
|
37
|
+
expect(validateCommitish('@~2^1')).toBe(true);
|
|
38
|
+
});
|
|
30
39
|
it('should validate branch names', () => {
|
|
31
40
|
// Valid branch names according to git rules
|
|
32
41
|
expect(validateCommitish('main')).toBe(true);
|
|
@@ -56,7 +65,7 @@ describe('CLI Utils', () => {
|
|
|
56
65
|
// Invalid branch names according to git rules
|
|
57
66
|
expect(validateCommitish('-feature')).toBe(false); // cannot start with dash
|
|
58
67
|
expect(validateCommitish('feature.')).toBe(false); // cannot end with dot
|
|
59
|
-
expect(validateCommitish('@')).toBe(
|
|
68
|
+
expect(validateCommitish('@')).toBe(true); // @ is a valid Git alias for HEAD
|
|
60
69
|
expect(validateCommitish('feature..test')).toBe(false); // no consecutive dots
|
|
61
70
|
expect(validateCommitish('feature@{upstream}')).toBe(false); // no @{ sequence
|
|
62
71
|
expect(validateCommitish('feature//test')).toBe(false); // no consecutive slashes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/*! tailwindcss v4.1.11 | MIT License | https://tailwindcss.com */@layer properties{@supports ((-webkit-hyphens:none) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-ease:initial;--tw-content:"";--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-100:oklch(93.6% .032 17.717);--color-orange-700:oklch(55.3% .195 38.402);--color-orange-800:oklch(47% .157 37.304);--color-orange-900:oklch(40.8% .123 38.172);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-600:oklch(68.1% .162 75.834);--color-green-100:oklch(96.2% .044 156.743);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-md:28rem;--container-4xl:56rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--font-weight-medium:500;--font-weight-semibold:600;--radius-md:.375rem;--radius-lg:.5rem;--ease-out:cubic-bezier(0,0,.2,1);--ease-in-out:cubic-bezier(.4,0,.2,1);--animate-spin:spin 1s linear infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-github-bg-primary:#0d1117;--color-github-bg-secondary:#161b22;--color-github-bg-tertiary:#21262d;--color-github-border:#30363d;--color-github-text-primary:#f0f6fc;--color-github-text-secondary:#8b949e;--color-github-text-muted:#6e7681;--color-github-accent:#238636;--color-github-danger:#da3633;--color-github-warning:#d29922;--color-diff-addition-bg:#0d4429;--color-diff-addition-border:#1b7c3d;--color-diff-deletion-bg:#67060c;--color-diff-deletion-border:#da3633;--color-diff-neutral-bg:#21262d;--color-diff-selected-bg:#ae7c1426;--color-diff-selected-border:#ae7c1466;--color-comment-bg:#1c2128;--color-comment-border:#373e47;--color-comment-text:#e6edf3}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::-moz-placeholder{opacity:1}::placeholder{opacity:1}@supports (not (-webkit-appearance:-apple-pay-button)) or (contain-intrinsic-size:1px){::-moz-placeholder{color:currentColor}::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::-moz-placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing)*0)}.top-0{top:calc(var(--spacing)*0)}.top-1\/2{top:50%}.-right-2{right:calc(var(--spacing)*-2)}.right-0{right:calc(var(--spacing)*0)}.left-0{left:calc(var(--spacing)*0)}.left-3{left:calc(var(--spacing)*3)}.z-10{z-index:10}.z-50{z-index:50}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-0{margin:calc(var(--spacing)*0)}.m-2{margin:calc(var(--spacing)*2)}.mx-3{margin-inline:calc(var(--spacing)*3)}.mx-4{margin-inline:calc(var(--spacing)*4)}.mx-auto{margin-inline:auto}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-4{margin-top:calc(var(--spacing)*4)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.h-2{height:calc(var(--spacing)*2)}.h-4{height:calc(var(--spacing)*4)}.h-7{height:calc(var(--spacing)*7)}.h-full{height:100%}.h-screen{height:100vh}.max-h-96{max-height:calc(var(--spacing)*96)}.max-h-\[80vh\]{max-height:80vh}.max-h-\[calc\(80vh-120px\)\]{max-height:calc(80vh - 120px)}.min-h-\[16px\]{min-height:16px}.min-h-\[20px\]{min-height:20px}.min-h-\[60px\]{min-height:60px}.w-1\/2{width:50%}.w-4{width:calc(var(--spacing)*4)}.w-5{width:calc(var(--spacing)*5)}.w-7{width:calc(var(--spacing)*7)}.w-8{width:calc(var(--spacing)*8)}.w-\[50px\]{width:50px}.w-\[60px\]{width:60px}.w-full{width:100%}.max-w-4xl{max-width:var(--container-4xl)}.max-w-full{max-width:100%}.max-w-md{max-width:var(--container-md)}.min-w-0{min-width:calc(var(--spacing)*0)}.flex-1{flex:1}.flex-shrink-0{flex-shrink:0}.border-collapse{border-collapse:collapse}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.rotate-180{rotate:180deg}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.animate-spin{animation:var(--animate-spin)}.cursor-col-resize{cursor:col-resize}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.resize-none{resize:none}.resize-y{resize:vertical}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-l{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-b{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-t-2{border-top-style:var(--tw-border-style);border-top-width:2px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-none{--tw-border-style:none;border-style:none}.border-\[var\(--border-muted\)\]{border-color:var(--border-muted)}.border-github-accent{border-color:var(--color-github-accent)}.border-github-border{border-color:var(--color-github-border)}.border-github-text-muted{border-color:var(--color-github-text-muted)}.border-orange-800{border-color:var(--color-orange-800)}.border-yellow-600\/50{border-color:#cd890080}@supports (color:color-mix(in lab,red,red)){.border-yellow-600\/50{border-color:color-mix(in oklab,var(--color-yellow-600)50%,transparent)}}.border-t-github-accent{border-top-color:var(--color-github-accent)}.border-l-yellow-400{border-left-color:var(--color-yellow-400)}.bg-\[var\(--bg-secondary\)\]{background-color:var(--bg-secondary)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab,red,red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black)50%,transparent)}}.bg-diff-addition-bg{background-color:var(--color-diff-addition-bg)}.bg-diff-deletion-bg{background-color:var(--color-diff-deletion-bg)}.bg-github-accent{background-color:var(--color-github-accent)}.bg-github-bg-primary{background-color:var(--color-github-bg-primary)}.bg-github-bg-secondary{background-color:var(--color-github-bg-secondary)}.bg-github-bg-tertiary{background-color:var(--color-github-bg-tertiary)}.bg-github-border{background-color:var(--color-github-border)}.bg-green-100\/10{background-color:#dcfce71a}@supports (color:color-mix(in lab,red,red)){.bg-green-100\/10{background-color:color-mix(in oklab,var(--color-green-100)10%,transparent)}}.bg-orange-700{background-color:var(--color-orange-700)}.bg-red-100\/10{background-color:#ffe2e21a}@supports (color:color-mix(in lab,red,red)){.bg-red-100\/10{background-color:color-mix(in oklab,var(--color-red-100)10%,transparent)}}.bg-transparent{background-color:#0000}.p-0{padding:calc(var(--spacing)*0)}.p-1{padding:calc(var(--spacing)*1)}.p-1\.5{padding:calc(var(--spacing)*1.5)}.p-2{padding:calc(var(--spacing)*2)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.px-1{padding-inline:calc(var(--spacing)*1)}.px-1\.5{padding-inline:calc(var(--spacing)*1.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-5{padding-inline:calc(var(--spacing)*5)}.px-6{padding-inline:calc(var(--spacing)*6)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.pt-4{padding-top:calc(var(--spacing)*4)}.pr-2{padding-right:calc(var(--spacing)*2)}.pr-3{padding-right:calc(var(--spacing)*3)}.pr-5{padding-right:calc(var(--spacing)*5)}.pb-px{padding-bottom:1px}.pl-9{padding-left:calc(var(--spacing)*9)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-top{vertical-align:top}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-5{--tw-leading:calc(var(--spacing)*5);line-height:calc(var(--spacing)*5)}.leading-6{--tw-leading:calc(var(--spacing)*6);line-height:calc(var(--spacing)*6)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.break-all{word-break:break-all}.text-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-github-accent{color:var(--color-github-accent)}.text-github-danger{color:var(--color-github-danger)}.text-github-text-muted{color:var(--color-github-text-muted)}.text-github-text-primary{color:var(--color-github-text-primary)}.text-github-text-secondary{color:var(--color-github-text-secondary)}.text-github-warning{color:var(--color-github-warning)}.text-green-600{color:var(--color-green-600)}.text-white{color:var(--color-white)}.lowercase{text-transform:lowercase}.italic{font-style:italic}.line-through{text-decoration-line:line-through}.placeholder-github-text-muted::-moz-placeholder{color:var(--color-github-text-muted)}.placeholder-github-text-muted::placeholder{color:var(--color-github-text-muted)}.accent-github-accent{accent-color:var(--color-github-accent)}.opacity-70{opacity:.7}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-blue-500{--tw-ring-color:var(--color-blue-500)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.\!transition-all{transition-property:all!important;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))!important;transition-duration:var(--tw-duration,var(--default-transition-duration))!important}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.\!duration-300{--tw-duration:.3s!important;transition-duration:.3s!important}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.\!ease-in-out{--tw-ease:var(--ease-in-out)!important;transition-timing-function:var(--ease-in-out)!important}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.select-text{-webkit-user-select:text;-moz-user-select:text;user-select:text}.after\:pointer-events-none:after{content:var(--tw-content);pointer-events:none}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:inset-0:after{content:var(--tw-content);inset:calc(var(--spacing)*0)}.after\:border-t-2:after{content:var(--tw-content);border-top-style:var(--tw-border-style);border-top-width:2px}.after\:border-b-2:after{content:var(--tw-content);border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.after\:border-l-4:after{content:var(--tw-content);border-left-style:var(--tw-border-style);border-left-width:4px}.after\:border-l-5:after{content:var(--tw-content);border-left-style:var(--tw-border-style);border-left-width:5px}.after\:border-blue-500:after{content:var(--tw-content);border-color:var(--color-blue-500)}.after\:border-l-diff-selected-border:after{content:var(--tw-content);border-left-color:var(--color-diff-selected-border)}.after\:bg-blue-100:after{content:var(--tw-content);background-color:var(--color-blue-100)}.after\:bg-diff-selected-bg:after{content:var(--tw-content);background-color:var(--color-diff-selected-bg)}.after\:opacity-30:after{content:var(--tw-content);opacity:.3}@media (hover:hover){.hover\:scale-110:hover{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x)var(--tw-scale-y)}.hover\:border-github-accent\/50:hover{border-color:#23863680}@supports (color:color-mix(in lab,red,red)){.hover\:border-github-accent\/50:hover{border-color:color-mix(in oklab,var(--color-github-accent)50%,transparent)}}.hover\:border-github-text-muted:hover{border-color:var(--color-github-text-muted)}.hover\:border-green-600:hover{border-color:var(--color-green-600)}.hover\:border-orange-900:hover{border-color:var(--color-orange-900)}.hover\:bg-github-bg-primary:hover{background-color:var(--color-github-bg-primary)}.hover\:bg-github-bg-tertiary:hover{background-color:var(--color-github-bg-tertiary)}.hover\:bg-github-text-muted:hover{background-color:var(--color-github-text-muted)}.hover\:bg-green-500\/10:hover{background-color:#00c7581a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-green-500\/10:hover{background-color:color-mix(in oklab,var(--color-green-500)10%,transparent)}}.hover\:bg-green-600:hover{background-color:var(--color-green-600)}.hover\:bg-orange-800:hover{background-color:var(--color-orange-800)}.hover\:text-github-text-primary:hover{color:var(--color-github-text-primary)}.hover\:opacity-80:hover{opacity:.8}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.focus\:min-h-\[80px\]:focus{min-height:80px}.focus\:border-blue-600:focus{border-color:var(--color-blue-600)}.focus\:border-github-accent:focus{border-color:var(--color-github-accent)}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-blue-600\/30:focus{--tw-ring-color:#155dfc4d}@supports (color:color-mix(in lab,red,red)){.focus\:ring-blue-600\/30:focus{--tw-ring-color:color-mix(in oklab,var(--color-blue-600)30%,transparent)}}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:opacity-50:disabled{opacity:.5}@media (min-width:64rem){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (prefers-color-scheme:dark){.dark\:border-slate-500{border-color:var(--color-slate-500)}.dark\:bg-slate-600{background-color:var(--color-slate-600)}.dark\:text-white{color:var(--color-white)}@media (hover:hover){.dark\:hover\:border-slate-400:hover{border-color:var(--color-slate-400)}.dark\:hover\:bg-slate-500:hover{background-color:var(--color-slate-500)}}}.\[\&_code\]\:\!bg-transparent code{background-color:#0000!important}.\[\&_code\]\:text-inherit code{color:inherit}.\[\&_pre\]\:m-0 pre{margin:calc(var(--spacing)*0)}.\[\&_pre\]\:\!bg-transparent pre{background-color:#0000!important}.\[\&_pre\]\:p-0 pre{padding:calc(var(--spacing)*0)}.\[\&_pre\]\:text-inherit pre{color:inherit}}:root{--app-font-size:14px;--app-font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif}html,body{background-color:var(--color-github-bg-primary);color:var(--color-github-text-primary);font-family:var(--app-font-family);line-height:1.5;font-size:var(--app-font-size)}:root{interpolate-size:allow-keywords}button{cursor:pointer}@keyframes sparkle-rise{0%{opacity:0;transform:translateY(20px)scale(.5)}20%{opacity:1;transform:translateY(10px)scale(1)}80%{opacity:1;transform:translateY(-30px)scale(1)}to{opacity:0;transform:translateY(-40px)scale(.8)}}.animate-sparkle-rise{animation:.8s ease-out both sparkle-rise}html,body,.bg-github-bg-primary,.bg-github-bg-secondary,.bg-github-bg-tertiary,[class*=bg-github],[class*=text-github],[class*=border-github],[class*=bg-diff],[class*=border-diff]{transition:background-color .3s,color .3s,border-color .3s}.keyboard-cursor{outline-offset:-2px;outline:2px solid #4d7adb}.word-token{cursor:pointer;transition:background-color .15s;position:relative}.word-highlight{background-color:var(--word-highlight-color,#fffd54);border-radius:2px}[data-theme=dark]{--word-highlight-color:#fffd544d}[data-theme=light]{--word-highlight-color:#fffd54}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@keyframes spin{to{transform:rotate(360deg)}}
|