difit 3.1.17 → 4.0.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 (94) hide show
  1. package/README.ja.md +30 -3
  2. package/README.ko.md +30 -3
  3. package/README.md +30 -3
  4. package/README.zh.md +30 -3
  5. package/dist/cli/github.d.ts +65 -0
  6. package/dist/cli/github.js +296 -0
  7. package/dist/cli/github.test.d.ts +1 -0
  8. package/dist/cli/github.test.js +341 -0
  9. package/dist/cli/index.js +26 -1
  10. package/dist/cli/index.test.js +206 -4
  11. package/dist/cli/utils.d.ts +2 -8
  12. package/dist/cli/utils.js +43 -56
  13. package/dist/cli/utils.test.js +61 -67
  14. package/dist/client/assets/{_basePickBy-r8KiD0PT.js → _basePickBy-B9N-f0iT.js} +1 -1
  15. package/dist/client/assets/{_baseUniq-WYpg9s_f.js → _baseUniq-tbL7nVvN.js} +1 -1
  16. package/dist/client/assets/{arc-BZWd656X.js → arc-BOY-7mep.js} +1 -1
  17. package/dist/client/assets/{architectureDiagram-2XIMDMQ5-BiaoV1Oc.js → architectureDiagram-2XIMDMQ5-59AvHaSB.js} +1 -1
  18. package/dist/client/assets/{blockDiagram-WCTKOSBZ-T1RU4TI6.js → blockDiagram-WCTKOSBZ-DXIlumQk.js} +1 -1
  19. package/dist/client/assets/{c4Diagram-IC4MRINW-C1aQSMsj.js → c4Diagram-IC4MRINW-BbfZ0uRn.js} +1 -1
  20. package/dist/client/assets/channel-cZXsTJxA.js +1 -0
  21. package/dist/client/assets/{chunk-4BX2VUAB-DFcwtPlK.js → chunk-4BX2VUAB-l7rcB2IW.js} +1 -1
  22. package/dist/client/assets/{chunk-55IACEB6-Bl3vvNDx.js → chunk-55IACEB6-CrZL3qv9.js} +1 -1
  23. package/dist/client/assets/{chunk-FMBD7UC4-B_2obFwM.js → chunk-FMBD7UC4-CrKv7ndg.js} +1 -1
  24. package/dist/client/assets/{chunk-JSJVCQXG-BrSq4jyX.js → chunk-JSJVCQXG-DyBDhAEM.js} +1 -1
  25. package/dist/client/assets/{chunk-KX2RTZJC-18m3UONJ.js → chunk-KX2RTZJC-By5mkZmU.js} +1 -1
  26. package/dist/client/assets/{chunk-NQ4KR5QH-hFDbMzZU.js → chunk-NQ4KR5QH-C30p9xRx.js} +1 -1
  27. package/dist/client/assets/{chunk-QZHKN3VN-CyCFXX2j.js → chunk-QZHKN3VN-DVlhR2wU.js} +1 -1
  28. package/dist/client/assets/{chunk-WL4C6EOR-BDdHa7t1.js → chunk-WL4C6EOR-Cn7a6CO3.js} +1 -1
  29. package/dist/client/assets/classDiagram-VBA2DB6C-B_coIPEy.js +1 -0
  30. package/dist/client/assets/classDiagram-v2-RAHNMMFH-B_coIPEy.js +1 -0
  31. package/dist/client/assets/clone-BjaT2HOk.js +1 -0
  32. package/dist/client/assets/{cose-bilkent-S5V4N54A-D7t718Sq.js → cose-bilkent-S5V4N54A-LyauIk_9.js} +1 -1
  33. package/dist/client/assets/{dagre-KLK3FWXG-DJXcjsV8.js → dagre-KLK3FWXG-DRWb2KE3.js} +1 -1
  34. package/dist/client/assets/{diagram-E7M64L7V-DL8ck_Al.js → diagram-E7M64L7V-ChT6mNWK.js} +1 -1
  35. package/dist/client/assets/{diagram-IFDJBPK2-NTxUWyD3.js → diagram-IFDJBPK2-CqbTduoP.js} +1 -1
  36. package/dist/client/assets/{diagram-P4PSJMXO-CGkcnGxk.js → diagram-P4PSJMXO-Bzv5Z3ri.js} +1 -1
  37. package/dist/client/assets/{erDiagram-INFDFZHY-BqpbHQrZ.js → erDiagram-INFDFZHY-CvXfUZ4L.js} +1 -1
  38. package/dist/client/assets/{flowDiagram-PKNHOUZH-B-DK3_9I.js → flowDiagram-PKNHOUZH-CxmpNUKq.js} +1 -1
  39. package/dist/client/assets/{ganttDiagram-A5KZAMGK-BK1C57ll.js → ganttDiagram-A5KZAMGK-9LpZCsg6.js} +1 -1
  40. package/dist/client/assets/{gitGraphDiagram-K3NZZRJ6-Duxlcz8R.js → gitGraphDiagram-K3NZZRJ6-C6yZOrQJ.js} +1 -1
  41. package/dist/client/assets/{graph-C7r58m4O.js → graph-bUZ7uHLW.js} +1 -1
  42. package/dist/client/assets/index-BLNN1bfE.js +98 -0
  43. package/dist/client/assets/index-VxkpzDXr.css +1 -0
  44. package/dist/client/assets/{infoDiagram-LFFYTUFH-Bqt-4V9X.js → infoDiagram-LFFYTUFH-Djdy3W21.js} +1 -1
  45. package/dist/client/assets/{ishikawaDiagram-PHBUUO56-B1ZVSkls.js → ishikawaDiagram-PHBUUO56-oOdwCpeS.js} +1 -1
  46. package/dist/client/assets/{journeyDiagram-4ABVD52K-LSEcxqrO.js → journeyDiagram-4ABVD52K-DTb_nGAw.js} +1 -1
  47. package/dist/client/assets/{kanban-definition-K7BYSVSG-CldPadPs.js → kanban-definition-K7BYSVSG-CMtP7pHA.js} +1 -1
  48. package/dist/client/assets/{layout-NpxIVVkp.js → layout-CXr5MatK.js} +1 -1
  49. package/dist/client/assets/{linear-JpKpxaS-.js → linear-pOMS9pjV.js} +1 -1
  50. package/dist/client/assets/{mermaid.core-gANNEmg0.js → mermaid.core-DV5JJ1Ie.js} +4 -4
  51. package/dist/client/assets/{mindmap-definition-YRQLILUH-ewFI1yc5.js → mindmap-definition-YRQLILUH-DN-sbonc.js} +1 -1
  52. package/dist/client/assets/{pieDiagram-SKSYHLDU-CWlAr2t8.js → pieDiagram-SKSYHLDU-tAHCkgh1.js} +1 -1
  53. package/dist/client/assets/{prism-csharp-CxRfePTX.js → prism-csharp-5CQ0RcEE.js} +1 -1
  54. package/dist/client/assets/{prism-elixir-B0H1PC_E.js → prism-elixir-BSOTyVg2.js} +1 -1
  55. package/dist/client/assets/{prism-hcl-Csmcce-t.js → prism-hcl-BYvi1mtM.js} +1 -1
  56. package/dist/client/assets/{prism-java-BRzwomgj.js → prism-java-DMU2FM4X.js} +1 -1
  57. package/dist/client/assets/{prism-perl-DQMRA6u_.js → prism-perl-CpfvaEQk.js} +1 -1
  58. package/dist/client/assets/{prism-php-C6fR1C7-.js → prism-php-SC920LoD.js} +1 -1
  59. package/dist/client/assets/{prism-ruby-CWeh27h1.js → prism-ruby-DZph-YiO.js} +1 -1
  60. package/dist/client/assets/{prism-solidity-3wCU4ra_.js → prism-solidity-qTLbmiAT.js} +1 -1
  61. package/dist/client/assets/{quadrantDiagram-337W2JSQ-D76E3PCD.js → quadrantDiagram-337W2JSQ-B0wODmgR.js} +1 -1
  62. package/dist/client/assets/{requirementDiagram-Z7DCOOCP-C49LvKzR.js → requirementDiagram-Z7DCOOCP-A3aeHC06.js} +1 -1
  63. package/dist/client/assets/{sankeyDiagram-WA2Y5GQK-DOvEhLMf.js → sankeyDiagram-WA2Y5GQK-BWa6kZhG.js} +1 -1
  64. package/dist/client/assets/{sequenceDiagram-2WXFIKYE-BR6dsfEq.js → sequenceDiagram-2WXFIKYE-Cx_COX9G.js} +1 -1
  65. package/dist/client/assets/{stateDiagram-RAJIS63D-CHII26YE.js → stateDiagram-RAJIS63D-BXGnN6rZ.js} +1 -1
  66. package/dist/client/assets/stateDiagram-v2-FVOUBMTO-CMw3xNha.js +1 -0
  67. package/dist/client/assets/{timeline-definition-YZTLITO2-DhUTiAsW.js → timeline-definition-YZTLITO2-DbqaUm9k.js} +1 -1
  68. package/dist/client/assets/{treemap-KZPCXAKY-C0Rh3R0y.js → treemap-KZPCXAKY-CfEujPCR.js} +1 -1
  69. package/dist/client/assets/{vennDiagram-LZ73GAT5-CWt3wBDG.js → vennDiagram-LZ73GAT5-CqJE8CAD.js} +1 -1
  70. package/dist/client/assets/{xychartDiagram-JWTSCODW-DhwJwxGz.js → xychartDiagram-JWTSCODW-CfdDvzHC.js} +1 -1
  71. package/dist/client/index.html +2 -2
  72. package/dist/server/generated-file-check.js +113 -58
  73. package/dist/server/generated-file-check.test.js +2 -0
  74. package/dist/server/git-diff.d.ts +1 -0
  75. package/dist/server/git-diff.js +33 -6
  76. package/dist/server/git-diff.test.js +45 -0
  77. package/dist/server/server.d.ts +2 -0
  78. package/dist/server/server.js +107 -34
  79. package/dist/server/server.test.js +120 -0
  80. package/dist/types/diff.d.ts +73 -14
  81. package/dist/utils/commentFormatting.d.ts +4 -2
  82. package/dist/utils/commentFormatting.js +57 -19
  83. package/dist/utils/commentImports.d.ts +9 -0
  84. package/dist/utils/commentImports.js +264 -0
  85. package/dist/utils/commentImports.test.d.ts +1 -0
  86. package/dist/utils/commentImports.test.js +197 -0
  87. package/package.json +3 -3
  88. package/dist/client/assets/channel-C081SflL.js +0 -1
  89. package/dist/client/assets/classDiagram-VBA2DB6C-CD8hB8X7.js +0 -1
  90. package/dist/client/assets/classDiagram-v2-RAHNMMFH-CD8hB8X7.js +0 -1
  91. package/dist/client/assets/clone-DL1yO1kL.js +0 -1
  92. package/dist/client/assets/index-DcsVWNsS.css +0 -1
  93. package/dist/client/assets/index-Igyd6olF.js +0 -92
  94. package/dist/client/assets/stateDiagram-v2-FVOUBMTO-DtCFGPiV.js +0 -1
