@vintasoftware/pr-review-canvas 0.2.0 → 0.4.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 (138) hide show
  1. package/README.md +61 -26
  2. package/docs/reference.md +231 -121
  3. package/package.json +18 -4
  4. package/pr-review.config.example.yml +10 -4
  5. package/prompts/chat-seed.md +3 -0
  6. package/prompts/generation-format.md +3 -0
  7. package/skills/pr-review-canvas/SKILL.md +77 -40
  8. package/src/acpx/acpx.ts +21 -4
  9. package/src/acpx/events.ts +5 -1
  10. package/src/acpx/ndjson.ts +3 -1
  11. package/src/acpx/preflight.ts +5 -1
  12. package/src/canvas/comment.ts +24 -0
  13. package/src/canvas/export.ts +11 -2
  14. package/src/canvas/import.ts +30 -9
  15. package/src/canvas/name.ts +1 -0
  16. package/src/canvas/zip.ts +25 -2
  17. package/src/chat/chat-manager.ts +55 -41
  18. package/src/chat/context.ts +10 -3
  19. package/src/chat/seed.ts +3 -1
  20. package/src/chat/threads.ts +27 -26
  21. package/src/cli.ts +24 -9
  22. package/src/commands.ts +86 -25
  23. package/src/config.ts +24 -24
  24. package/src/contract/api.ts +32 -1
  25. package/src/contract/canvas-manifest.ts +2 -0
  26. package/src/contract/comments.ts +8 -1
  27. package/src/contract/discovery.ts +5 -2
  28. package/src/contract/generation-context.ts +27 -2
  29. package/src/contract/review-artifact.ts +14 -8
  30. package/src/contract/review-key.ts +51 -0
  31. package/src/contract/reviews.ts +17 -0
  32. package/src/contract/settings.ts +2 -0
  33. package/src/contract/state.ts +41 -19
  34. package/src/git/diff-collector.ts +2 -1
  35. package/src/git/environment.mjs +27 -0
  36. package/src/git/git.ts +117 -12
  37. package/src/git/local-target.ts +138 -0
  38. package/src/git/patch-lines.ts +34 -2
  39. package/src/git/pr-refs.ts +36 -0
  40. package/src/github/attachments.ts +9 -257
  41. package/src/github/canvas-comment.ts +22 -0
  42. package/src/github/capabilities.ts +3 -37
  43. package/src/github/comments.ts +9 -27
  44. package/src/github/post-comment.ts +7 -37
  45. package/src/github/post-review.ts +4 -19
  46. package/src/github/pr.ts +6 -84
  47. package/src/github/threads.ts +6 -2
  48. package/src/gitlab/attachments.ts +40 -0
  49. package/src/gitlab/canvas-comment.ts +26 -0
  50. package/src/gitlab/capabilities.ts +64 -0
  51. package/src/gitlab/comments.ts +164 -0
  52. package/src/gitlab/mr.ts +115 -0
  53. package/src/gitlab/post-comment.ts +111 -0
  54. package/src/gitlab/post-review.ts +54 -0
  55. package/src/gitlab/project.ts +13 -0
  56. package/src/host/attachments.ts +293 -0
  57. package/src/host/capabilities.ts +38 -0
  58. package/src/host/client.ts +245 -0
  59. package/src/host/host.ts +136 -0
  60. package/src/host/pr.ts +51 -0
  61. package/src/host/remote.ts +42 -0
  62. package/src/project-config.ts +25 -9
  63. package/src/prompt-files.ts +4 -3
  64. package/src/review/carry-over.ts +79 -0
  65. package/src/review/doctor.ts +31 -14
  66. package/src/review/normalize.ts +3 -1
  67. package/src/review/prepare.ts +87 -17
  68. package/src/review/prompt.ts +8 -2
  69. package/src/review/publish.ts +76 -13
  70. package/src/{github → review}/review-body.ts +17 -5
  71. package/src/review/skill-command.ts +5 -3
  72. package/src/review/trim-caps.ts +10 -6
  73. package/src/review/validate-folds.ts +2 -2
  74. package/src/review/validate.ts +67 -15
  75. package/src/server/app.ts +12 -4
  76. package/src/server/bundle.ts +320 -106
  77. package/src/server/context.ts +24 -10
  78. package/src/server/errors.ts +56 -10
  79. package/src/server/html.ts +36 -12
  80. package/src/server/node-server.ts +4 -2
  81. package/src/server/routes/api.ts +92 -31
  82. package/src/server/routes/chat-routes.ts +105 -46
  83. package/src/server/routes/pages.ts +25 -9
  84. package/src/server/routes/review-routes.ts +101 -35
  85. package/src/server/sse.ts +3 -1
  86. package/src/store/atomic-json.ts +5 -1
  87. package/src/store/canvas-store.ts +95 -46
  88. package/src/store/data-dir.ts +2 -1
  89. package/src/store/derived-store.ts +41 -23
  90. package/src/store/pr-store.ts +25 -15
  91. package/src/store/state-store.ts +42 -37
  92. package/static/brand.svg +19 -0
  93. package/static/js/api.js +47 -27
  94. package/static/js/app.js +27 -7
  95. package/static/js/chat-panel.js +93 -0
  96. package/static/js/chat.js +41 -17
  97. package/static/js/composer.js +30 -17
  98. package/static/js/contract-types.d.ts +3 -0
  99. package/static/js/diagram.js +2 -1
  100. package/static/js/diff-decorations.js +5 -3
  101. package/static/js/diff-renderer.js +7 -2
  102. package/static/js/dom.js +5 -7
  103. package/static/js/download.js +1 -1
  104. package/static/js/empty-state.js +85 -16
  105. package/static/js/errors.js +22 -6
  106. package/static/js/header.js +32 -9
  107. package/static/js/host.js +40 -0
  108. package/static/js/import-zone.js +1 -1
  109. package/static/js/interactions.js +56 -17
  110. package/static/js/keyboard.js +7 -2
  111. package/static/js/layers.js +26 -14
  112. package/static/js/links.js +9 -3
  113. package/static/js/markdown.js +28 -1
  114. package/static/js/nav.js +5 -2
  115. package/static/js/overview.js +32 -4
  116. package/static/js/points.js +5 -3
  117. package/static/js/progress.js +2 -1
  118. package/static/js/proposed-comment.js +4 -1
  119. package/static/js/quick-questions.js +2 -1
  120. package/static/js/regenerate.js +4 -1
  121. package/static/js/review-session.js +7 -2
  122. package/static/js/settings.js +2 -1
  123. package/static/js/signoff.js +9 -6
  124. package/static/styles/base.css +16 -6
  125. package/static/styles/chat-panel.css +81 -0
  126. package/static/styles/chat-tools.css +28 -0
  127. package/static/styles/chat.css +1 -1
  128. package/static/styles/commands.css +10 -4
  129. package/static/styles/diff.css +1 -1
  130. package/static/styles/header.css +18 -4
  131. package/static/styles/layout.css +4 -4
  132. package/static/styles/panels.css +4 -0
  133. package/static/styles/responsive.css +1 -15
  134. package/static/styles/review-actions.css +1 -0
  135. package/static/styles/review.css +24 -3
  136. package/static/styles/skin-github.css +99 -100
  137. package/static/styles.css +13 -12
  138. package/src/github/gh.ts +0 -196
