@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.
- package/README.md +52 -23
- package/docs/reference.md +154 -44
- package/package.json +9 -4
- package/pr-review.config.example.yml +6 -0
- package/prompts/chat-seed.md +3 -0
- package/prompts/generation-format.md +3 -0
- package/skills/pr-review-canvas/SKILL.md +69 -39
- package/src/canvas/comment.ts +24 -0
- package/src/canvas/import.ts +30 -9
- package/src/canvas/name.ts +1 -0
- package/src/canvas/zip.ts +21 -1
- package/src/chat/chat-manager.ts +40 -39
- package/src/chat/threads.ts +27 -26
- package/src/cli.ts +12 -7
- package/src/commands.ts +45 -16
- package/src/config.ts +14 -26
- package/src/contract/api.ts +32 -1
- package/src/contract/canvas-manifest.ts +2 -0
- package/src/contract/comments.ts +5 -0
- package/src/contract/discovery.ts +5 -2
- package/src/contract/generation-context.ts +24 -1
- package/src/contract/review-key.ts +51 -0
- package/src/contract/reviews.ts +17 -0
- package/src/contract/settings.ts +2 -0
- package/src/contract/state.ts +41 -21
- package/src/git/environment.mjs +27 -0
- package/src/git/git.ts +109 -9
- package/src/git/local-target.ts +138 -0
- package/src/git/patch-lines.ts +34 -2
- package/src/git/pr-refs.ts +36 -0
- package/src/github/attachments.ts +9 -257
- package/src/github/canvas-comment.ts +22 -0
- package/src/github/capabilities.ts +3 -41
- package/src/github/comments.ts +3 -24
- package/src/github/post-comment.ts +3 -36
- package/src/github/post-review.ts +4 -19
- package/src/github/pr.ts +6 -87
- package/src/github/threads.ts +2 -2
- package/src/gitlab/attachments.ts +40 -0
- package/src/gitlab/canvas-comment.ts +26 -0
- package/src/gitlab/capabilities.ts +64 -0
- package/src/gitlab/comments.ts +164 -0
- package/src/gitlab/mr.ts +115 -0
- package/src/gitlab/post-comment.ts +111 -0
- package/src/gitlab/post-review.ts +54 -0
- package/src/gitlab/project.ts +13 -0
- package/src/host/attachments.ts +293 -0
- package/src/host/capabilities.ts +38 -0
- package/src/host/client.ts +245 -0
- package/src/host/host.ts +136 -0
- package/src/host/pr.ts +51 -0
- package/src/host/remote.ts +42 -0
- package/src/project-config.ts +13 -0
- package/src/review/carry-over.ts +79 -0
- package/src/review/doctor.ts +22 -12
- package/src/review/prepare.ts +64 -10
- package/src/review/publish.ts +61 -10
- package/src/{github → review}/review-body.ts +17 -5
- package/src/review/skill-command.ts +5 -3
- package/src/review/validate-folds.ts +2 -2
- package/src/review/validate.ts +4 -4
- package/src/server/bundle.ts +312 -111
- package/src/server/context.ts +10 -8
- package/src/server/errors.ts +30 -8
- package/src/server/html.ts +34 -10
- package/src/server/routes/api.ts +63 -26
- package/src/server/routes/chat-routes.ts +76 -38
- package/src/server/routes/pages.ts +22 -8
- package/src/server/routes/review-routes.ts +84 -33
- package/src/store/canvas-store.ts +90 -55
- package/src/store/data-dir.ts +2 -1
- package/src/store/derived-store.ts +41 -27
- package/src/store/pr-store.ts +23 -14
- package/src/store/state-store.ts +40 -36
- package/static/js/api.js +32 -24
- package/static/js/app.js +24 -10
- package/static/js/chat.js +3 -2
- package/static/js/composer.js +24 -14
- package/static/js/contract-types.d.ts +3 -0
- package/static/js/diff-renderer.js +1 -1
- package/static/js/download.js +1 -1
- package/static/js/empty-state.js +85 -18
- package/static/js/errors.js +22 -6
- package/static/js/header.js +31 -8
- package/static/js/host.js +40 -0
- package/static/js/import-zone.js +1 -1
- package/static/js/interactions.js +1 -2
- package/static/js/layers.js +3 -3
- package/static/js/links.js +3 -3
- package/static/js/markdown.js +28 -1
- package/static/js/points.js +2 -1
- package/static/js/review-session.js +7 -2
- package/static/js/settings.js +2 -1
- package/static/js/signoff.js +6 -7
- package/static/styles/commands.css +6 -0
- package/static/styles/panels.css +4 -0
- package/static/styles/review-actions.css +1 -0
- package/static/styles/skin-github.css +7 -1
- 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
|
-
}
|