package/README.ja.md CHANGED
@@ -28,9 +28,14 @@ difit # 最新コミットのdiffをWebUIで表示
28
28
  AIエージェントから使えるようにする
29
29
 
30
30
  ```bash
31
- npx skills add yoshiko-pg/difit # エージェントにスキルを追加
31
+ npx skills add yoshiko-pg/difit # エージェントにスキル群を追加
32
32
  ```
33
33
 
34
+ インストールされる主な skill:
35
+
36
+ - `difit`: コード変更後に difit を使ってユーザーへレビューを依頼する
37
+ - `difit-review`: 特定の diff や PR をレビューし、指摘や解説をコメントとして事前投入した difit を起動する
38
+
34
39
  ## 🚀 使い方
35
40
 
36
41
  ### 基本的な使い方
@@ -73,6 +78,7 @@ difit --pr https://github.com/owner/repo/pull/123
73
78
  ```
74
79
 
75
80
  `--pr` モードでは、内部で `gh pr diff --patch` を実行してパッチを取得します。
81
+ 加えて、PR 上の未解決 inline review thread も起動時コメントとして取り込み、difit 上でそのまま表示します。
76
82
 
77
83
  認証は GitHub CLI が処理します:
78
84
 
@@ -86,6 +92,21 @@ Enterprise Server の PR を表示する場合は、GitHub CLI を Enterprise
86
92
  1. `gh auth login --hostname YOUR-ENTERPRISE-SERVER`
87
93
  2. または `GH_HOST=YOUR-ENTERPRISE-SERVER` と `GH_TOKEN` / `GITHUB_TOKEN` を設定
88
94
 
95
+ ### 起動時コメント注入
96
+
97
+ difit の起動時に初期コメントを注入できます。
98
+
99
+ ```bash
100
+ difit --comment '{"type":"thread","filePath":"src/example.ts","position":{"side":"new","line":10},"body":"この変更の背景は…"}'
101
+ ```
102
+
103
+ `--comment` は複数回指定でき、単一の JSON object と JSON array の両方を受け付けます。対応する type は次の 2 種類です。
104
+
105
+ - `thread`: 指定位置に新しい thread を追加
106
+ - `reply`: 同じ位置にある最新 thread に reply を追加
107
+
108
+ 同じコメントが既に存在する場合は import をスキップします。
109
+
89
110
  ### 標準入力
90
111
 
91
112
  パイプを使用して標準入力経由で統一diff形式を渡すことで、任意のツールからのdiffをdifitで表示できます。
@@ -120,6 +141,7 @@ git diff --cached | difit -
120
141
  | `<target>` | HEAD | コミットハッシュ、タグ、HEAD~n、ブランチ、または特別な引数 |
121
142
  | `[compare-with]` | - | 比較対象の2番目のコミット(2つの間のdiffを表示) |
122
143
  | `--pr <url>` | - | レビューするGitHub PRのURL(例:https://github.com/owner/repo/pull/123) |
144
+ | `--comment <json>` | - | 起動時に初期コメントを注入(複数指定可。JSON object または array を受け付ける) |
123
145
  | `--port` | 4966 | 優先ポート。使用中の場合は+1にフォールバック |
124
146
  | `--host` | 127.0.0.1 | サーバーをバインドするホストアドレス(外部からアクセスしたい場合は0.0.0.0を指定) |
125
147
  | `--no-open` | false | ブラウザを自動的に開かない |
@@ -155,13 +177,18 @@ src/components/Button.tsx:L42-L48 # この行が自動的に追加されます
155
177
 
156
178
  ## 🤖 エージェントからの呼び出し
157
179
 
158
- difitを利用してユーザーにレビューを依頼するSkillを以下でインストールできます。
180
+ AIエージェントから difit を使うための skill 群を以下でインストールできます。
159
181
 
160
182
  ```sh