@@ -9,7 +9,7 @@
9
9
 
10
10
  Dark is GitHub's "dark dimmed": a softer grey-blue ground rather than the near-black one. */
11
11
 
12
- :root[data-skin="github"] {
12
+ :root[data-skin='github'] {
13
13
  --r: 6px;
14
14
  --gap: 16px;
15
15
  --main-pad: 16px 16px 40px;
@@ -70,45 +70,38 @@
70
70
  }
71
71
 
72
72
  /* ---- Header ---- */
73
- [data-skin="github"] .hdr-bar {
73
+ [data-skin='github'] .hdr-bar {
74
74
  background: light-dark(#ffffff, #151b23);
75
75
  }
76
76
 
77
- [data-skin="github"] .brand {
77
+ [data-skin='github'] .brand {
78
78
  text-transform: none;
79
79
  letter-spacing: normal;
80
80
  font-size: 14px;
81
- align-items: center;
82
- }
83
- [data-skin="github"] .brand .box {
84
- border-width: 1px;
85
- border-radius: var(--r);
86
- background: var(--panel-2);
87
- padding: 3px 8px;
88
81
  }
89
82
  /* The rainbow bar becomes the hairline that runs under GitHub's page header. */
90
- [data-skin="github"] .stripe {
83
+ [data-skin='github'] .stripe {
91
84
  height: 1px;
92
85
  background: var(--ink);
93
86
  }
94
- [data-skin="github"] .hdr-actions {
87
+ [data-skin='github'] .hdr-actions {
95
88
  gap: 8px;
96
89
  align-items: center;
97
90
  }
98
- [data-skin="github"] .hdr-title {
91
+ [data-skin='github'] .hdr-title {
99
92
  padding-bottom: 16px;
100
93
  }
101
94
 
102
95
  /* ---- Commands ---- */
103
- [data-skin="github"] .cmd::before,
104
- [data-skin="github"] .cmd::after {
96
+ [data-skin='github'] .cmd::before,
97
+ [data-skin='github'] .cmd::after {
105
98
  content: none;
106
99
  }
107
- [data-skin="github"] .cmd {
100
+ [data-skin='github'] .cmd {
108
101
  font-weight: 500;
109
102
  }
110
103
  /* The header's commands read as GitHub's toolbar buttons; commands inside the page stay links. */
111
- [data-skin="github"] .hdr-actions .cmd {
104
+ [data-skin='github'] .hdr-actions .cmd {
112
105
  padding: 3px 12px;
113
106
  border: 1px solid var(--line);
114
107
  border-radius: var(--r);
@@ -117,16 +110,16 @@
117
110
  font-size: 12px;
118
111
  line-height: 20px;
119
112
  }
120
- [data-skin="github"] .hdr-actions .cmd:hover {
113
+ [data-skin='github'] .hdr-actions .cmd:hover {
121
114
  background: var(--btn-bg-hover);
122
115
  text-decoration: none;
123
116
  }
124
- [data-skin="github"] .hdr-actions .cmd:disabled,
125
- [data-skin="github"] .hdr-actions .cmd[aria-disabled="true"] {
117
+ [data-skin='github'] .hdr-actions .cmd:disabled,
118
+ [data-skin='github'] .hdr-actions .cmd[aria-disabled='true'] {
126
119
  color: var(--fg-dim);
127
120
  }
128
- [data-skin="github"] .cmd.fill,
129
- [data-skin="github"] .cmd.big {
121
+ [data-skin='github'] .cmd.fill,
122
+ [data-skin='github'] .cmd.big {
130
123
  border: 1px solid transparent;
131
124
  border-radius: var(--r);
132
125
  background: var(--btn-primary);
@@ -134,56 +127,62 @@
134
127
  box-shadow: none;
135
128
  padding: 5px 16px;
136
129
  }
137
- [data-skin="github"] .cmd.big {
130
+ [data-skin='github'] .cmd.big {
138
131
  padding: 8px 20px;
139
132
  font-size: 14px;
140
133
  }
141
- [data-skin="github"] .cmd.fill:hover,
142
- [data-skin="github"] .cmd.big:hover,
143
- [data-skin="github"] .cmd.fill:active {
134
+ [data-skin='github'] .cmd.fill:hover,
135
+ [data-skin='github'] .cmd.big:hover,
136
+ [data-skin='github'] .cmd.fill:active {
144
137
  background: var(--btn-primary-hover);
145
138
  transform: none;
146
139
  box-shadow: none;
147
140
  }
148
- [data-skin="github"] .chat-composer .cmd.fill {
141
+ [data-skin='github'] .chat-composer .cmd.fill {
149
142
  box-shadow: none;
150
143
  }
151
144
 
152
145
  /* ---- Pills: GitHub's rounded labels, with the state ones filled ---- */
153
- [data-skin="github"] .pill {
146
+ [data-skin='github'] .pill {
154
147
  border-radius: 2em;
155
148
  padding: 0 8px;
156
149
  font-size: 12px;
157
150
  line-height: 20px;
158
151
  border-color: var(--line);
159
152
  }
160
- [data-skin="github"] .pill.open,
161
- [data-skin="github"] .pill.merged,
162
- [data-skin="github"] .pill.closed,
163
- [data-skin="github"] .pill.draft {
153
+ [data-skin='github'] .pill.open,
154
+ [data-skin='github'] .pill.merged,
155
+ [data-skin='github'] .pill.closed,
156
+ [data-skin='github'] .pill.draft,
157
+ [data-skin='github'] .pill.branch,
158
+ [data-skin='github'] .pill.uncommitted {
164
159
  border-color: transparent;
165
160
  color: #ffffff;
166
161
  }
167
- [data-skin="github"] .pill.open {
162
+ [data-skin='github'] .pill.open {
168
163
  background: light-dark(#1f883d, #347d39);
169
164
  }
170
- [data-skin="github"] .pill.merged {
165
+ [data-skin='github'] .pill.merged {
171
166
  background: light-dark(#8250df, #8256d0);
172
167
  }
173
- [data-skin="github"] .pill.closed {
168
+ [data-skin='github'] .pill.closed {
174
169
  background: light-dark(#cf222e, #c93c37);
175
170
  }
176
- [data-skin="github"] .pill.draft {
171
+ [data-skin='github'] .pill.draft {
177
172
  background: light-dark(#6e7781, #636e7b);
178
173
  }
179
- [data-skin="github"] .pill.risk,
180
- [data-skin="github"] .pill.kind,
181
- [data-skin="github"] .pill.agent {
174
+ [data-skin='github'] .pill.branch,
175
+ [data-skin='github'] .pill.uncommitted {
176
+ background: light-dark(#0969da, #1f6feb);
177
+ }
178
+ [data-skin='github'] .pill.risk,
179
+ [data-skin='github'] .pill.kind,
180
+ [data-skin='github'] .pill.agent {
182
181
  background: var(--panel-2);
183
182
  color: var(--fg-muted);
184
183
  font-weight: 500;
185
184
  }
186
- [data-skin="github"] .pill.test-tag {
185
+ [data-skin='github'] .pill.test-tag {
187
186
  background: var(--accent-bg);
188
187
  color: var(--accent);
189
188
  border-color: transparent;
@@ -193,164 +192,164 @@
193
192
  }
194
193
 
195
194
  /* ---- Cards ---- */
196
- [data-skin="github"] .panel,
197
- [data-skin="github"] .layer,
198
- [data-skin="github"] .diagram {
195
+ [data-skin='github'] .panel,
196
+ [data-skin='github'] .layer,
197
+ [data-skin='github'] .diagram {
199
198
  border-radius: var(--r);
200
199
  }
201
- [data-skin="github"] .panel-h,
202
- [data-skin="github"] .layer-h,
203
- [data-skin="github"] .diagram-h {
200
+ [data-skin='github'] .panel-h,
201
+ [data-skin='github'] .layer-h,
202
+ [data-skin='github'] .diagram-h {
204
203
  border-radius: calc(var(--r) - 1px) calc(var(--r) - 1px) 0 0;
205
204
  }
206
- [data-skin="github"] .chev {
205
+ [data-skin='github'] .chev {
207
206
  color: var(--fg-muted);
208
207
  }
209
- [data-skin="github"] .summary h3,
210
- [data-skin="github"] .judgment .lbl,
211
- [data-skin="github"] .role,
212
- [data-skin="github"] .diff th {
208
+ [data-skin='github'] .summary h3,
209
+ [data-skin='github'] .judgment .lbl,
210
+ [data-skin='github'] .role,
211
+ [data-skin='github'] .diff th {
213
212
  text-transform: none;
214
213
  letter-spacing: normal;
215
214
  }
216
- [data-skin="github"] .summary h3 {
215
+ [data-skin='github'] .summary h3 {
217
216
  font-size: 14px;
218
217
  color: var(--fg);
219
218
  }
220
- [data-skin="github"] .prose ul {
219
+ [data-skin='github'] .prose ul {
221
220
  list-style: disc;
222
221
  }
223
- [data-skin="github"] .sq {
222
+ [data-skin='github'] .sq {
224
223
  border-radius: 2px;
225
224
  }
226
- [data-skin="github"] .sevsum > .legend {
225
+ [data-skin='github'] .sevsum > .legend {
227
226
  border-radius: var(--r);
228
227
  box-shadow: var(--drop-shadow);
229
228
  }
230
229
  /* GitHub draws progress as a filled track rather than a hairline. */
231
- [data-skin="github"] .pline {
230
+ [data-skin='github'] .pline {
232
231
  background: var(--line);
233
232
  border-radius: var(--r);
234
233
  }
235
- [data-skin="github"] .pline span {
234
+ [data-skin='github'] .pline span {
236
235
  border-radius: var(--r);
237
236
  }
238
237
 
239
238
  /* ---- Rail ---- */
240
- [data-skin="github"] .tree a {
239
+ [data-skin='github'] .tree a {
241
240
  margin: 0 8px;
242
241
  padding: 6px 8px;
243
242
  border-radius: var(--r);
244
243
  }
245
- [data-skin="github"] .tree .layers a {
244
+ [data-skin='github'] .tree .layers a {
246
245
  padding-left: 20px;
247
246
  }
248
- [data-skin="github"] .tree a[aria-current] {
247
+ [data-skin='github'] .tree a[aria-current] {
249
248
  background: var(--accent-bg);
250
249
  box-shadow: inset 3px 0 0 var(--accent);
251
250
  }
252
251
 
253
252
  /* ---- Diff ---- */
254
- [data-skin="github"] .plus {
253
+ [data-skin='github'] .plus {
255
254
  border: 0;
256
255
  border-radius: var(--r);
257
256
  background: var(--accent);
258
257
  color: #ffffff;
259
258
  }
260
- [data-skin="github"] .av {
259
+ [data-skin='github'] .av {
261
260
  border-radius: 50%;
262
261
  border-color: var(--line);
263
262
  background: var(--panel-2);
264
263
  }
265
- [data-skin="github"] tr.hunk td {
264
+ [data-skin='github'] tr.hunk td {
266
265
  color: light-dark(#0550ae, #9198a1);
267
266
  }
268
267
 
269
- [data-skin="github"] .diff .ln {
268
+ [data-skin='github'] .diff .ln {
270
269
  border-right-color: light-dark(#d1d9e0, transparent);
271
270
  }
272
- [data-skin="github"] tr.add .ln {
271
+ [data-skin='github'] tr.add .ln {
273
272
  background: light-dark(#e6ffec, #31503d);
274
273
  color: light-dark(#818b98, #d1d7e0);
275
274
  }
276
- [data-skin="github"] tr.del .ln {
275
+ [data-skin='github'] tr.del .ln {
277
276
  background: light-dark(#ffebe9, rgba(229, 83, 75, 0.3));
278
277
  color: light-dark(#818b98, #d1d7e0);
279
278
  }
280
279
 
281
280
  /* ---- Boxes, menus, and overlays ---- */
282
- [data-skin="github"] code,
283
- [data-skin="github"] .snip,
284
- [data-skin="github"] .ctx-chip,
285
- [data-skin="github"] .diagram-src,
286
- [data-skin="github"] .cmdbox code,
287
- [data-skin="github"] .notice,
288
- [data-skin="github"] .callout,
289
- [data-skin="github"] .stale-bar,
290
- [data-skin="github"] .proposed,
291
- [data-skin="github"] .proposed-invalid,
292
- [data-skin="github"] .drop,
293
- [data-skin="github"] .prose pre,
294
- [data-skin="github"] dialog.settings .panel-ro {
281
+ [data-skin='github'] code,
282
+ [data-skin='github'] .snip,
283
+ [data-skin='github'] .ctx-chip,
284
+ [data-skin='github'] .diagram-src,
285
+ [data-skin='github'] .cmdbox code,
286
+ [data-skin='github'] .notice,
287
+ [data-skin='github'] .callout,
288
+ [data-skin='github'] .stale-bar,
289
+ [data-skin='github'] .proposed,
290
+ [data-skin='github'] .proposed-invalid,
291
+ [data-skin='github'] .drop,
292
+ [data-skin='github'] .prose pre,
293
+ [data-skin='github'] dialog.settings .panel-ro {
295
294
  border-radius: var(--r);
296
295
  }
297
- [data-skin="github"] code {
296
+ [data-skin='github'] code {
298
297
  border: 0;
299
298
  background: light-dark(rgba(129, 139, 152, 0.2), rgba(99, 110, 123, 0.4));
300
299
  padding: 0.2em 0.4em;
301
300
  }
302
- [data-skin="github"] .cmdbox code,
303
- [data-skin="github"] .diagram-src {
301
+ [data-skin='github'] .cmdbox code,
302
+ [data-skin='github'] .diagram-src {
304
303
  background: var(--panel-2);
305
304
  border: 1px solid var(--line);
306
305
  }
307
- [data-skin="github"] .drop {
306
+ [data-skin='github'] .drop {
308
307
  border-width: 1px;
309
308
  background: var(--panel-2);
310
309
  }
311
- [data-skin="github"] .drop:hover,
312
- [data-skin="github"] .drop.over {
310
+ [data-skin='github'] .drop:hover,
311
+ [data-skin='github'] .drop.over {
313
312
  transform: none;
314
313
  background: var(--accent-bg);
315
314
  }
316
- [data-skin="github"] .proposed,
317
- [data-skin="github"] .cmd.unseen,
318
- [data-skin="github"] .qq-menu {
315
+ [data-skin='github'] .proposed,
316
+ [data-skin='github'] .cmd.unseen,
317
+ [data-skin='github'] .qq-menu {
319
318
  border-width: 1px;
320
319
  }
321
- [data-skin="github"] .cmd.unseen,
322
- [data-skin="github"] .qq-menu,
323
- [data-skin="github"] .toast {
320
+ [data-skin='github'] .cmd.unseen,
321
+ [data-skin='github'] .qq-menu,
322
+ [data-skin='github'] .toast {
324
323
  border-radius: var(--r);
325
324
  box-shadow: var(--drop-shadow);
326
325
  }
327
- [data-skin="github"] select,
328
- [data-skin="github"] input,
329
- [data-skin="github"] textarea,
330
- [data-skin="github"] .composer-box textarea,
331
- [data-skin="github"] dialog.signoff-dialog textarea {
326
+ [data-skin='github'] select,
327
+ [data-skin='github'] input,
328
+ [data-skin='github'] textarea,
329
+ [data-skin='github'] .composer-box textarea,
330
+ [data-skin='github'] dialog.signoff-dialog textarea {
332
331
  border-width: 1px;
333
332
  border-color: var(--line);
334
333
  border-radius: var(--r);
335
334
  padding: 5px 12px;
336
335
  }
337
- [data-skin="github"] dialog {
336
+ [data-skin='github'] dialog {
338
337
  border-width: 1px;
339
338
  border-color: var(--line);
340
339
  border-radius: 12px;
341
340
  box-shadow: var(--drop-shadow);
342
341
  }
343
- [data-skin="github"] dialog::backdrop {
342
+ [data-skin='github'] dialog::backdrop {
344
343
  background: rgba(28, 33, 40, 0.6);
345
344
  }
346
345
 
347
346
  @media (max-width: 1000px) {
348
347
  /* The rail is a strip of chips at this width; the current one is underlined, not filled. */
349
- [data-skin="github"] .tree a {
348
+ [data-skin='github'] .tree a {
350
349
  margin: 0;
351
350
  border-radius: var(--r);
352
351
  }
353
- [data-skin="github"] .tree a[aria-current] {
352
+ [data-skin='github'] .tree a[aria-current] {
354
353
  box-shadow: inset 0 -3px 0 var(--accent);
355
354
  }
356
355
  }
package/static/styles.css CHANGED
@@ -1,14 +1,15 @@
1
1
  /* Keep this order: shared styles, components, responsive rules, interaction controls, and the
2
2
  github skin last, because it repaints the rules above it. */
3
- @import "./styles/base.css";
4
- @import "./styles/commands.css";
5
- @import "./styles/header.css";
6
- @import "./styles/layout.css";
7
- @import "./styles/review.css";
8
- @import "./styles/diff.css";
9
- @import "./styles/chat.css";
10
- @import "./styles/panels.css";
11
- @import "./styles/responsive.css";
12
- @import "./styles/review-actions.css";
13
- @import "./styles/chat-tools.css";
14
- @import "./styles/skin-github.css";
3
+ @import './styles/base.css';
4
+ @import './styles/commands.css';
5
+ @import './styles/header.css';
6
+ @import './styles/layout.css';
7
+ @import './styles/review.css';
8
+ @import './styles/diff.css';
9
+ @import './styles/chat.css';
10
+ @import './styles/panels.css';
11
+ @import './styles/responsive.css';
12
+ @import './styles/review-actions.css';
13
+ @import './styles/chat-tools.css';
14
+ @import './styles/skin-github.css';
15
+ @import './styles/chat-panel.css';
package/src/github/gh.ts DELETED
@@ -1,196 +0,0 @@
1
- import { execFile } from 'node:child_process'
2
-
3
- /**
4
- * The GitHub operations the tool needs, all through the `gh` CLI so the user's own login is
5
- * used and no token is ever read by this code. Routes receive an implementation through
6
- * AppContext; tests use an in-memory fake.
7
- */
8
- export interface GitHubClient {
9
- /** `gh api --method GET <path>` with optional query params; returns the parsed JSON body. */
10
- api(path: string, params?: Record<string, string>): Promise<unknown>
11
- /** `gh api -i --method GET <path>`: the response headers as well as the body. */
12
- apiWithHeaders(path: string): Promise<GhResponse>
13
- /** `gh api --method POST <path> --input -`; the JSON body goes over stdin, never the command line. */
14
- post(path: string, body: unknown): Promise<unknown>
15
- /** `gh api graphql`; returns the parsed `data` object. */
16
- graphql(query: string, variables: Record<string, string | number>): Promise<unknown>
17
- /** `gh auth status`: is the CLI installed and logged in? */
18
- authStatus(): Promise<{ installed: boolean; authenticated: boolean; detail: string }>
19
- /**
20
- * `gh auth token`: the token of the current login, or null when there is none. Callers keep it
21
- * in a local variable for the length of one request; it is never logged or written to disk.
22
- */
23
- authToken(): Promise<string | null>
24
- }
25
-
26
- /** One HTTP answer from `gh api -i`: the status, the header names in lower case, and the body. */
27
- export interface GhResponse {
28
- status: number
29
- headers: Record<string, string>
30
- body: unknown
31
- }
32
-
33
- /**
34
- * Splits the output of `gh api -i` into the last header block and the body. A redirect prints
35
- * one block per hop, and only the final one describes the answer.
36
- */
37
- export function parseIncludedResponse(stdout: string): GhResponse {
38
- const normalized = stdout.replace(/\r\n/g, '\n')
39
- const parts = normalized.split('\n\n')
40
- let body: unknown = null
41
- const headerBlocks: string[] = []
42
- for (const [i, part] of parts.entries()) {
43
- if (/^HTTP\/[\d.]+ \d{3}/.test(part)) {
44
- headerBlocks.push(part)
45
- continue
46
- }
47
- // Everything after the last header block is the body, which may hold blank lines itself.
48
- body = parseJsonOrNull(parts.slice(i).join('\n\n'))
49
- break
50
- }
51
- const last = headerBlocks[headerBlocks.length - 1] ?? ''
52
- const lines = last.split('\n').filter(l => l.trim() !== '')
53
- const status = Number(/^HTTP\/[\d.]+ (\d{3})/.exec(lines[0] ?? '')?.[1] ?? 0)
54
- const headers: Record<string, string> = {}
55
- for (const line of lines.slice(1)) {
56
- const at = line.indexOf(':')
57
- if (at > 0) {
58
- headers[line.slice(0, at).trim().toLowerCase()] = line.slice(at + 1).trim()
59
- }
60
- }
61
- return { status, headers, body }
62
- }
63
-
64
- function parseJsonOrNull(text: string): unknown {
65
- if (text.trim() === '') {
66
- return null
67
- }
68
- try {
69
- return JSON.parse(text) as unknown
70
- } catch {
71
- return null
72
- }
73
- }
74
-
75
- export class GitHubApiError extends Error {
76
- readonly path: string
77
- readonly stderr: string
78
- readonly exitCode: number
79
- /** True when the `gh` binary is not on PATH. */
80
- readonly missingBinary: boolean
81
-
82
- constructor(path: string, stderr: string, exitCode: number, missingBinary = false) {
83
- super(`gh api ${path} failed (${exitCode}): ${stderr.trim()}`)
84
- this.name = 'GitHubApiError'
85
- this.path = path
86
- this.stderr = stderr
87
- this.exitCode = exitCode
88
- this.missingBinary = missingBinary
89
- }
90
- /** `gh api` exits 1 with "HTTP 404" in stderr for a missing resource. */
91
- get notFound(): boolean {
92
- return /HTTP 404/.test(this.stderr)
93
- }
94
- get unauthenticated(): boolean {
95
- return /HTTP 401|not logged into|gh auth login/i.test(this.stderr)
96
- }
97
- }
98
-
99
- interface ExecResult {
100
- stdout: string
101
- stderr: string
102
- code: number
103
- missingBinary: boolean
104
- }
105
-
106
- export interface GhExecOptions {
107
- binary?: string
108
- /** Written to the child's stdin, for `gh api --input -`. */
109
- input?: string
110
- }
111
-
112
- export function execGh(args: string[], opts: GhExecOptions = {}): Promise<ExecResult> {
113
- return new Promise(resolve => {
114
- const child = execFile(
115
- opts.binary ?? 'gh',
116
- args,
117
- { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 },
118
- (error, stdout, stderr) => {
119
- const missingBinary = error !== null && 'code' in error && error.code === 'ENOENT'
120
- const code = error && typeof error.code === 'number' ? error.code : error ? 1 : 0
121
- resolve({ stdout, stderr, code, missingBinary })
122
- }
123
- )
124
- if (opts.input !== undefined) {
125
- child.stdin?.on('error', () => undefined)
126
- child.stdin?.end(opts.input)
127
- }
128
- })
129
- }
130
-
131
- export type GhExec = typeof execGh
132
-
133
- export function createGitHubClient(exec: GhExec = execGh): GitHubClient {
134
- return {
135
- api: async (path, params = {}) => {
136
- // `gh api` switches to POST as soon as a field is given; these are reads, so pin GET.
137
- const args = ['api', '--method', 'GET', path]
138
- for (const [k, v] of Object.entries(params)) {
139
- args.push('-F', `${k}=${v}`)
140
- }
141
- const r = await exec(args)
142
- if (r.code !== 0) {
143
- throw new GitHubApiError(path, r.missingBinary ? 'gh: command not found' : r.stderr, r.code, r.missingBinary)
144
- }
145
- return JSON.parse(r.stdout) as unknown
146
- },
147
- apiWithHeaders: async path => {
148
- const r = await exec(['api', '-i', '--method', 'GET', path])
149
- if (r.code !== 0) {
150
- throw new GitHubApiError(path, r.missingBinary ? 'gh: command not found' : r.stderr, r.code, r.missingBinary)
151
- }
152
- return parseIncludedResponse(r.stdout)
153
- },
154
- post: async (path, body) => {
155
- // The payload goes over stdin, so no comment text ever appears in an argument list.
156
- const r = await exec(['api', '--method', 'POST', path, '--input', '-'], { input: JSON.stringify(body) })
157
- if (r.code !== 0) {
158
- throw new GitHubApiError(path, r.missingBinary ? 'gh: command not found' : r.stderr, r.code, r.missingBinary)
159
- }
160
- return JSON.parse(r.stdout) as unknown
161
- },
162
- graphql: async (query, variables) => {
163
- const args = ['api', 'graphql', '-f', `query=${query}`]
164
- for (const [k, v] of Object.entries(variables)) {
165
- args.push(typeof v === 'number' ? '-F' : '-f', `${k}=${v}`)
166
- }
167
- const r = await exec(args)
168
- if (r.code !== 0) {
169
- throw new GitHubApiError(
170
- 'graphql',
171
- r.missingBinary ? 'gh: command not found' : r.stderr,
172
- r.code,
173
- r.missingBinary
174
- )
175
- }
176
- const body = JSON.parse(r.stdout) as { data?: unknown; errors?: Array<{ message: string }> }
177
- if (body.errors && body.errors.length > 0) {
178
- throw new GitHubApiError('graphql', body.errors.map(e => e.message).join('; '), 1)
179
- }
180
- return body.data
181
- },
182
- authStatus: async () => {
183
- const r = await exec(['auth', 'status'])
184
- if (r.missingBinary) {
185
- return { installed: false, authenticated: false, detail: 'gh is not on PATH' }
186
- }
187
- const detail = (r.stdout + r.stderr).trim().split('\n')[0] ?? ''
188
- return { installed: true, authenticated: r.code === 0, detail }
189
- },
190
- authToken: async () => {
191
- const r = await exec(['auth', 'token'])
192
- const token = r.stdout.trim()
193
- return r.code === 0 && token !== '' ? token : null
194
- },
195
- }
196
- }