@vintasoftware/pr-review-canvas 0.3.0 → 0.5.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 +79 -31
- package/docs/reference.md +396 -101
- package/package.json +9 -4
- package/pr-review.config.example.yml +44 -4
- package/prompts/chat-seed.md +3 -0
- package/prompts/generation-format.md +123 -26
- package/prompts/generation-strict-incremental.md +53 -0
- package/prompts/generation-strict.md +1 -27
- package/prompts/generation-surfacing-incremental.md +56 -0
- package/prompts/generation-surfacing.md +1 -58
- package/prompts/judging-strict.md +27 -0
- package/prompts/judging-surfacing.md +58 -0
- package/skills/pr-review-canvas/SKILL.md +81 -42
- package/src/acpx/acpx.ts +98 -5
- package/src/acpx/models.ts +43 -0
- 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 +67 -40
- package/src/chat/threads.ts +27 -26
- package/src/cli.ts +70 -8
- package/src/commands.ts +50 -17
- package/src/config.ts +14 -26
- package/src/contract/api.ts +58 -2
- package/src/contract/canvas-manifest.ts +7 -0
- package/src/contract/comments.ts +5 -0
- package/src/contract/discovery.ts +5 -2
- package/src/contract/generation-context.ts +76 -2
- package/src/contract/keys.ts +1 -0
- package/src/contract/pending.ts +49 -0
- package/src/contract/review-artifact.ts +50 -7
- package/src/contract/review-key.ts +51 -0
- package/src/contract/reviews.ts +26 -0
- package/src/contract/settings.ts +7 -0
- package/src/contract/state.ts +53 -22
- package/src/contract/validation.ts +1 -0
- 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 +64 -23
- 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 +93 -0
- package/src/gitlab/project.ts +13 -0
- package/src/gitlab/publish-drafts.ts +69 -0
- package/src/host/attachments.ts +293 -0
- package/src/host/capabilities.ts +38 -0
- package/src/host/client.ts +246 -0
- package/src/host/host.ts +154 -0
- package/src/host/pr.ts +51 -0
- package/src/host/remote.ts +42 -0
- package/src/project-config.ts +26 -0
- package/src/review/carry-marks.ts +131 -0
- package/src/review/carry-over.ts +79 -0
- package/src/review/doctor.ts +82 -38
- package/src/review/incremental.ts +107 -0
- package/src/review/normalize.ts +14 -4
- package/src/review/prepare.ts +111 -10
- package/src/review/prompt.ts +112 -5
- package/src/review/publish.ts +62 -10
- package/src/{github → review}/review-body.ts +17 -5
- package/src/review/skill-command.ts +5 -3
- package/src/review/test-paths.ts +44 -4
- package/src/review/validate-folds.ts +349 -24
- package/src/review/validate.ts +14 -5
- package/src/server/bundle.ts +324 -111
- package/src/server/context.ts +10 -8
- package/src/server/errors.ts +30 -8
- package/src/server/html.ts +35 -11
- package/src/server/routes/api.ts +63 -26
- package/src/server/routes/chat-routes.ts +91 -44
- package/src/server/routes/pages.ts +26 -9
- package/src/server/routes/review-routes.ts +278 -67
- package/src/store/canvas-store.ts +93 -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/settings-store.ts +9 -1
- package/src/store/state-store.ts +105 -36
- package/src/upgrade.ts +338 -0
- package/static/js/api.js +87 -25
- package/static/js/app.js +50 -15
- package/static/js/chat-panel.js +32 -9
- package/static/js/chat.js +30 -6
- package/static/js/code-folds.js +171 -44
- package/static/js/composer.js +132 -17
- package/static/js/contract-types.d.ts +7 -0
- package/static/js/diff-decorations.js +67 -1
- package/static/js/diff-renderer.js +1 -1
- package/static/js/download.js +1 -1
- package/static/js/empty-state.js +102 -18
- package/static/js/errors.js +22 -6
- package/static/js/fold-levels.js +176 -0
- package/static/js/header.js +65 -15
- package/static/js/host.js +40 -0
- package/static/js/import-zone.js +1 -1
- package/static/js/interactions.js +274 -46
- package/static/js/keyboard.js +4 -1
- package/static/js/keys.js +12 -0
- package/static/js/layers.js +295 -32
- package/static/js/links.js +3 -3
- package/static/js/markdown.js +28 -1
- package/static/js/nav.js +22 -4
- package/static/js/pending.js +161 -0
- package/static/js/points.js +70 -9
- package/static/js/progress.js +4 -5
- package/static/js/quick-questions.js +15 -2
- package/static/js/reading-level.js +97 -0
- package/static/js/review-session.js +113 -29
- package/static/js/settings.js +55 -24
- package/static/js/signoff.js +79 -10
- package/static/js/skin.js +2 -2
- package/static/styles/chat-panel.css +22 -24
- package/static/styles/chat.css +4 -0
- package/static/styles/commands.css +6 -0
- package/static/styles/header.css +21 -0
- package/static/styles/panels.css +4 -0
- package/static/styles/pending.css +102 -0
- package/static/styles/review-actions.css +1 -0
- package/static/styles/review.css +4 -0
- package/static/styles/skin-github.css +7 -1
- package/static/styles.css +1 -0
- package/src/github/gh.ts +0 -211
package/src/upgrade.ts
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// `pr-review upgrade`: brings pr-review, acpx, and the project's copy of the skill up to date. It
|
|
2
|
+
// says what it will change and asks first. Once pr-review itself moves, the new version checks the
|
|
3
|
+
// skill, so the skill it ships is stamped by the code that ships it.
|
|
4
|
+
import { realpath } from 'node:fs/promises'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { parseArgs } from 'node:util'
|
|
7
|
+
import { z } from 'zod'
|
|
8
|
+
import { type CliIo, EXIT, printJson, UsageError } from './commands.js'
|
|
9
|
+
import { findSkillCopies, type ReadSkill, type SkillCopy } from './review/doctor.js'
|
|
10
|
+
import { installSkill, SkillDirExistsError } from './review/install-skill.js'
|
|
11
|
+
|
|
12
|
+
export const ACPX_PACKAGE = 'acpx'
|
|
13
|
+
|
|
14
|
+
export interface CommandResult {
|
|
15
|
+
ok: boolean
|
|
16
|
+
stdout: string
|
|
17
|
+
stderr: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface UpgradeDeps {
|
|
21
|
+
/** The running pr-review: its package name, version, and where it is installed. */
|
|
22
|
+
packageName: string
|
|
23
|
+
version: string
|
|
24
|
+
packageRoot: string
|
|
25
|
+
/** The repository whose skill copies are refreshed, or null outside one. */
|
|
26
|
+
repoRoot: string | null
|
|
27
|
+
acpxVersion: () => Promise<string | null>
|
|
28
|
+
/** The acpx that PATH runs, which chat uses. */
|
|
29
|
+
acpxPath: string | null
|
|
30
|
+
/** Runs one command without a shell. Only `npm` is run. */
|
|
31
|
+
run: (file: string, args: string[]) => Promise<CommandResult>
|
|
32
|
+
/** Runs `pr-review <args>` from the package on disk, which is the new one after its upgrade. */
|
|
33
|
+
runInstalled: (args: string[]) => Promise<CommandResult>
|
|
34
|
+
/** Asks a yes/no question, or returns null when there is no one to ask. */
|
|
35
|
+
confirm: (question: string) => Promise<boolean | null>
|
|
36
|
+
readSkill?: ReadSkill
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** What one step upgrades, in the order a plan runs them. */
|
|
40
|
+
const StepKindSchema = z.enum(['package', 'acpx', 'skill'])
|
|
41
|
+
type StepKind = z.infer<typeof StepKindSchema>
|
|
42
|
+
|
|
43
|
+
const NpmStepSchema = z.object({
|
|
44
|
+
kind: z.enum(['package', 'acpx']),
|
|
45
|
+
name: z.string(),
|
|
46
|
+
from: z.string(),
|
|
47
|
+
to: z.string(),
|
|
48
|
+
})
|
|
49
|
+
const SkillStepSchema = z.object({ kind: z.literal('skill'), paths: z.array(z.string()) })
|
|
50
|
+
const OutcomeFields = {
|
|
51
|
+
status: z.enum(['done', 'failed']),
|
|
52
|
+
detail: z.string().optional(),
|
|
53
|
+
}
|
|
54
|
+
const StepOutcomeSchema = z.discriminatedUnion('kind', [
|
|
55
|
+
NpmStepSchema.extend(OutcomeFields),
|
|
56
|
+
SkillStepSchema.extend(OutcomeFields),
|
|
57
|
+
])
|
|
58
|
+
/** What an applied upgrade prints, and so what the parent reads back from the new version. */
|
|
59
|
+
const AppliedReportSchema = z.object({ steps: z.array(StepOutcomeSchema) })
|
|
60
|
+
|
|
61
|
+
export type UpgradeStep = z.infer<typeof NpmStepSchema> | z.infer<typeof SkillStepSchema>
|
|
62
|
+
export type StepOutcome = z.infer<typeof StepOutcomeSchema>
|
|
63
|
+
|
|
64
|
+
export interface UpgradePlan {
|
|
65
|
+
steps: UpgradeStep[]
|
|
66
|
+
/** What was looked at and left alone, and why. */
|
|
67
|
+
notes: string[]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** `1.2.10` against `1.2.9`, numerically. A prerelease tag is ignored. */
|
|
71
|
+
export function isNewer(candidate: string, current: string): boolean {
|
|
72
|
+
const parts = (v: string): number[] => v.trim().replace(/^v/, '').split(/[-+]/)[0]!.split('.').map(Number)
|
|
73
|
+
const a = parts(candidate)
|
|
74
|
+
const b = parts(current)
|
|
75
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
76
|
+
const diff = (a[i] ?? 0) - (b[i] ?? 0)
|
|
77
|
+
if (Number.isNaN(diff)) return false
|
|
78
|
+
if (diff !== 0) return diff > 0
|
|
79
|
+
}
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function latestVersion(deps: UpgradeDeps, name: string): Promise<string | null> {
|
|
84
|
+
const result = await deps.run('npm', ['view', name, 'version'])
|
|
85
|
+
const version = result.stdout.trim()
|
|
86
|
+
return result.ok && version !== '' ? version : null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Whether `installed` lies in the copy of `name` that `npm install -g` replaces. Installing over
|
|
91
|
+
* any other copy (pnpm, brew, another node) would leave the one in use as it was.
|
|
92
|
+
*/
|
|
93
|
+
async function ownedByGlobalNpm(
|
|
94
|
+
globalRoot: string | null,
|
|
95
|
+
name: string,
|
|
96
|
+
installed: string
|
|
97
|
+
): Promise<boolean> {
|
|
98
|
+
if (globalRoot === null) return false
|
|
99
|
+
try {
|
|
100
|
+
const globalCopy = await realpath(path.join(globalRoot, name))
|
|
101
|
+
const real = await realpath(installed)
|
|
102
|
+
return real === globalCopy || real.startsWith(`${globalCopy}${path.sep}`)
|
|
103
|
+
} catch {
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function notGlobalNote(name: string, latest: string, installed: string): string {
|
|
109
|
+
return `${name} ${latest} is out, but the copy in use (${installed}) is not the global npm install; update it the way you installed it`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function skillCopies(deps: UpgradeDeps): Promise<SkillCopy[]> {
|
|
113
|
+
return deps.repoRoot === null ? [] : findSkillCopies(deps.repoRoot, deps.readSkill)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function planUpgrade(deps: UpgradeDeps): Promise<UpgradePlan> {
|
|
117
|
+
const steps: UpgradeStep[] = []
|
|
118
|
+
const notes: string[] = []
|
|
119
|
+
|
|
120
|
+
const root = await deps.run('npm', ['root', '-g'])
|
|
121
|
+
const globalRoot = root.ok ? root.stdout.trim() : null
|
|
122
|
+
const latest = await latestVersion(deps, deps.packageName)
|
|
123
|
+
if (latest === null) {
|
|
124
|
+
notes.push(`${deps.packageName}: could not read the latest version from npm`)
|
|
125
|
+
} else if (!isNewer(latest, deps.version)) {
|
|
126
|
+
notes.push(`${deps.packageName} ${deps.version} is up to date`)
|
|
127
|
+
} else if (await ownedByGlobalNpm(globalRoot, deps.packageName, deps.packageRoot)) {
|
|
128
|
+
steps.push({ kind: 'package', name: deps.packageName, from: deps.version, to: latest })
|
|
129
|
+
} else {
|
|
130
|
+
notes.push(notGlobalNote(deps.packageName, latest, deps.packageRoot))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const acpx = deps.acpxPath === null ? null : await deps.acpxVersion()
|
|
134
|
+
const acpxLatest = acpx === null ? null : await latestVersion(deps, ACPX_PACKAGE)
|
|
135
|
+
if (acpx === null || deps.acpxPath === null) {
|
|
136
|
+
notes.push('acpx is not installed; AI Chat needs it: npm install -g acpx@latest')
|
|
137
|
+
} else if (acpxLatest === null) {
|
|
138
|
+
notes.push('acpx: could not read the latest version from npm')
|
|
139
|
+
} else if (!isNewer(acpxLatest, acpx)) {
|
|
140
|
+
notes.push(`acpx ${acpx} is up to date`)
|
|
141
|
+
} else if (await ownedByGlobalNpm(globalRoot, ACPX_PACKAGE, deps.acpxPath)) {
|
|
142
|
+
steps.push({ kind: 'acpx', name: ACPX_PACKAGE, from: acpx, to: acpxLatest })
|
|
143
|
+
} else {
|
|
144
|
+
notes.push(notGlobalNote(ACPX_PACKAGE, acpxLatest, deps.acpxPath))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (deps.repoRoot === null) {
|
|
148
|
+
notes.push('not in a repository, so no project skill to refresh')
|
|
149
|
+
} else {
|
|
150
|
+
const copies = await skillCopies(deps)
|
|
151
|
+
// A new pr-review can ship a new skill, so every copy is in question once the package moves.
|
|
152
|
+
const packageMoves = steps.some(step => step.kind === 'package')
|
|
153
|
+
const toCheck = copies.filter(copy => packageMoves || copy.stale)
|
|
154
|
+
if (copies.length === 0) {
|
|
155
|
+
notes.push('the project has no copy of the skill; run `pr-review install-skill` to add one')
|
|
156
|
+
} else if (toCheck.length > 0) {
|
|
157
|
+
steps.push({ kind: 'skill', paths: toCheck.map(copy => copy.path) })
|
|
158
|
+
} else {
|
|
159
|
+
notes.push(`the project skill matches pr-review ${deps.version}`)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { steps, notes }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function describeStep(step: UpgradeStep): string {
|
|
166
|
+
if (step.kind === 'skill') {
|
|
167
|
+
return `refresh the project skill where it differs from pr-review's: ${step.paths.join(', ')}`
|
|
168
|
+
}
|
|
169
|
+
return `upgrade ${step.name} ${step.from} -> ${step.to} (npm install -g ${step.name}@${step.to})`
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function applySkill(deps: UpgradeDeps): Promise<{ written: string[]; skipped: string[] }> {
|
|
173
|
+
const written: string[] = []
|
|
174
|
+
const skipped: string[] = []
|
|
175
|
+
for (const copy of await skillCopies(deps)) {
|
|
176
|
+
if (!copy.stale) continue
|
|
177
|
+
try {
|
|
178
|
+
await installSkill({ targets: [{ kind: copy.kind, dir: copy.dir }] })
|
|
179
|
+
written.push(copy.path)
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (!(err instanceof SkillDirExistsError)) throw err
|
|
182
|
+
skipped.push(copy.path)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return { written, skipped }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function applySkillStep(
|
|
189
|
+
step: z.infer<typeof SkillStepSchema>,
|
|
190
|
+
deps: UpgradeDeps
|
|
191
|
+
): Promise<StepOutcome> {
|
|
192
|
+
const { written, skipped } = await applySkill(deps)
|
|
193
|
+
if (skipped.length > 0) {
|
|
194
|
+
return {
|
|
195
|
+
...step,
|
|
196
|
+
paths: written,
|
|
197
|
+
status: 'failed',
|
|
198
|
+
detail: `not a managed copy, left alone: ${skipped.join(', ')}; run \`pr-review install-skill --force\` to replace it`,
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return { ...step, paths: written, status: 'done' }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The new pr-review's own `upgrade --yes --only <kinds>`: it plans again with its own code, and runs
|
|
206
|
+
* only the kinds of step the user confirmed. Its report's steps join this one's, so this process
|
|
207
|
+
* stays the one that prints them.
|
|
208
|
+
*/
|
|
209
|
+
async function handOff(deps: UpgradeDeps, kinds: StepKind[]): Promise<StepOutcome[]> {
|
|
210
|
+
const result = await deps.runInstalled([
|
|
211
|
+
'upgrade',
|
|
212
|
+
'--yes',
|
|
213
|
+
'--only',
|
|
214
|
+
kinds.join(','),
|
|
215
|
+
...(deps.repoRoot === null ? [] : ['--repo', deps.repoRoot]),
|
|
216
|
+
])
|
|
217
|
+
let report: unknown
|
|
218
|
+
try {
|
|
219
|
+
// The report is the last line; String() turns a missing one into text JSON.parse rejects.
|
|
220
|
+
report = JSON.parse(String(result.stdout.trim().split('\n').at(-1)))
|
|
221
|
+
} catch {
|
|
222
|
+
report = null
|
|
223
|
+
}
|
|
224
|
+
const parsed = AppliedReportSchema.safeParse(report)
|
|
225
|
+
if (parsed.success) return parsed.data.steps
|
|
226
|
+
return [
|
|
227
|
+
{
|
|
228
|
+
kind: 'skill',
|
|
229
|
+
paths: [],
|
|
230
|
+
status: 'failed',
|
|
231
|
+
detail:
|
|
232
|
+
`the new pr-review did not report a result; run \`pr-review upgrade\` again\n` +
|
|
233
|
+
result.stderr.trim().split('\n').slice(-3).join('\n'),
|
|
234
|
+
},
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Runs every step in order. A failed npm install does not stop the rest. Once pr-review itself is
|
|
240
|
+
* upgraded, the new version plans and runs whatever is left, so the skill it ships is checked and
|
|
241
|
+
* copied by its own code.
|
|
242
|
+
*/
|
|
243
|
+
export async function applyUpgrade(plan: UpgradePlan, deps: UpgradeDeps): Promise<StepOutcome[]> {
|
|
244
|
+
const outcomes: StepOutcome[] = []
|
|
245
|
+
for (const [index, step] of plan.steps.entries()) {
|
|
246
|
+
if (step.kind === 'skill') {
|
|
247
|
+
outcomes.push(await applySkillStep(step, deps))
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
const result = await deps.run('npm', ['install', '-g', `${step.name}@${step.to}`])
|
|
251
|
+
if (!result.ok) {
|
|
252
|
+
const detail = result.stderr.trim().split('\n').slice(-3).join('\n') || 'npm install failed'
|
|
253
|
+
outcomes.push({ ...step, status: 'failed', detail })
|
|
254
|
+
continue
|
|
255
|
+
}
|
|
256
|
+
outcomes.push({ ...step, status: 'done' })
|
|
257
|
+
const rest = plan.steps.slice(index + 1)
|
|
258
|
+
if (step.kind === 'package' && rest.length > 0) {
|
|
259
|
+
return [
|
|
260
|
+
...outcomes,
|
|
261
|
+
...(await handOff(
|
|
262
|
+
deps,
|
|
263
|
+
rest.map(later => later.kind)
|
|
264
|
+
)),
|
|
265
|
+
]
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return outcomes
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** `--only package,acpx,skill`: the kinds of step this run may take. */
|
|
272
|
+
function parseOnly(raw: string | undefined): ReadonlySet<StepKind> | null {
|
|
273
|
+
if (raw === undefined) return null
|
|
274
|
+
const kinds = z.array(StepKindSchema).safeParse(raw.split(','))
|
|
275
|
+
if (!kinds.success) {
|
|
276
|
+
throw new UsageError(`--only takes a comma list of ${StepKindSchema.options.join(', ')}; got "${raw}"`)
|
|
277
|
+
}
|
|
278
|
+
return new Set(kinds.data)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* `upgrade [--yes] [--only <kinds>]`: the plan on stderr, a confirmation, then one JSON line with
|
|
283
|
+
* what happened.
|
|
284
|
+
*/
|
|
285
|
+
export async function runUpgrade(deps: UpgradeDeps, argv: string[], io: CliIo): Promise<number> {
|
|
286
|
+
const { values } = parseArgs({
|
|
287
|
+
args: argv,
|
|
288
|
+
options: { yes: { type: 'boolean', short: 'y' }, only: { type: 'string' } },
|
|
289
|
+
strict: true,
|
|
290
|
+
})
|
|
291
|
+
const only = parseOnly(values.only)
|
|
292
|
+
const full = await planUpgrade(deps)
|
|
293
|
+
const plan: UpgradePlan =
|
|
294
|
+
only === null
|
|
295
|
+
? full
|
|
296
|
+
: {
|
|
297
|
+
steps: full.steps.filter(step => only.has(step.kind)),
|
|
298
|
+
notes: [
|
|
299
|
+
...full.notes,
|
|
300
|
+
...full.steps
|
|
301
|
+
.filter(step => !only.has(step.kind))
|
|
302
|
+
.map(step => `left out by --only: ${describeStep(step)}`),
|
|
303
|
+
],
|
|
304
|
+
}
|
|
305
|
+
for (const note of plan.notes) io.stderr(` ${note}`)
|
|
306
|
+
if (plan.steps.length === 0) {
|
|
307
|
+
io.stderr('Everything is up to date.')
|
|
308
|
+
printJson(io, { applied: false, steps: [], notes: plan.notes })
|
|
309
|
+
return EXIT.ok
|
|
310
|
+
}
|
|
311
|
+
io.stderr('pr-review upgrade will:')
|
|
312
|
+
for (const step of plan.steps) io.stderr(` - ${describeStep(step)}`)
|
|
313
|
+
if (plan.steps.some(step => step.kind === 'package')) {
|
|
314
|
+
io.stderr(' Once pr-review is upgraded, the new version checks and runs the steps after it.')
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const confirmed = values.yes === true ? true : await deps.confirm('Proceed? [y/N] ')
|
|
318
|
+
if (confirmed !== true) {
|
|
319
|
+
io.stderr(confirmed === null ? 'Nothing changed. Re-run with --yes to apply.' : 'Nothing changed.')
|
|
320
|
+
printJson(io, { applied: false, steps: plan.steps, notes: plan.notes })
|
|
321
|
+
return EXIT.ok
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const outcomes = await applyUpgrade(plan, deps)
|
|
325
|
+
for (const outcome of outcomes) {
|
|
326
|
+
io.stderr(
|
|
327
|
+
` ${outcome.status}: ${describeStep(outcome)}${outcome.detail ? `\n ${outcome.detail}` : ''}`
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
// A skill step lists only the copies it wrote, failed or not.
|
|
331
|
+
const refreshed = outcomes.flatMap(o => (o.kind === 'skill' ? o.paths : []))
|
|
332
|
+
if (refreshed.length > 0) {
|
|
333
|
+
io.stderr(`The project skill changed. Commit and push ${refreshed.join(' and ')} so your team gets it.`)
|
|
334
|
+
}
|
|
335
|
+
const ok = outcomes.every(o => o.status !== 'failed')
|
|
336
|
+
printJson(io, { applied: true, ok, steps: outcomes, notes: plan.notes })
|
|
337
|
+
return ok ? EXIT.ok : EXIT.error
|
|
338
|
+
}
|
package/static/js/api.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/** @typedef {import('./contract-types.js').ErrorEnvelope} ErrorEnvelope */
|
|
3
|
+
/** @typedef {import('./contract-types.js').ReviewKey} ReviewKey */
|
|
3
4
|
/** @typedef {import('./contract-types.js').PrBundle} PrBundle */
|
|
4
5
|
/** @typedef {import('./contract-types.js').PatchesResponse} PatchesResponse */
|
|
5
6
|
|
|
@@ -69,17 +70,20 @@ export async function fetchJson(url, opts = {}) {
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
73
|
+
* The bundle for one target. `refresh` asks the server to read the forge, or the working tree,
|
|
74
|
+
* again; `poll` says this is the background poller, which is answered from the head the page was
|
|
75
|
+
* opened with rather than by snapshotting the working tree every few seconds.
|
|
76
|
+
* @param {ReviewKey} prNumber
|
|
77
|
+
* @param {{ refresh?: boolean, poll?: boolean, fetchImpl?: typeof fetch | undefined }} [opts]
|
|
74
78
|
* @returns {Promise<PrBundle>}
|
|
75
79
|
*/
|
|
76
80
|
export function fetchBundle(prNumber, opts = {}) {
|
|
77
|
-
const q = opts.refresh ? '?refresh=1' : ''
|
|
81
|
+
const q = opts.refresh ? '?refresh=1' : opts.poll ? '?poll=1' : ''
|
|
78
82
|
return fetchJson(`/api/prs/${prNumber}${q}`, { fetchImpl: opts.fetchImpl })
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
/**
|
|
82
|
-
* @param {
|
|
86
|
+
* @param {ReviewKey} prNumber
|
|
83
87
|
* @param {{ headSha?: string, fetchImpl?: typeof fetch }} [opts]
|
|
84
88
|
* @returns {Promise<PatchesResponse>}
|
|
85
89
|
*/
|
|
@@ -89,7 +93,7 @@ export function fetchPatches(prNumber, opts = {}) {
|
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
/**
|
|
92
|
-
* @param {
|
|
96
|
+
* @param {ReviewKey} prNumber
|
|
93
97
|
* @param {{ fetchImpl?: typeof fetch }} [opts]
|
|
94
98
|
* @returns {Promise<import('./contract-types.js').StateResponse>}
|
|
95
99
|
*/
|
|
@@ -98,14 +102,18 @@ export function fetchState(prNumber, opts = {}) {
|
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
/**
|
|
101
|
-
* @param {
|
|
105
|
+
* @param {ReviewKey} prNumber
|
|
102
106
|
* @param {string} id `layer:<id>` or `layer:<id>/file:<key>`
|
|
103
107
|
* @param {boolean} reviewed
|
|
104
|
-
* @param {{ headSha?: string, fetchImpl?: typeof fetch }} [opts]
|
|
108
|
+
* @param {{ headSha?: string, canvasSha?: string, fetchImpl?: typeof fetch }} [opts]
|
|
105
109
|
* @returns {Promise<import('./contract-types.js').StateResponse>}
|
|
106
110
|
*/
|
|
107
111
|
export function putReviewed(prNumber, id, reviewed, opts = {}) {
|
|
108
|
-
const body =
|
|
112
|
+
const body = {
|
|
113
|
+
reviewed,
|
|
114
|
+
...(opts.headSha === undefined ? {} : { headSha: opts.headSha }),
|
|
115
|
+
...(opts.canvasSha === undefined ? {} : { canvasSha: opts.canvasSha }),
|
|
116
|
+
}
|
|
109
117
|
return fetchJson(`/api/prs/${prNumber}/reviewed/${id}`, {
|
|
110
118
|
method: 'PUT',
|
|
111
119
|
body,
|
|
@@ -114,7 +122,7 @@ export function putReviewed(prNumber, id, reviewed, opts = {}) {
|
|
|
114
122
|
}
|
|
115
123
|
|
|
116
124
|
/**
|
|
117
|
-
* @param {
|
|
125
|
+
* @param {ReviewKey} prNumber
|
|
118
126
|
* @param {string} fingerprint
|
|
119
127
|
* @param {boolean} dismissed
|
|
120
128
|
* @param {{ reason?: string, fetchImpl?: typeof fetch }} [opts]
|
|
@@ -130,7 +138,7 @@ export function putDismissed(prNumber, fingerprint, dismissed, opts = {}) {
|
|
|
130
138
|
}
|
|
131
139
|
|
|
132
140
|
/**
|
|
133
|
-
* @param {
|
|
141
|
+
* @param {ReviewKey} prNumber
|
|
134
142
|
* @param {number} rootCommentId
|
|
135
143
|
* @param {boolean} hidden
|
|
136
144
|
* @param {{ fetchImpl?: typeof fetch }} [opts]
|
|
@@ -145,7 +153,7 @@ export function putThreadHidden(prNumber, rootCommentId, hidden, opts = {}) {
|
|
|
145
153
|
}
|
|
146
154
|
|
|
147
155
|
/**
|
|
148
|
-
* @param {
|
|
156
|
+
* @param {ReviewKey} prNumber
|
|
149
157
|
* @param {{ refresh?: boolean, fetchImpl?: typeof fetch }} [opts]
|
|
150
158
|
* @returns {Promise<import('./contract-types.js').Capabilities>}
|
|
151
159
|
*/
|
|
@@ -155,7 +163,7 @@ export function fetchCapabilities(prNumber, opts = {}) {
|
|
|
155
163
|
}
|
|
156
164
|
|
|
157
165
|
/**
|
|
158
|
-
* @param {
|
|
166
|
+
* @param {ReviewKey} prNumber
|
|
159
167
|
* @param {import('./contract-types.js').PostCommentInput} input
|
|
160
168
|
* @param {{ fetchImpl?: typeof fetch }} [opts]
|
|
161
169
|
* @returns {Promise<import('./contract-types.js').PostCommentResponse>}
|
|
@@ -169,7 +177,61 @@ export function postComment(prNumber, input, opts = {}) {
|
|
|
169
177
|
}
|
|
170
178
|
|
|
171
179
|
/**
|
|
172
|
-
*
|
|
180
|
+
* Adds one comment to the pending review. Nothing reaches the forge: it is kept in the local
|
|
181
|
+
* state until the review is submitted.
|
|
182
|
+
* @param {ReviewKey} prNumber
|
|
183
|
+
* @param {import('./contract-types.js').AddPendingInput} input
|
|
184
|
+
* @param {{ fetchImpl?: typeof fetch }} [opts]
|
|
185
|
+
* @returns {Promise<import('./contract-types.js').StateResponse>}
|
|
186
|
+
*/
|
|
187
|
+
export function addPending(prNumber, input, opts = {}) {
|
|
188
|
+
return fetchJson(`/api/prs/${prNumber}/pending`, {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
body: input,
|
|
191
|
+
fetchImpl: opts.fetchImpl,
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @param {ReviewKey} prNumber
|
|
197
|
+
* @param {string} id
|
|
198
|
+
* @param {string} body
|
|
199
|
+
* @param {{ fetchImpl?: typeof fetch }} [opts]
|
|
200
|
+
* @returns {Promise<import('./contract-types.js').StateResponse>}
|
|
201
|
+
*/
|
|
202
|
+
export function editPending(prNumber, id, body, opts = {}) {
|
|
203
|
+
return fetchJson(`/api/prs/${prNumber}/pending/${encodeURIComponent(id)}`, {
|
|
204
|
+
method: 'PATCH',
|
|
205
|
+
body: { body },
|
|
206
|
+
fetchImpl: opts.fetchImpl,
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* @param {ReviewKey} prNumber
|
|
212
|
+
* @param {string} id
|
|
213
|
+
* @param {{ fetchImpl?: typeof fetch }} [opts]
|
|
214
|
+
* @returns {Promise<import('./contract-types.js').StateResponse>}
|
|
215
|
+
*/
|
|
216
|
+
export function deletePending(prNumber, id, opts = {}) {
|
|
217
|
+
return fetchJson(`/api/prs/${prNumber}/pending/${encodeURIComponent(id)}`, {
|
|
218
|
+
method: 'DELETE',
|
|
219
|
+
fetchImpl: opts.fetchImpl,
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Throws the whole pending review away.
|
|
225
|
+
* @param {ReviewKey} prNumber
|
|
226
|
+
* @param {{ fetchImpl?: typeof fetch }} [opts]
|
|
227
|
+
* @returns {Promise<import('./contract-types.js').StateResponse>}
|
|
228
|
+
*/
|
|
229
|
+
export function discardPending(prNumber, opts = {}) {
|
|
230
|
+
return fetchJson(`/api/prs/${prNumber}/pending`, { method: 'DELETE', fetchImpl: opts.fetchImpl })
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* @param {ReviewKey} prNumber
|
|
173
235
|
* @param {{ fetchImpl?: typeof fetch }} [opts]
|
|
174
236
|
* @returns {Promise<import('./contract-types.js').ReviewBodyResponse>}
|
|
175
237
|
*/
|
|
@@ -178,8 +240,8 @@ export function fetchReviewBody(prNumber, opts = {}) {
|
|
|
178
240
|
}
|
|
179
241
|
|
|
180
242
|
/**
|
|
181
|
-
* @param {
|
|
182
|
-
* @param {{ event: '
|
|
243
|
+
* @param {ReviewKey} prNumber
|
|
244
|
+
* @param {{ event: import('./contract-types.js').ReviewEvent, body?: string, headSha?: string, includePending?: boolean }} input
|
|
183
245
|
* @param {{ fetchImpl?: typeof fetch }} [opts]
|
|
184
246
|
* @returns {Promise<import('./contract-types.js').PostReviewResponse>}
|
|
185
247
|
*/
|
|
@@ -210,7 +272,7 @@ export const POLL_SLOW_INTERVAL_MS = 15_000
|
|
|
210
272
|
* After five minutes of waiting the gap grows to 15 s, because a generation that has not finished
|
|
211
273
|
* by then takes minutes more. A failed poll is reported and polling goes on; the page never
|
|
212
274
|
* blocks on it.
|
|
213
|
-
* @param {
|
|
275
|
+
* @param {ReviewKey} prNumber
|
|
214
276
|
* @param {PollOptions} [opts]
|
|
215
277
|
* @returns {{ stop: () => void }}
|
|
216
278
|
*/
|
|
@@ -230,7 +292,7 @@ export function pollBundle(prNumber, opts = {}) {
|
|
|
230
292
|
/** @type {PrBundle} */
|
|
231
293
|
let bundle
|
|
232
294
|
try {
|
|
233
|
-
bundle = await fetchBundle(prNumber, { fetchImpl: opts.fetchImpl })
|
|
295
|
+
bundle = await fetchBundle(prNumber, { poll: true, fetchImpl: opts.fetchImpl })
|
|
234
296
|
} catch (err) {
|
|
235
297
|
if (!stopped) {
|
|
236
298
|
opts.onError?.(err)
|
|
@@ -306,7 +368,7 @@ export function uploadForm(url, form, opts = {}) {
|
|
|
306
368
|
}
|
|
307
369
|
|
|
308
370
|
/**
|
|
309
|
-
* @param {
|
|
371
|
+
* @param {ReviewKey} prNumber
|
|
310
372
|
* @param {File} file
|
|
311
373
|
* @param {{ force?: boolean, onProgress?: (fraction: number) => void, xhrImpl?: () => XMLHttpRequest }} [opts]
|
|
312
374
|
* @returns {Promise<import('./contract-types.js').ImportResult>}
|
|
@@ -321,7 +383,7 @@ export function importCanvas(prNumber, file, opts = {}) {
|
|
|
321
383
|
}
|
|
322
384
|
|
|
323
385
|
/**
|
|
324
|
-
* @param {
|
|
386
|
+
* @param {ReviewKey} prNumber
|
|
325
387
|
* @param {{ fetchImpl?: typeof fetch }} [opts]
|
|
326
388
|
* @returns {Promise<import('./contract-types.js').SharedCanvasFetchResponse>}
|
|
327
389
|
*/
|
|
@@ -331,7 +393,7 @@ export function fetchSharedCanvas(prNumber, opts = {}) {
|
|
|
331
393
|
|
|
332
394
|
/**
|
|
333
395
|
* The zip as a blob plus the name the server chose, ready to hand to the browser.
|
|
334
|
-
* @param {
|
|
396
|
+
* @param {ReviewKey} prNumber
|
|
335
397
|
* @param {{ headSha?: string, fetchImpl?: typeof fetch }} [opts]
|
|
336
398
|
* @returns {Promise<{ blob: Blob, filename: string }>}
|
|
337
399
|
*/
|
|
@@ -378,7 +440,7 @@ export function saveAppearance(input, opts = {}) {
|
|
|
378
440
|
/* ---- AI Chat and its settings ---- */
|
|
379
441
|
|
|
380
442
|
/**
|
|
381
|
-
* @param {
|
|
443
|
+
* @param {ReviewKey} prNumber
|
|
382
444
|
* @param {{ fetchImpl?: typeof fetch }} [opts]
|
|
383
445
|
* @returns {Promise<import('./contract-types.js').ChatThreadsResponse>}
|
|
384
446
|
*/
|
|
@@ -387,7 +449,7 @@ export function fetchThreads(prNumber, opts = {}) {
|
|
|
387
449
|
}
|
|
388
450
|
|
|
389
451
|
/**
|
|
390
|
-
* @param {
|
|
452
|
+
* @param {ReviewKey} prNumber
|
|
391
453
|
* @param {{ fetchImpl?: typeof fetch }} [opts]
|
|
392
454
|
* @returns {Promise<import('./contract-types.js').ChatThreadsResponse>}
|
|
393
455
|
*/
|
|
@@ -400,7 +462,7 @@ export function createThread(prNumber, opts = {}) {
|
|
|
400
462
|
}
|
|
401
463
|
|
|
402
464
|
/**
|
|
403
|
-
* @param {
|
|
465
|
+
* @param {ReviewKey} prNumber
|
|
404
466
|
* @param {string} name
|
|
405
467
|
* @param {{ fetchImpl?: typeof fetch }} [opts]
|
|
406
468
|
* @returns {Promise<import('./contract-types.js').ChatHistoryResponse>}
|
|
@@ -412,7 +474,7 @@ export function fetchThreadHistory(prNumber, name, opts = {}) {
|
|
|
412
474
|
}
|
|
413
475
|
|
|
414
476
|
/**
|
|
415
|
-
* @param {
|
|
477
|
+
* @param {ReviewKey} prNumber
|
|
416
478
|
* @param {{ fetchImpl?: typeof fetch }} [opts]
|
|
417
479
|
* @returns {Promise<{ cancelled: boolean }>}
|
|
418
480
|
*/
|
|
@@ -501,7 +563,7 @@ export function readSseFrames(buffer, chunk) {
|
|
|
501
563
|
/**
|
|
502
564
|
* Sends one chat message and calls `onEvent` for every frame until the turn ends. Rejects with
|
|
503
565
|
* an ApiError when the server refuses the message (a busy chat, a context it cannot resolve).
|
|
504
|
-
* @param {
|
|
566
|
+
* @param {ReviewKey} prNumber
|
|
505
567
|
* @param {{ message: string, context: import('./chat-context.js').ChatContext, thread?: string }} input
|
|
506
568
|
* @param {{
|
|
507
569
|
* onEvent: (event: { event: string, data: unknown }) => void,
|