161
183
  npx skills add yoshiko-pg/difit
162
184
  ```
163
185
 
164
- エージェントがコードを編集したあと、difitサーバーを立ち上げるようになります。
186
+ インストールされる主な skill:
187
+
188
+ - `difit`: コード変更後に difit を使ってユーザーへレビューを依頼する
189
+ - `difit-review`: 特定の diff や PR をレビューし、指摘や解説をコメントとして事前投入した difit を起動する
190
+
191
+ コード編集後や自動レビュー時に、目的に応じた skill で difit サーバーを起動できます。
165
192
 
166
193
  ## 🎨 シンタックスハイライト対応言語
167
194
 
package/README.ko.md CHANGED
@@ -28,9 +28,14 @@ difit # WebUI에서 최신 커밋 diff 보기
28
28
  AI 에이전트에서 사용할 수 있도록 설정
29
29
 
30
30
  ```bash
31
- npx skills add yoshiko-pg/difit # 에이전트에 Skill 추가
31
+ npx skills add yoshiko-pg/difit # 에이전트에 Skill 추가
32
32
  ```
33
33
 
34
+ 설치되는 주요 Skill:
35
+
36
+ - `difit`: 코드 변경 후 difit을 통해 사용자에게 리뷰를 요청
37
+ - `difit-review`: 특정 diff 또는 PR을 검토하고, 지적사항이나 설명을 코멘트로 미리 넣은 difit을 실행
38
+
34
39
  ## 🚀 사용법
35
40
 
36
41
  ### 기본 사용법
@@ -73,6 +78,7 @@ difit --pr https://github.com/owner/repo/pull/123
73
78
  ```
