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.
Files changed (32) hide show
  1. package/README.ja.md +192 -0
  2. package/README.ko.md +192 -0
  3. package/README.md +61 -44
  4. package/README.zh.md +192 -0
  5. package/dist/cli/index.js +50 -0
  6. package/dist/cli/index.test.js +141 -12
  7. package/dist/cli/utils.js +18 -3
  8. package/dist/cli/utils.test.js +10 -1
  9. package/dist/client/assets/index-BtavrLIu.css +1 -0
  10. package/dist/client/assets/index-Bx2n4Aep.js +210 -0
  11. package/dist/client/assets/{prism-csharp-68c6WkNx.js → prism-csharp-BTkEzOdP.js} +1 -1
  12. package/dist/client/assets/{prism-java-C8EIlB8E.js → prism-java-B6gV82l4.js} +1 -1
  13. package/dist/client/assets/{prism-php-DHZyM8JV.js → prism-php-gnpy0VQF.js} +1 -1
  14. package/dist/client/assets/prism-protobuf-DiQ_z8B5.js +1 -0
  15. package/dist/client/assets/{prism-ruby-MnFNFfyf.js → prism-ruby-CMkpRodx.js} +1 -1
  16. package/dist/client/assets/{prism-solidity-CIeB0O-m.js → prism-solidity-BDXCWkss.js} +1 -1
  17. package/dist/client/index.html +2 -2
  18. package/dist/server/file-watcher.d.ts +23 -0
  19. package/dist/server/file-watcher.js +236 -0
  20. package/dist/server/file-watcher.test.d.ts +1 -0
  21. package/dist/server/file-watcher.test.js +225 -0
  22. package/dist/server/git-diff.d.ts +2 -0
  23. package/dist/server/git-diff.js +47 -4
  24. package/dist/server/git-diff.test.js +209 -0
  25. package/dist/server/server.d.ts +5 -2
  26. package/dist/server/server.js +66 -16
  27. package/dist/server/server.test.js +3 -3
  28. package/dist/types/watch.d.ts +30 -0
  29. package/dist/types/watch.js +8 -0
  30. package/package.json +3 -2
  31. package/dist/client/assets/index-DMBW6MaM.css +0 -1
  32. 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) {
@@ -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: 3000,
41
- url: 'http://localhost:3000',
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: 3000,
425
- url: 'http://localhost:3000',
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: 3000,
465
- url: 'http://localhost:3000',
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: 3000,
507
- url: 'http://localhost:3000',
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:3000');
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: 3000,
550
- url: 'http://localhost:3000',
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:3000 if you want to check manually.\n');
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
- if (name === '@')
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
- const authHint = token ? '' : ' (Try: gh auth login or set GITHUB_TOKEN environment variable)';
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');
@@ -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(false); // cannot be just @
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)}}