@vintasoftware/pr-review-canvas 0.3.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 (99) hide show
  1. package/README.md +52 -23
  2. package/docs/reference.md +154 -44
  3. package/package.json +9 -4
  4. package/pr-review.config.example.yml +6 -0
  5. package/prompts/chat-seed.md +3 -0
  6. package/prompts/generation-format.md +3 -0
  7. package/skills/pr-review-canvas/SKILL.md +69 -39
  8. package/src/canvas/comment.ts +24 -0
  9. package/src/canvas/import.ts +30 -9
  10. package/src/canvas/name.ts +1 -0
  11. package/src/canvas/zip.ts +21 -1
  12. package/src/chat/chat-manager.ts +40 -39
  13. package/src/chat/threads.ts +27 -26
  14. package/src/cli.ts +12 -7
  15. package/src/commands.ts +45 -16
  16. package/src/config.ts +14 -26
  17. package/src/contract/api.ts +32 -1
  18. package/src/contract/canvas-manifest.ts +2 -0
  19. package/src/contract/comments.ts +5 -0
  20. package/src/contract/discovery.ts +5 -2
  21. package/src/contract/generation-context.ts +24 -1
  22. package/src/contract/review-key.ts +51 -0
  23. package/src/contract/reviews.ts +17 -0
  24. package/src/contract/settings.ts +2 -0
  25. package/src/contract/state.ts +41 -21
  26. package/src/git/environment.mjs +27 -0
  27. package/src/git/git.ts +109 -9
  28. package/src/git/local-target.ts +138 -0
  29. package/src/git/patch-lines.ts +34 -2
  30. package/src/git/pr-refs.ts +36 -0
  31. package/src/github/attachments.ts +9 -257
  32. package/src/github/canvas-comment.ts +22 -0
  33. package/src/github/capabilities.ts +3 -41
  34. package/src/github/comments.ts +3 -24
  35. package/src/github/post-comment.ts +3 -36
  36. package/src/github/post-review.ts +4 -19
  37. package/src/github/pr.ts +6 -87
  38. package/src/github/threads.ts +2 -2
  39. package/src/gitlab/attachments.ts +40 -0
  40. package/src/gitlab/canvas-comment.ts +26 -0
  41. package/src/gitlab/capabilities.ts +64 -0
  42. package/src/gitlab/comments.ts +164 -0
  43. package/src/gitlab/mr.ts +115 -0
  44. package/src/gitlab/post-comment.ts +111 -0
  45. package/src/gitlab/post-review.ts +54 -0
  46. package/src/gitlab/project.ts +13 -0
  47. package/src/host/attachments.ts +293 -0
  48. package/src/host/capabilities.ts +38 -0
  49. package/src/host/client.ts +245 -0
  50. package/src/host/host.ts +136 -0
  51. package/src/host/pr.ts +51 -0
  52. package/src/host/remote.ts +42 -0
  53. package/src/project-config.ts +13 -0
  54. package/src/review/carry-over.ts +79 -0
  55. package/src/review/doctor.ts +22 -12
  56. package/src/review/prepare.ts +64 -10
  57. package/src/review/publish.ts +61 -10
  58. package/src/{github → review}/review-body.ts +17 -5
  59. package/src/review/skill-command.ts +5 -3
  60. package/src/review/validate-folds.ts +2 -2
  61. package/src/review/validate.ts +4 -4
  62. package/src/server/bundle.ts +312 -111
  63. package/src/server/context.ts +10 -8
  64. package/src/server/errors.ts +30 -8
  65. package/src/server/html.ts +34 -10
  66. package/src/server/routes/api.ts +63 -26
  67. package/src/server/routes/chat-routes.ts +76 -38
  68. package/src/server/routes/pages.ts +22 -8
  69. package/src/server/routes/review-routes.ts +84 -33
  70. package/src/store/canvas-store.ts +90 -55
  71. package/src/store/data-dir.ts +2 -1
  72. package/src/store/derived-store.ts +41 -27
  73. package/src/store/pr-store.ts +23 -14
  74. package/src/store/state-store.ts +40 -36
  75. package/static/js/api.js +32 -24
  76. package/static/js/app.js +24 -10
  77. package/static/js/chat.js +3 -2
  78. package/static/js/composer.js +24 -14
  79. package/static/js/contract-types.d.ts +3 -0
  80. package/static/js/diff-renderer.js +1 -1
  81. package/static/js/download.js +1 -1
  82. package/static/js/empty-state.js +85 -18
  83. package/static/js/errors.js +22 -6
  84. package/static/js/header.js +31 -8
  85. package/static/js/host.js +40 -0
  86. package/static/js/import-zone.js +1 -1
  87. package/static/js/interactions.js +1 -2
  88. package/static/js/layers.js +3 -3
  89. package/static/js/links.js +3 -3
  90. package/static/js/markdown.js +28 -1
  91. package/static/js/points.js +2 -1
  92. package/static/js/review-session.js +7 -2
  93. package/static/js/settings.js +2 -1
  94. package/static/js/signoff.js +6 -7
  95. package/static/styles/commands.css +6 -0
  96. package/static/styles/panels.css +4 -0
  97. package/static/styles/review-actions.css +1 -0
  98. package/static/styles/skin-github.css +7 -1
  99. package/src/github/gh.ts +0 -211