74
79
 
75
80
  `--pr` 모드는 내부적으로 `gh pr diff --patch`를 실행해 패치를 가져옵니다.
81
+ 추가로 PR의 unresolved inline review thread도 시작 코멘트로 가져와 difit에서 그대로 표시합니다.
76
82
 
77
83
  인증은 GitHub CLI가 처리합니다:
78
84
 
@@ -86,6 +92,21 @@ Enterprise Server PR의 경우 GitHub CLI를 Enterprise 호스트에 인증하
86
92
  1. `gh auth login --hostname YOUR-ENTERPRISE-SERVER`
87
93
  2. 또는 `GH_HOST=YOUR-ENTERPRISE-SERVER`와 `GH_TOKEN`/`GITHUB_TOKEN` 설정
88
94
 
95
+ ### 시작 시 코멘트 주입
96
+
97
+ difit 실행 시 초기 리뷰 코멘트를 주입할 수 있습니다:
98
+
99
+ ```bash
100
+ difit --comment '{"type":"thread","filePath":"src/example.ts","position":{"side":"new","line":10},"body":"이 변경의 배경은..."}'
101
+ ```
102
+
103
+ `--comment` 는 여러 번 지정할 수 있으며, 단일 JSON object 와 JSON array 둘 다 받을 수 있습니다. 지원하는 타입은 다음과 같습니다:
104
+
105
+ - `thread`: 지정한 diff 위치에 새 thread 생성
106
+ - `reply`: 같은 diff 위치에 있는 가장 최신 thread 에 reply 추가
107
+
108
+ 같은 코멘트가 이미 있으면 difit 이 import 를 건너뜁니다.
109
+
89
110
  ### 표준 입력
90
111
 
91
112
  파이프를 사용하여 표준 입력을 통해 통합 diff를 전달하면 모든 도구의 diff를 difit으로 볼 수 있습니다.
@@ -120,6 +141,7 @@ git diff --cached | difit -
120
141
  | `<target>` | HEAD | 커밋 해시, 태그, HEAD~n, 브랜치 또는 특수 인수 |
121
142
  | `[compare-with]` | - | 비교할 선택적 두 번째 커밋 (둘 사이의 diff 표시) |
122
143
  | `--pr <url>` | - | 검토할 GitHub PR URL (예: https://github.com/owner/repo/pull/123) |
144
+ | `--comment <json>` | - | 초기 코멘트 주입 (반복 가능; JSON object 또는 array 허용) |
123
145
  | `--port` | 4966 | 선호 포트; 사용 중인 경우 +1로 대체 |
124
146
  | `--host` | 127.0.0.1 | 서버를 바인딩할 호스트 주소 (외부 액세스는 0.0.0.0 사용) |
125
147
  | `--no-open` | false | 브라우저를 자동으로 열지 않음 |
@@ -155,13 +177,18 @@ src/components/Button.tsx:L42-L48 # 이 줄은 자동으로 추가됩니다
155
177
 
156
178
  ## 🤖 에이전트에서 호출
157
179
 
158
- difit을 사용해 사용자에게 리뷰를 요청하는 Skill 아래 명령으로 설치할 수 있습니다:
180
+ AI 에이전트에서 difit을 사용하기 위한 Skill들은 아래 명령으로 설치할 수 있습니다:
159
181
 
160
182
  ```sh
161
183
  npx skills add yoshiko-pg/difit
162
184
  ```
163
185
 
164
- 에이전트가 코드를 수정한 뒤에는 difit 서버를 실행합니다.
186
+ 설치되는 주요 Skill:
187
+
188
+ - `difit`: 코드 변경 후 difit을 통해 사용자에게 리뷰를 요청
189
+ - `difit-review`: 특정 diff 또는 PR을 검토하고, 지적사항이나 설명을 코멘트로 미리 넣은 difit을 실행
190
+
191
+ 코드 수정 후나 자동 리뷰 시, 목적에 맞는 Skill로 difit 서버를 실행할 수 있습니다.
165
192
 
166
193
  ## 🎨 구문 강조 언어
167
194
 
package/README.md CHANGED
@@ -28,9 +28,14 @@ difit # View the latest commit diff in WebUI
28
28
  Enable use from AI agents
29
29
 
30
30
  ```bash
31
- npx skills add yoshiko-pg/difit # Add the Skill to your agent
31
+ npx skills add yoshiko-pg/difit # Add the Skills to your agent
32
32
  ```
33
33
 
34
+ Installed skills include:
35
+
36
+ - `difit`: ask the user for a review through difit after code changes
37
+ - `difit-review`: review a specific diff or PR and launch difit with findings or explanations preloaded as comments
38
+
34
39
  ## 🚀 Usage
35
40
 
36
41
  ### Basic Usage
@@ -73,6 +78,7 @@ difit --pr https://github.com/owner/repo/pull/123
73
78
  ```
74
79
 
75
80
  `--pr` mode fetches patches by running `gh pr diff --patch` under the hood.
81
+ It also imports unresolved inline review threads from the PR so they appear as startup comments in difit.
76
82
 
77
83
  Authentication is handled by GitHub CLI:
78
84
 
@@ -86,6 +92,21 @@ For Enterprise Server PRs, authenticate GitHub CLI against your Enterprise host:
86
92
  1. `gh auth login --hostname YOUR-ENTERPRISE-SERVER`
87
93
  2. Or set `GH_HOST=YOUR-ENTERPRISE-SERVER` with `GH_TOKEN`/`GITHUB_TOKEN`
88
94
 
95
+ ### Initial Comments
96
+
97
+ You can inject initial review comments when launching difit:
98
+
99
+ ```bash
100
+ difit --comment '{"type":"thread","filePath":"src/example.ts","position":{"side":"new","line":10},"body":"The background for this change is..."}'
101
+ ```
102
+
103
+ `--comment` is repeatable and accepts either a single JSON object or a JSON array. Supported types:
104
+
105
+ - `thread`: create a new thread at the specified diff position
106
+ - `reply`: add a reply to the latest existing thread at the same diff position
107
+
108
+ If the same comment already exists, difit skips importing it.
109
+
89
110
  ### Stdin
90
111
 
91
112
  By using a pipe to pass unified diffs via stdin, you can view diffs from any tool with difit.
@@ -120,6 +141,7 @@ Stdin mode is selected with intent-first rules:
120
141
  | `<target>` | HEAD | Commit hash, tag, HEAD~n, branch, or special arguments |
121
142
  | `[compare-with]` | - | Optional second commit to compare with (shows diff between the two) |
122
143
  | `--pr <url>` | - | GitHub PR URL to review (e.g., https://github.com/owner/repo/pull/123) |
144
+ | `--comment <json>` | - | Inject initial comments (repeatable; accepts a JSON object or array) |
123
145
  | `--port` | 4966 | Preferred port; falls back to +1 if occupied |
124
146
  | `--host` | 127.0.0.1 | Host address to bind server to (use 0.0.0.0 for external access) |
125
147
  | `--no-open` | false | Don't automatically open browser |
@@ -155,13 +177,18 @@ This section is unnecessary
155
177
 
156
178
  ## 🤖 Calling from Agents
157
179
 
158
- You can install the following Skill to request reviews from users with difit.
180
+ You can install the following Skills to work with difit from AI agents.
159
181
 
160
182
  ```sh
161
183
  npx skills add yoshiko-pg/difit
162
184
  ```
163
185
 
164
- After an agent edits code, it will start the difit server.
186
+ Installed skills include:
187
+
188
+ - `difit`: ask the user for a review through difit after code changes
189
+ - `difit-review`: review a specific diff or PR and launch difit with findings or explanations preloaded as comments
190
+
191
+ After code edits or automated review, the agent can start the difit server with the appropriate skill.
165
192
 
166
193
  ## 🎨 Syntax Highlighting Languages
167
194
 
package/README.zh.md CHANGED
@@ -28,9 +28,14 @@ difit # 在 WebUI 中查看最新提交的差异
28
28
  使其可供 AI 代理使用
29
29
 
30
30
  ```bash
31
- npx skills add yoshiko-pg/difit # 为代理添加 Skill
31
+ npx skills add yoshiko-pg/difit # 为代理添加 Skills
32
32
  ```
33
33
 
34
+ 安装后包含的主要 Skill:
35
+
36
+ - `difit`:在代码修改后通过 difit 向用户请求审查
37
+ - `difit-review`:审查特定 diff 或 PR,并启动一个已预载评论或说明的 difit
38
+
34
39
  ## 🚀 使用方法
35
40
 
36
41
  ### 基本用法
@@ -73,6 +78,7 @@ difit --pr https://github.com/owner/repo/pull/123
73
78
  ```
74
79
 
75
80
  `--pr` 模式会在内部执行 `gh pr diff --patch` 来获取补丁。
81
+ 同时还会导入 PR 中未解决的 inline review thread,并在 difit 中作为启动评论显示出来。
76
82
 
77
83
  认证由 GitHub CLI 处理:
78
84
 
@@ -86,6 +92,21 @@ difit --pr https://github.com/owner/repo/pull/123
86
92
  1. `gh auth login --hostname YOUR-ENTERPRISE-SERVER`
87
93
  2. 或设置 `GH_HOST=YOUR-ENTERPRISE-SERVER`,并配置 `GH_TOKEN` / `GITHUB_TOKEN`
88
94
 
95
+ ### 启动时注入评论
96
+
97
+ 你可以在启动 difit 时注入初始审查评论:
98
+
99
+ ```bash
100
+ difit --comment '{"type":"thread","filePath":"src/example.ts","position":{"side":"new","line":10},"body":"这次修改的背景是……"}'
101
+ ```
102
+
103
+ `--comment` 可重复指定,同时接受单个 JSON object 或 JSON array。支持的类型如下:
104
+
105
+ - `thread`:在指定 diff 位置创建新的 thread
106
+ - `reply`:向相同 diff 位置上最新的现有 thread 添加 reply
107
+
108
+ 如果相同评论已经存在,difit 会跳过导入。
109
+
89
110
  ### 标准输入
90
111
 
91
112
  通过使用管道通过标准输入传递统一差异,您可以使用 difit 查看来自任何工具的差异。
@@ -120,6 +141,7 @@ git diff --cached | difit -
120
141
  | `<target>` | HEAD | 提交哈希、标签、HEAD~n、分支或特殊参数 |
121
142
  | `[compare-with]` | - | 要比较的可选第二个提交(显示两者之间的差异) |
122
143
  | `--pr <url>` | - | 要审查的 GitHub PR URL(例如:https://github.com/owner/repo/pull/123) |
144
+ | `--comment <json>` | - | 注入初始评论(可重复指定;接受 JSON object 或 array) |
123
145
  | `--port` | 4966 | 首选端口;如果被占用则回退到 +1 |
124
146
  | `--host` | 127.0.0.1 | 绑定服务器的主机地址(使用 0.0.0.0 进行外部访问) |
125
147
  | `--no-open` | false | 不自动打开浏览器 |
@@ -155,13 +177,18 @@ src/components/Button.tsx:L42-L48 # 此行自动添加
155
177
 
156
178
  ## 🤖 从代理调用
157
179
 
158
- 你可以通过以下命令安装 Skill,以便使用 difit 向用户请求审查:
180
+ 你可以通过以下命令安装这些 Skill,以便从 AI 代理中使用 difit
159
181
 
160
182
  ```sh
161
183
  npx skills add yoshiko-pg/difit
162
184
  ```
163
185
 
164
- 代理编辑代码后,将会启动 difit 服务器。
186
+ 安装后包含的主要 Skill:
187
+
188
+ - `difit`:在代码修改后通过 difit 向用户请求审查
189
+ - `difit-review`:审查特定 diff 或 PR,并启动一个已预载评论或说明的 difit
190
+
191
+ 在代码修改后或自动审查时,代理可以根据目的使用对应的 Skill 来启动 difit 服务器。
165
192
 
166
193
  ## 🎨 语法高亮语言
167
194
 
@@ -0,0 +1,65 @@
1
+ import type { CommentImport } from '../types/diff.js';
2
+ interface PullRequestInfo {
3
+ owner: string;
4
+ repo: string;
5
+ pullNumber: number;
6
+ hostname: string;
7
+ }
8
+ interface GitHubReviewThreadAuthor {
9
+ login?: string | null;
10
+ }
11
+ interface GitHubReviewThreadCommentNode {
12
+ id?: string | null;
13
+ body?: string | null;
14
+ createdAt?: string | null;
15
+ updatedAt?: string | null;
16
+ author?: GitHubReviewThreadAuthor | null;
17
+ }
18
+ interface GitHubReviewThreadNode {
19
+ id?: string | null;
20
+ isResolved?: boolean | null;
21
+ isOutdated?: boolean | null;
22
+ subjectType?: string | null;
23
+ path?: string | null;
24
+ diffSide?: string | null;
25
+ startDiffSide?: string | null;
26
+ line?: number | null;
27
+ startLine?: number | null;
28
+ originalLine?: number | null;
29
+ originalStartLine?: number | null;
30
+ comments?: {
31
+ nodes?: GitHubReviewThreadCommentNode[] | null;
32
+ } | null;
33
+ }
34
+ interface GitHubReviewThreadsPageInfo {
35
+ hasNextPage?: boolean | null;
36
+ endCursor?: string | null;
37
+ }
38
+ interface GitHubReviewThreadsConnection {
39
+ nodes?: GitHubReviewThreadNode[] | null;
40
+ pageInfo?: GitHubReviewThreadsPageInfo | null;
41
+ }
42
+ interface GitHubReviewThreadsGraphqlResponse {
43
+ data?: {
44
+ repository?: {
45
+ pullRequest?: {
46
+ reviewThreads?: GitHubReviewThreadsConnection | null;
47
+ } | null;
48
+ } | null;
49
+ } | null;
50
+ errors?: Array<{
51
+ message?: string | null;
52
+ }> | null;
53
+ }
54
+ interface PrCommentImportsPage {
55
+ commentImports: CommentImport[];
56
+ pageInfo: {
57
+ hasNextPage: boolean;
58
+ endCursor: string | null;
59
+ };
60
+ }
61
+ export declare function parseGitHubPrUrl(url: string): PullRequestInfo | null;
62
+ export declare function getPrPatch(prArg: string): string;
63
+ export declare function parsePrCommentImportsResponse(response: GitHubReviewThreadsGraphqlResponse): PrCommentImportsPage;
64
+ export declare function getPrCommentImports(prArg: string): Promise<CommentImport[]>;
65
+ export {};
@@ -0,0 +1,296 @@
1
+ import { execFileSync } from 'child_process';
2
+ const PR_REVIEW_THREADS_GRAPHQL_QUERY = `
3
+ query($owner: String!, $repo: String!, $number: Int!, $endCursor: String) {
4
+ repository(owner: $owner, name: $repo) {
5
+ pullRequest(number: $number) {
6
+ reviewThreads(first: 100, after: $endCursor) {
7
+ nodes {
8
+ id
9
+ isResolved
10
+ isOutdated
11
+ subjectType
12
+ path
13
+ diffSide
14
+ startDiffSide
15
+ line
16
+ startLine
17
+ originalLine
18
+ originalStartLine
19
+ comments(first: 100) {
20
+ nodes {
21
+ id
22
+ body
23
+ createdAt
24
+ updatedAt
25
+ author {
26
+ login
27
+ }
28
+ }
29
+ }
30
+ }
31
+ pageInfo {
32
+ hasNextPage
33
+ endCursor
34
+ }
35
+ }
36
+ }
37
+ }
38
+ }
39
+ `;
40
+ function isPositiveInteger(value) {
41
+ return typeof value === 'number' && Number.isInteger(value) && value > 0;
42
+ }
43
+ function formatGhCommandError(error) {
44
+ const stderr = error.stderr;
45
+ const stderrText = typeof stderr === 'string'
46
+ ? stderr.trim()
47
+ : Buffer.isBuffer(stderr)
48
+ ? stderr.toString('utf8').trim()
49
+ : '';
50
+ const message = stderrText || (error instanceof Error ? error.message : 'Unknown error while running gh');
51
+ return new Error(`${message}\nTry: gh auth login`);
52
+ }
53
+ function warnPrCommentImport(threadId, message) {
54
+ const threadLabel = threadId ? ` ${threadId}` : '';
55
+ console.warn(`Warning: Skipping PR review thread${threadLabel}: ${message}`);
56
+ }
57
+ function createSingleLinePosition(side, line) {
58
+ return { side, line };
59
+ }
60
+ function createMultiLinePosition(side, start, end) {
61
+ const line = { start, end };
62
+ return { side, line };
63
+ }
64
+ function createRightSidePosition(thread) {
65
+ if (!isPositiveInteger(thread.line)) {
66
+ warnPrCommentImport(thread.id ?? undefined, 'RIGHT thread is missing line.');
67
+ return null;
68
+ }
69
+ const startLine = thread.startLine !== undefined && thread.startLine !== null ? thread.startLine : null;
70
+ const isSingleLineWithRedundantStart = startLine !== null && startLine === thread.line && thread.startDiffSide == null;
71
+ const hasMultiLineMetadata = thread.startDiffSide !== undefined && thread.startDiffSide !== null
72
+ ? true
73
+ : startLine !== null && !isSingleLineWithRedundantStart;
74
+ if (!hasMultiLineMetadata) {
75
+ return createSingleLinePosition('new', thread.line);
76
+ }
77
+ if (thread.startDiffSide !== 'RIGHT') {
78
+ warnPrCommentImport(thread.id ?? undefined, 'RIGHT thread has mismatched startDiffSide.');
79
+ return null;
80
+ }
81
+ if (!isPositiveInteger(startLine) || startLine > thread.line) {
82
+ warnPrCommentImport(thread.id ?? undefined, 'RIGHT thread has an invalid multi-line range.');
83
+ return null;
84
+ }
85
+ return createMultiLinePosition('new', startLine, thread.line);
86
+ }
87
+ function createLeftSidePosition(thread) {
88
+ if (!isPositiveInteger(thread.originalLine)) {
89
+ warnPrCommentImport(thread.id ?? undefined, 'LEFT thread is missing originalLine.');
90
+ return null;
91
+ }
92
+ const originalStartLine = thread.originalStartLine !== undefined && thread.originalStartLine !== null
93
+ ? thread.originalStartLine
94
+ : null;
95
+ const isSingleLineWithRedundantStart = originalStartLine !== null &&
96
+ originalStartLine === thread.originalLine &&
97
+ thread.startDiffSide == null;
98
+ const hasMultiLineMetadata = thread.startDiffSide !== undefined && thread.startDiffSide !== null
99
+ ? true
100
+ : originalStartLine !== null && !isSingleLineWithRedundantStart;
101
+ if (!hasMultiLineMetadata) {
102
+ return createSingleLinePosition('old', thread.originalLine);
103
+ }
104
+ if (thread.startDiffSide !== 'LEFT') {
105
+ warnPrCommentImport(thread.id ?? undefined, 'LEFT thread has mismatched startDiffSide.');
106
+ return null;
107
+ }
108
+ if (!isPositiveInteger(originalStartLine) || originalStartLine > thread.originalLine) {
109
+ warnPrCommentImport(thread.id ?? undefined, 'LEFT thread has an invalid multi-line range.');
110
+ return null;
111
+ }
112
+ return createMultiLinePosition('old', originalStartLine, thread.originalLine);
113
+ }
114
+ function getThreadPosition(thread) {
115
+ if (thread.diffSide === 'RIGHT') {
116
+ return createRightSidePosition(thread);
117
+ }
118
+ if (thread.diffSide === 'LEFT') {
119
+ return createLeftSidePosition(thread);
120
+ }
121
+ warnPrCommentImport(thread.id ?? undefined, 'Unsupported diffSide.');
122
+ return null;
123
+ }
124
+ function toSortedComments(thread) {
125
+ return [...(thread.comments?.nodes ?? [])].sort((left, right) => (left.createdAt ?? '').localeCompare(right.createdAt ?? ''));
126
+ }
127
+ function toCommentImportsForThread(thread) {
128
+ if (thread.isResolved || thread.isOutdated || thread.subjectType !== 'LINE') {
129
+ return [];
130
+ }
131
+ if (typeof thread.path !== 'string' || thread.path.trim().length === 0) {
132
+ warnPrCommentImport(thread.id ?? undefined, 'Thread is missing path.');
133
+ return [];
134
+ }
135
+ const filePath = thread.path;
136
+ const position = getThreadPosition(thread);
137
+ if (!position) {
138
+ return [];
139
+ }
140
+ const comments = toSortedComments(thread);
141
+ if (comments.length === 0) {
142
+ warnPrCommentImport(thread.id ?? undefined, 'Thread has no comments.');
143
+ return [];
144
+ }
145
+ const rootComment = comments[0];
146
+ if (!rootComment ||
147
+ typeof rootComment.id !== 'string' ||
148
+ typeof rootComment.body !== 'string' ||
149
+ typeof rootComment.createdAt !== 'string' ||
150
+ typeof rootComment.updatedAt !== 'string') {
151
+ warnPrCommentImport(thread.id ?? undefined, 'Thread has a comment with missing fields.');
152
+ return [];
153
+ }
154
+ const commentImports = [
155
+ {
156
+ type: 'thread',
157
+ id: rootComment.id,
158
+ filePath,
159
+ position,
160
+ body: rootComment.body,
161
+ author: rootComment.author?.login ?? undefined,
162
+ createdAt: rootComment.createdAt,
163
+ updatedAt: rootComment.updatedAt,
164
+ },
165
+ ];
166
+ for (const comment of comments.slice(1)) {
167
+ if (typeof comment.id !== 'string' ||
168
+ typeof comment.body !== 'string' ||
169
+ typeof comment.createdAt !== 'string' ||
170
+ typeof comment.updatedAt !== 'string') {
171
+ warnPrCommentImport(thread.id ?? undefined, 'Thread has a comment with missing fields.');
172
+ continue;
173
+ }
174
+ commentImports.push({
175
+ type: 'reply',
176
+ id: comment.id,
177
+ filePath,
178
+ position,
179
+ body: comment.body,
180
+ author: comment.author?.login ?? undefined,
181
+ createdAt: comment.createdAt,
182
+ updatedAt: comment.updatedAt,
183
+ });
184
+ }
185
+ return commentImports;
186
+ }
187
+ export function parseGitHubPrUrl(url) {
188
+ try {
189
+ const urlObj = new URL(url);
190
+ const pathParts = urlObj.pathname.split('/').filter(Boolean);
191
+ if (pathParts.length < 4 || pathParts[2] !== 'pull') {
192
+ return null;
193
+ }
194
+ const owner = pathParts[0];
195
+ const repo = pathParts[1];
196
+ const pullNumber = parseInt(pathParts[3], 10);
197
+ if (isNaN(pullNumber)) {
198
+ return null;
199
+ }
200
+ return { owner, repo, pullNumber, hostname: urlObj.hostname };
201
+ }
202
+ catch {
203
+ return null;
204
+ }
205
+ }
206
+ export function getPrPatch(prArg) {
207
+ try {
208
+ const patch = execFileSync('gh', ['pr', 'diff', prArg], {
209
+ encoding: 'utf8',
210
+ stdio: ['ignore', 'pipe', 'pipe'],
211
+ });
212
+ if (!patch.trim()) {
213
+ throw new Error('No diff content returned from gh pr diff');
214
+ }
215
+ return patch;
216
+ }
217
+ catch (error) {
218
+ throw formatGhCommandError(error);
219
+ }
220
+ }
221
+ export function parsePrCommentImportsResponse(response) {
222
+ const errors = response.errors?.map((error) => error.message).filter((message) => message) ?? [];
223
+ if (errors.length > 0) {
224
+ throw new Error(errors.join('\n'));
225
+ }
226
+ const reviewThreads = response.data?.repository?.pullRequest?.reviewThreads;
227
+ if (!reviewThreads) {
228
+ return {
229
+ commentImports: [],
230
+ pageInfo: {
231
+ hasNextPage: false,
232
+ endCursor: null,
233
+ },
234
+ };
235
+ }
236
+ return {
237
+ commentImports: (reviewThreads.nodes ?? []).flatMap((thread) => toCommentImportsForThread(thread)),
238
+ pageInfo: {
239
+ hasNextPage: reviewThreads.pageInfo?.hasNextPage === true,
240
+ endCursor: reviewThreads.pageInfo?.endCursor ?? null,
241
+ },
242
+ };
243
+ }
244
+ export function getPrCommentImports(prArg) {
245
+ const pullRequestInfo = parseGitHubPrUrl(prArg);
246
+ if (!pullRequestInfo) {
247
+ throw new Error('Invalid GitHub PR URL');
248
+ }
249
+ const { owner, repo, pullNumber, hostname } = pullRequestInfo;
250
+ const commentImports = [];
251
+ let endCursor = null;
252
+ while (true) {
253
+ try {
254
+ const args = [
255
+ 'api',
256
+ 'graphql',
257
+ '--hostname',
258
+ hostname,
259
+ '-f',
260
+ `query=${PR_REVIEW_THREADS_GRAPHQL_QUERY}`,
261
+ '-F',
262
+ `owner=${owner}`,
263
+ '-F',
264
+ `repo=${repo}`,
265
+ '-F',
266
+ `number=${pullNumber}`,
267
+ ];
268
+ if (endCursor) {
269
+ args.push('-F', `endCursor=${endCursor}`);
270
+ }
271
+ const stdout = execFileSync('gh', args, {
272
+ encoding: 'utf8',
273
+ stdio: ['ignore', 'pipe', 'pipe'],
274
+ });
275
+ const parsed = JSON.parse(stdout);
276
+ const page = parsePrCommentImportsResponse(parsed);
277
+ commentImports.push(...page.commentImports);
278
+ if (!page.pageInfo.hasNextPage) {
279
+ return Promise.resolve(commentImports);
280
+ }
281
+ if (!page.pageInfo.endCursor) {
282
+ throw new Error('GitHub GraphQL response indicated more pages without endCursor.');
283
+ }
284
+ endCursor = page.pageInfo.endCursor;
285
+ }
286
+ catch (error) {
287
+ if (error instanceof SyntaxError) {
288
+ throw new Error('Invalid JSON returned from gh api graphql');
289
+ }
290
+ if (error instanceof Error && !('stderr' in error)) {
291
+ throw error;
292
+ }
293
+ throw formatGhCommandError(error);
294
+ }
295
+ }
296
+ }
@@ -0,0 +1 @@
1
+ export {};