@@ -153,7 +153,9 @@
153
153
  [data-skin='github'] .pill.open,
154
154
  [data-skin='github'] .pill.merged,
155
155
  [data-skin='github'] .pill.closed,
156
- [data-skin='github'] .pill.draft {
156
+ [data-skin='github'] .pill.draft,
157
+ [data-skin='github'] .pill.branch,
158
+ [data-skin='github'] .pill.uncommitted {
157
159
  border-color: transparent;
158
160
  color: #ffffff;
159
161
  }
@@ -169,6 +171,10 @@
169
171
  [data-skin='github'] .pill.draft {
170
172
  background: light-dark(#6e7781, #636e7b);
171
173
  }
174
+ [data-skin='github'] .pill.branch,
175
+ [data-skin='github'] .pill.uncommitted {
176
+ background: light-dark(#0969da, #1f6feb);
177
+ }
172
178
  [data-skin='github'] .pill.risk,
173
179
  [data-skin='github'] .pill.kind,
174
180
  [data-skin='github'] .pill.agent {
package/src/github/gh.ts DELETED
@@ -1,211 +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(
144
- path,
145
- r.missingBinary ? 'gh: command not found' : r.stderr,
146
- r.code,
147
- r.missingBinary
148
- )
149
- }
150
- return JSON.parse(r.stdout) as unknown
151
- },
152
- apiWithHeaders: async path => {
153
- const r = await exec(['api', '-i', '--method', 'GET', path])
154
- if (r.code !== 0) {
155
- throw new GitHubApiError(
156
- path,
157
- r.missingBinary ? 'gh: command not found' : r.stderr,
158
- r.code,
159
- r.missingBinary
160
- )
161
- }
162
- return parseIncludedResponse(r.stdout)
163
- },
164
- post: async (path, body) => {
165
- // The payload goes over stdin, so no comment text ever appears in an argument list.
166
- const r = await exec(['api', '--method', 'POST', path, '--input', '-'], { input: JSON.stringify(body) })
167
- if (r.code !== 0) {
168
- throw new GitHubApiError(
169
- path,
170
- r.missingBinary ? 'gh: command not found' : r.stderr,
171
- r.code,
172
- r.missingBinary
173
- )
174
- }
175
- return JSON.parse(r.stdout) as unknown
176
- },
177
- graphql: async (query, variables) => {
178
- const args = ['api', 'graphql', '-f', `query=${query}`]
179
- for (const [k, v] of Object.entries(variables)) {
180
- args.push(typeof v === 'number' ? '-F' : '-f', `${k}=${v}`)
181
- }
182
- const r = await exec(args)
183
- if (r.code !== 0) {
184
- throw new GitHubApiError(
185
- 'graphql',
186
- r.missingBinary ? 'gh: command not found' : r.stderr,
187
- r.code,
188
- r.missingBinary
189
- )
190
- }
191
- const body = JSON.parse(r.stdout) as { data?: unknown; errors?: Array<{ message: string }> }
192
- if (body.errors && body.errors.length > 0) {
193
- throw new GitHubApiError('graphql', body.errors.map(e => e.message).join('; '), 1)
194
- }
195
- return body.data
196
- },
197
- authStatus: async () => {
198
- const r = await exec(['auth', 'status'])
199
- if (r.missingBinary) {
200
- return { installed: false, authenticated: false, detail: 'gh is not on PATH' }
201
- }
202
- const detail = (r.stdout + r.stderr).trim().split('\n')[0] ?? ''
203
- return { installed: true, authenticated: r.code === 0, detail }
204
- },
205
- authToken: async () => {
206
- const r = await exec(['auth', 'token'])
207
- const token = r.stdout.trim()
208
- return r.code === 0 && token !== '' ? token : null
209
- },
210
- }
211
- }