bottleship 0.1.0-prototype.1 → 0.1.0-prototype.2
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/CHANGELOG.md +10 -0
- package/README.md +118 -117
- package/dist/src/boxes.d.ts +41 -39
- package/dist/src/boxes.js +181 -179
- package/dist/src/cli.js +298 -84
- package/dist/src/config.d.ts +3 -6
- package/dist/src/config.js +19 -85
- package/dist/src/init.d.ts +2 -4
- package/dist/src/init.js +92 -228
- package/dist/src/local.d.ts +8 -0
- package/dist/src/local.js +24 -0
- package/dist/src/onboard.d.ts +5 -0
- package/dist/src/onboard.js +25 -0
- package/dist/src/paths.d.ts +0 -1
- package/dist/src/paths.js +0 -1
- package/dist/src/provider.d.ts +16 -0
- package/dist/src/provider.js +81 -0
- package/dist/src/repo.d.ts +21 -0
- package/dist/src/repo.js +75 -0
- package/dist/src/review/comments.d.ts +66 -0
- package/dist/src/review/comments.js +344 -0
- package/dist/src/review/contract.d.ts +82 -0
- package/dist/src/review/contract.js +189 -0
- package/dist/src/review/github.d.ts +120 -0
- package/dist/src/review/github.js +160 -0
- package/dist/src/review/local.d.ts +14 -0
- package/dist/src/review/local.js +70 -0
- package/dist/src/review/loop.d.ts +12 -0
- package/dist/src/review/loop.js +119 -0
- package/dist/src/review/pr.d.ts +14 -0
- package/dist/src/review/pr.js +68 -0
- package/dist/src/review/prompt.d.ts +15 -0
- package/dist/src/review/prompt.js +48 -0
- package/dist/src/review/serve.d.ts +1 -0
- package/dist/src/review/serve.js +8 -0
- package/dist/src/review/server.d.ts +14 -0
- package/dist/src/review/server.js +484 -0
- package/dist/src/review/setting-up.d.ts +4 -0
- package/dist/src/review/setting-up.js +74 -0
- package/dist/src/review/start.d.ts +32 -0
- package/dist/src/review/start.js +140 -0
- package/dist/src/review/target.d.ts +20 -0
- package/dist/src/review/target.js +66 -0
- package/dist/src/review/template.d.ts +3 -0
- package/dist/src/review/template.js +13 -0
- package/dist/src/review/terminal.d.ts +4 -0
- package/dist/src/review/terminal.js +91 -0
- package/dist/src/types.d.ts +1 -71
- package/dist/src/ui.d.ts +6 -1
- package/dist/src/ui.js +22 -23
- package/dist/src/util.d.ts +11 -0
- package/dist/src/util.js +40 -0
- package/package.json +7 -14
- package/presets/agents/claude.json +1 -5
- package/presets/agents/codex.json +1 -10
- package/presets/agents/cursor.json +1 -9
- package/presets/agents/opencode.json +1 -14
- package/presets/providers/docker/create +12 -0
- package/presets/providers/docker/destroy +2 -0
- package/presets/providers/docker/exec +2 -0
- package/presets/providers/docker/list +4 -0
- package/presets/providers/docker/status +2 -0
- package/presets/providers/docker/url +4 -0
- package/review-app/KIT.md +106 -0
- package/review-app/WRITING.md +93 -0
- package/review-app/examples/rply-16.tsx +98 -0
- package/review-app/index.html +15 -0
- package/review-app/package-lock.json +2539 -0
- package/review-app/package.json +24 -0
- package/review-app/playground/fixtures/excalidraw-12602/data/box.json +1 -0
- package/review-app/playground/fixtures/excalidraw-12602/data/pr.diff +91 -0
- package/review-app/playground/fixtures/excalidraw-12602/data/pr.json +62 -0
- package/review-app/playground/fixtures/excalidraw-12602/data/review-comments.json +44 -0
- package/review-app/playground/fixtures/excalidraw-12602/events.json +1 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-ar-zoom.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-en-US-zoom.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-he-IL-zoom.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-he-dark.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-he-hover.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-he-page.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-he-tip.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-he-zen.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-ar-zoom.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-en-US-zoom.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-he-IL-zoom.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-he-dark.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-he-hover.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-he-page.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-he-tip.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-he-zen.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/instances.json +1 -0
- package/review-app/playground/fixtures/excalidraw-12602/review.tsx +103 -0
- package/review-app/playground/fixtures/rply-16/data/box.json +1 -0
- package/review-app/playground/fixtures/rply-16/data/hooks.diff +28 -0
- package/review-app/playground/fixtures/rply-16/data/pr.diff +114 -0
- package/review-app/playground/fixtures/rply-16/data/pr.json +68 -0
- package/review-app/playground/fixtures/rply-16/data/review-comments.json +1 -0
- package/review-app/playground/fixtures/rply-16/events.json +156 -0
- package/review-app/playground/fixtures/rply-16/instances.json +1 -0
- package/review-app/playground/fixtures/rply-16/review.tsx +98 -0
- package/review-app/playground/gallery.tsx +127 -0
- package/review-app/playground/index.html +15 -0
- package/review-app/playground/main.tsx +26 -0
- package/review-app/playground/mock.ts +215 -0
- package/review-app/playground/playground.css +18 -0
- package/review-app/playground/vite.config.ts +32 -0
- package/review-app/scripts/check.cjs +47 -0
- package/review-app/scripts/clip.cjs +104 -0
- package/review-app/scripts/shot.cjs +66 -0
- package/review-app/src/App.tsx +132 -0
- package/review-app/src/data/box.json +1 -0
- package/review-app/src/data/hooks.diff +28 -0
- package/review-app/src/data/pr.diff +114 -0
- package/review-app/src/data/pr.json +68 -0
- package/review-app/src/data/review-comments.json +1 -0
- package/review-app/src/kit/agent.tsx +287 -0
- package/review-app/src/kit/chapter.tsx +178 -0
- package/review-app/src/kit/code.tsx +217 -0
- package/review-app/src/kit/comments.tsx +377 -0
- package/review-app/src/kit/finish.tsx +274 -0
- package/review-app/src/kit/flow.tsx +184 -0
- package/review-app/src/kit/index.ts +10 -0
- package/review-app/src/kit/kit.css +696 -0
- package/review-app/src/kit/live.ts +91 -0
- package/review-app/src/kit/media.tsx +348 -0
- package/review-app/src/kit/overview.tsx +161 -0
- package/review-app/src/kit/pr.ts +100 -0
- package/review-app/src/kit/pullrequest.tsx +298 -0
- package/review-app/src/kit/review.ts +74 -0
- package/review-app/src/kit/theme.ts +30 -0
- package/review-app/src/kit/ui.tsx +127 -0
- package/review-app/src/main.tsx +6 -0
- package/review-app/src/review.tsx +22 -0
- package/review-app/tsconfig.json +17 -0
- package/review-app/vite.config.ts +12 -0
- package/dist/presets/providers/command.d.ts +0 -3
- package/dist/presets/providers/command.js +0 -39
- package/dist/presets/providers/daytona.d.ts +0 -3
- package/dist/presets/providers/daytona.js +0 -173
- package/dist/presets/providers/docker.d.ts +0 -3
- package/dist/presets/providers/docker.js +0 -31
- package/dist/presets/providers/e2b.d.ts +0 -3
- package/dist/presets/providers/e2b.js +0 -124
- package/dist/presets/providers/exe.d.ts +0 -3
- package/dist/presets/providers/exe.js +0 -28
- package/dist/presets/providers/http.example.d.ts +0 -3
- package/dist/presets/providers/http.example.js +0 -47
- package/dist/presets/providers/local.d.ts +0 -3
- package/dist/presets/providers/local.js +0 -52
- package/dist/presets/providers/modal.d.ts +0 -3
- package/dist/presets/providers/modal.js +0 -117
- package/dist/presets/providers/ssh.d.ts +0 -3
- package/dist/presets/providers/ssh.js +0 -49
- package/dist/presets/providers/util.d.ts +0 -13
- package/dist/presets/providers/util.js +0 -102
- package/presets/providers/command.ts +0 -43
- package/presets/providers/daytona.ts +0 -181
- package/presets/providers/docker.ts +0 -38
- package/presets/providers/e2b.ts +0 -130
- package/presets/providers/exe.ts +0 -35
- package/presets/providers/http.example.ts +0 -64
- package/presets/providers/local.ts +0 -56
- package/presets/providers/modal.ts +0 -121
- package/presets/providers/ssh.ts +0 -54
- package/presets/providers/util.ts +0 -112
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// A provider is a directory of bash scripts that run boxes somewhere. Docker ships with bottleship;
|
|
2
|
+
// for any other sandbox an agent writes the same scripts into ~/.bottleship/providers/<name>/.
|
|
3
|
+
//
|
|
4
|
+
// create <id> <dockerfile> <image> build the box from the Dockerfile (cache it as <image>), start it, exit once it runs commands
|
|
5
|
+
// exec <id> <script> run `bash -c <script>` in the box as its user, stdin passed through
|
|
6
|
+
// status <id> print running or gone
|
|
7
|
+
// destroy <id> remove the box and everything in it
|
|
8
|
+
// url <id> <port> (optional) print a URL on this machine for a port the box exposes
|
|
9
|
+
// list (optional) print the boxes bottleship made, one per line: <id> [<created, ISO 8601>]
|
|
10
|
+
import { existsSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { HOME, ROOT } from "./paths.js";
|
|
14
|
+
import { BsError } from "./config.js";
|
|
15
|
+
import { run, spawnStream } from "./util.js";
|
|
16
|
+
const SCRIPTS = ["create", "exec", "status", "destroy"];
|
|
17
|
+
const dirs = () => [join(HOME, "providers"), join(ROOT, "presets/providers")];
|
|
18
|
+
// user providers first, so a user can replace the builtin docker scripts
|
|
19
|
+
export function providerNames() {
|
|
20
|
+
const names = dirs().flatMap((d) => (existsSync(d) ? readdirSync(d).filter((n) => existsSync(join(d, n, "create"))) : []));
|
|
21
|
+
return [...new Set(names)];
|
|
22
|
+
}
|
|
23
|
+
export function getProvider(name) {
|
|
24
|
+
const dir = dirs().map((d) => join(d, name)).find((d) => existsSync(d));
|
|
25
|
+
if (!dir)
|
|
26
|
+
throw new BsError("unknown_provider", `no provider "${name}"`, `known: ${providerNames().join(", ")}. A provider is a directory of scripts in ${join(HOME, "providers")}, see "Providers" in ${join(ROOT, "README.md")}`);
|
|
27
|
+
const missing = SCRIPTS.filter((s) => !existsSync(join(dir, s)));
|
|
28
|
+
if (missing.length)
|
|
29
|
+
throw new BsError("bad_provider", `provider "${name}" (${dir}) has no ${missing.join(", ")} script`, `add them, see "Providers" in ${join(ROOT, "README.md")}`);
|
|
30
|
+
const script = (s) => join(dir, s);
|
|
31
|
+
const call = (s, args, stdin) => run("bash", [script(s), ...args], { stdin, check: false });
|
|
32
|
+
const failed = (s, r) => new Error(`${name}/${s} exited ${r.code}: ${(r.stderr || r.stdout).trim().slice(-2000)}`);
|
|
33
|
+
return {
|
|
34
|
+
name,
|
|
35
|
+
async create(id, dockerfile, image) {
|
|
36
|
+
// the Dockerfile goes in a file, since providers build it with their own tools
|
|
37
|
+
const tmp = mkdtempSync(join(tmpdir(), "bs-df-"));
|
|
38
|
+
try {
|
|
39
|
+
writeFileSync(join(tmp, "Dockerfile"), dockerfile);
|
|
40
|
+
const r = await call("create", [id, join(tmp, "Dockerfile"), image]);
|
|
41
|
+
if (r.code !== 0)
|
|
42
|
+
throw failed("create", r);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
exec: (id, s, stdin) => call("exec", [id, s], stdin),
|
|
49
|
+
stream: (id, s) => spawnStream("bash", [script("exec"), id, s]),
|
|
50
|
+
async status(id) {
|
|
51
|
+
const r = await call("status", [id]);
|
|
52
|
+
if (r.code !== 0)
|
|
53
|
+
throw failed("status", r);
|
|
54
|
+
return r.stdout.trim() === "running" ? "running" : "gone";
|
|
55
|
+
},
|
|
56
|
+
async destroy(id) {
|
|
57
|
+
const r = await call("destroy", [id]);
|
|
58
|
+
if (r.code !== 0)
|
|
59
|
+
throw failed("destroy", r);
|
|
60
|
+
},
|
|
61
|
+
...(existsSync(script("url")) && {
|
|
62
|
+
async url(id, port) {
|
|
63
|
+
const r = await call("url", [id, String(port)]);
|
|
64
|
+
if (r.code !== 0)
|
|
65
|
+
throw failed("url", r);
|
|
66
|
+
return r.stdout.trim();
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
...(existsSync(script("list")) && {
|
|
70
|
+
async list() {
|
|
71
|
+
const r = await call("list", []);
|
|
72
|
+
if (r.code !== 0)
|
|
73
|
+
throw failed("list", r);
|
|
74
|
+
return r.stdout.split("\n").filter(Boolean).map((l) => {
|
|
75
|
+
const [box, created] = l.trim().split(/\s+/);
|
|
76
|
+
return { box, ...(created && { created }) };
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface Repo {
|
|
2
|
+
url: string;
|
|
3
|
+
label: string;
|
|
4
|
+
}
|
|
5
|
+
export interface RepoEnv {
|
|
6
|
+
dockerfile?: string;
|
|
7
|
+
ports: number[];
|
|
8
|
+
}
|
|
9
|
+
export declare const git: (cwd: string, ...args: string[]) => string;
|
|
10
|
+
export declare function toRepo(s: string): Repo;
|
|
11
|
+
export declare function checkout(cwd: string): {
|
|
12
|
+
root: string;
|
|
13
|
+
origin?: Repo;
|
|
14
|
+
remote?: string;
|
|
15
|
+
} | undefined;
|
|
16
|
+
export declare function dotenvFiles(root: string): Record<string, string>;
|
|
17
|
+
export declare function readRepoEnv(root: string): RepoEnv | undefined;
|
|
18
|
+
export declare function readEnvFiles(root: string): Record<string, string> | undefined;
|
|
19
|
+
export declare function parseRepoEnv(dockerfile: string | undefined): RepoEnv;
|
|
20
|
+
export declare function workingTree(root: string): Promise<Buffer>;
|
|
21
|
+
export declare function tarFiles(root: string, files: string[]): Promise<Buffer>;
|
package/dist/src/repo.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// The user's local checkout: its origin, its .env files, its working tree, and its box environment in .bottleship/.
|
|
2
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
3
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { BsError } from "./config.js";
|
|
6
|
+
import { run } from "./util.js";
|
|
7
|
+
export const git = (cwd, ...args) => {
|
|
8
|
+
try {
|
|
9
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return "";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
// owner/name or a GitHub URL becomes an https clone URL, so the GH_TOKEN helper applies; anything else is cloned as is
|
|
16
|
+
export function toRepo(s) {
|
|
17
|
+
const gh = s.match(/^([\w.-]+\/[\w.-]+?)(?:\.git)?$/) ?? s.match(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
18
|
+
return gh ? { url: `https://github.com/${gh[1]}.git`, label: gh[1] } : { url: s, label: s };
|
|
19
|
+
}
|
|
20
|
+
// the checkout around cwd, if any
|
|
21
|
+
// remote: the origin URL as git has it
|
|
22
|
+
export function checkout(cwd) {
|
|
23
|
+
const root = git(cwd, "rev-parse", "--show-toplevel");
|
|
24
|
+
if (!root)
|
|
25
|
+
return undefined;
|
|
26
|
+
const origin = git(root, "remote", "get-url", "origin");
|
|
27
|
+
return { root, ...(origin && { origin: toRepo(origin), remote: origin }) };
|
|
28
|
+
}
|
|
29
|
+
// .env files at the repo root, minus templates like .env.example
|
|
30
|
+
export function dotenvFiles(root) {
|
|
31
|
+
const names = readdirSync(root).filter((f) => /^\.env/.test(f) && !/\.(example|sample|template)$/.test(f) && statSync(join(root, f)).isFile());
|
|
32
|
+
return Object.fromEntries(names.map((f) => [f, readFileSync(join(root, f), "utf8")]));
|
|
33
|
+
}
|
|
34
|
+
// undefined when the repo has no .bottleship/ yet
|
|
35
|
+
export function readRepoEnv(root) {
|
|
36
|
+
const files = readEnvFiles(root);
|
|
37
|
+
return files && parseRepoEnv(files.Dockerfile);
|
|
38
|
+
}
|
|
39
|
+
// every file in the checkout's .bottleship/, by name
|
|
40
|
+
export function readEnvFiles(root) {
|
|
41
|
+
const dir = join(root, ".bottleship");
|
|
42
|
+
if (!existsSync(dir))
|
|
43
|
+
return undefined;
|
|
44
|
+
const names = readdirSync(dir).filter((f) => statSync(join(dir, f)).isFile());
|
|
45
|
+
return Object.fromEntries(names.map((f) => [f, readFileSync(join(dir, f), "utf8")]));
|
|
46
|
+
}
|
|
47
|
+
// the image lines from .bottleship/Dockerfile and the ports it EXPOSEs
|
|
48
|
+
export function parseRepoEnv(dockerfile) {
|
|
49
|
+
if (dockerfile === undefined)
|
|
50
|
+
return { ports: [] };
|
|
51
|
+
if (/^\s*FROM\s/im.test(dockerfile))
|
|
52
|
+
throw new BsError("bad_dockerfile", ".bottleship/Dockerfile has a FROM line", "drop it: the lines are appended to bottleship's base image, see \"Repo environment\" in the README");
|
|
53
|
+
const ports = [...dockerfile.matchAll(/^\s*EXPOSE\s+(.+)$/gim)].flatMap((m) => m[1].trim().split(/\s+/)).map((p) => Number(p.replace(/\/tcp$/, ""))).filter((p) => Number.isInteger(p) && p > 0);
|
|
54
|
+
return { dockerfile, ports };
|
|
55
|
+
}
|
|
56
|
+
// ponytail: tracked and untracked files minus .gitignore'd ones, without .git; send .git too if setup scripts turn out to need history
|
|
57
|
+
export async function workingTree(root) {
|
|
58
|
+
const listed = (await run("git", ["ls-files", "-z", "-co", "--exclude-standard"], { cwd: root })).stdout;
|
|
59
|
+
// ls-files still lists files deleted from the working tree but not from the index
|
|
60
|
+
return tarFiles(root, listed.split("\0").filter((f) => f && existsSync(join(root, f))));
|
|
61
|
+
}
|
|
62
|
+
// a tar of these files, relative to root
|
|
63
|
+
export function tarFiles(root, files) {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
// COPYFILE_DISABLE keeps macOS tar from adding ._ files
|
|
66
|
+
const tar = spawn("tar", ["-c", "--no-xattrs", "--null", "-T", "-"], { cwd: root, env: { ...process.env, COPYFILE_DISABLE: "1" }, stdio: ["pipe", "pipe", "pipe"] });
|
|
67
|
+
const chunks = [];
|
|
68
|
+
let stderr = "";
|
|
69
|
+
tar.stdout.on("data", (d) => chunks.push(d));
|
|
70
|
+
tar.stderr.on("data", (d) => (stderr += d));
|
|
71
|
+
tar.on("error", reject);
|
|
72
|
+
tar.on("close", (code) => (code === 0 ? resolve(Buffer.concat(chunks)) : reject(new Error(`tar exited ${code}: ${stderr.trim()}`))));
|
|
73
|
+
tar.stdin.end(files.join("\0"));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type GhRepo } from "./github.ts";
|
|
2
|
+
import type { CommentInput, Quote, ReviewComment, Verdict } from "./contract.ts";
|
|
3
|
+
export interface PrTarget {
|
|
4
|
+
repo: GhRepo;
|
|
5
|
+
number: number;
|
|
6
|
+
head?: string;
|
|
7
|
+
}
|
|
8
|
+
export type Asked = "started" | "running" | "failed";
|
|
9
|
+
export interface Request {
|
|
10
|
+
id: string;
|
|
11
|
+
where: string;
|
|
12
|
+
body: string;
|
|
13
|
+
quotes?: Quote[];
|
|
14
|
+
thread: {
|
|
15
|
+
by: string;
|
|
16
|
+
body: string;
|
|
17
|
+
}[];
|
|
18
|
+
}
|
|
19
|
+
export interface Waited {
|
|
20
|
+
comments: Request[];
|
|
21
|
+
done?: {
|
|
22
|
+
note?: string;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
type Fail = {
|
|
26
|
+
ok: false;
|
|
27
|
+
code: number;
|
|
28
|
+
errors: string[];
|
|
29
|
+
};
|
|
30
|
+
type Result<T> = {
|
|
31
|
+
ok: true;
|
|
32
|
+
value: T;
|
|
33
|
+
} | Fail;
|
|
34
|
+
export interface StoreOptions {
|
|
35
|
+
file: string;
|
|
36
|
+
pr: () => PrTarget | undefined;
|
|
37
|
+
ask?: (message: string) => Promise<Asked>;
|
|
38
|
+
running?: () => Promise<boolean>;
|
|
39
|
+
watchMs?: number;
|
|
40
|
+
url: () => string;
|
|
41
|
+
changed: (comments: ReviewComment[]) => void;
|
|
42
|
+
listening: (on: boolean) => void;
|
|
43
|
+
}
|
|
44
|
+
export declare function commentStore(o: StoreOptions): {
|
|
45
|
+
list: () => ReviewComment[];
|
|
46
|
+
listening: () => boolean;
|
|
47
|
+
create(input: CommentInput, by?: {
|
|
48
|
+
agent: true;
|
|
49
|
+
}): Promise<Result<ReviewComment>>;
|
|
50
|
+
edit(id: string, body: string): Promise<Result<ReviewComment>>;
|
|
51
|
+
drop(id: string): Promise<Result<null>>;
|
|
52
|
+
retry(id: string): Result<ReviewComment>;
|
|
53
|
+
answer(id: string, a: {
|
|
54
|
+
body: string;
|
|
55
|
+
commit?: string;
|
|
56
|
+
}): Result<ReviewComment>;
|
|
57
|
+
finish(note?: string): void;
|
|
58
|
+
wait(ms: number): Promise<Waited>;
|
|
59
|
+
submit(v: Verdict): Promise<Result<{
|
|
60
|
+
url: string;
|
|
61
|
+
}>>;
|
|
62
|
+
sync(): Promise<void>;
|
|
63
|
+
};
|
|
64
|
+
export type CommentStore = ReturnType<typeof commentStore>;
|
|
65
|
+
export declare function boxMessage(requests: Request[], url: string, number: number): string;
|
|
66
|
+
export {};
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// The comments in a review and where they go. To GitHub: each one lands in the reviewer's pending review (or posts
|
|
2
|
+
// at once as a single comment) as it is written, so the tour and github.com always agree. To an agent: requests it
|
|
3
|
+
// answers, handed to the box agent as they come in a PR review, or collected for the developer's own agent in a
|
|
4
|
+
// local one, which picks them up with `bottleship agent review next`. Persisted to comments.json.
|
|
5
|
+
import { readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { addReviewReply, addReviewThread, commentStates, deleteGhComment, editGhComment, pendingReview, postSingle, postSingleReply, reviewThreads, startPendingReview, submitReview } from "./github.js";
|
|
7
|
+
const fail = (code, ...errors) => ({ ok: false, code, errors });
|
|
8
|
+
// enough for any real review, and a bound on what the box keeps
|
|
9
|
+
const MAX_COMMENTS = 1000;
|
|
10
|
+
// a wait that ended this recently still counts as an agent listening: it is about to wait again
|
|
11
|
+
const LISTEN_GRACE = 15_000;
|
|
12
|
+
const RETRY = 5_000;
|
|
13
|
+
// how often a PR review checks that the box agent still runs while it has comments
|
|
14
|
+
const WATCH = 4_000;
|
|
15
|
+
export function commentStore(o) {
|
|
16
|
+
let comments = [];
|
|
17
|
+
try {
|
|
18
|
+
const v = JSON.parse(readFileSync(o.file, "utf8"));
|
|
19
|
+
// drafts from before comments had a recipient were meant for GitHub; sync posts them to the pending review
|
|
20
|
+
// a single quote came before quotes
|
|
21
|
+
if (Array.isArray(v))
|
|
22
|
+
comments = v.map(({ quote, ...c }) => ({ by: "reviewer", to: "github", ...c, ...(quote && { quotes: [{ ...quote, kind: "selection" }] }) }));
|
|
23
|
+
}
|
|
24
|
+
catch { }
|
|
25
|
+
let next = comments.reduce((n, c) => Math.max(n, Number(c.id.replace(/^c/, "")) || 0), 0) + 1;
|
|
26
|
+
let done;
|
|
27
|
+
const waiters = [];
|
|
28
|
+
let lastWait = 0;
|
|
29
|
+
let heard = false;
|
|
30
|
+
let dispatching = false;
|
|
31
|
+
let retry;
|
|
32
|
+
let watching;
|
|
33
|
+
const save = () => {
|
|
34
|
+
writeFileSync(o.file + ".tmp", JSON.stringify(comments, null, 2));
|
|
35
|
+
renameSync(o.file + ".tmp", o.file);
|
|
36
|
+
o.changed(comments);
|
|
37
|
+
};
|
|
38
|
+
const byId = (id) => comments.find((c) => c.id === id);
|
|
39
|
+
const add = (c) => {
|
|
40
|
+
const full = { ...c, id: `c${next++}`, at: new Date().toISOString() };
|
|
41
|
+
comments.push(full);
|
|
42
|
+
return full;
|
|
43
|
+
};
|
|
44
|
+
const lineOf = (c) => ({ path: c.path, side: c.side, line: c.line, ...(c.startLine && { startLine: c.startLine, startSide: c.startSide }) });
|
|
45
|
+
const github = async (f) => {
|
|
46
|
+
try {
|
|
47
|
+
return { ok: true, value: await f() };
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
return fail(502, e.message);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
// the comment a thread here starts from
|
|
54
|
+
const rootOf = (c) => {
|
|
55
|
+
let r = c;
|
|
56
|
+
for (let i = 0; r.replyTo && i < MAX_COMMENTS; i++)
|
|
57
|
+
r = byId(r.replyTo) ?? r;
|
|
58
|
+
return r;
|
|
59
|
+
};
|
|
60
|
+
const where = (c) => {
|
|
61
|
+
const r = rootOf(c);
|
|
62
|
+
// a chat message says which page the reviewer was on when they wrote it
|
|
63
|
+
if (c.topic?.chat)
|
|
64
|
+
return c.topic.label;
|
|
65
|
+
if (r.topic)
|
|
66
|
+
return r.topic.label;
|
|
67
|
+
const lines = r.startLine && r.startLine !== r.line ? `lines ${r.startLine} to ${r.line}` : `line ${r.line}`;
|
|
68
|
+
return `${r.path}, ${lines} of the ${r.side === "LEFT" ? "old" : "new"} file${r.inReplyTo ? `, in the GitHub thread on comment ${r.inReplyTo} (src/data/review-comments.json)` : ""}`;
|
|
69
|
+
};
|
|
70
|
+
const request = (c) => {
|
|
71
|
+
const root = rootOf(c);
|
|
72
|
+
const thread = comments.filter((x) => x !== c && x.at <= c.at && rootOf(x) === root).map((x) => ({ by: x.by === "agent" ? "agent" : "reviewer", body: x.body }));
|
|
73
|
+
return { id: c.id, where: where(c), body: c.body, ...(c.quotes?.length && { quotes: c.quotes }), thread };
|
|
74
|
+
};
|
|
75
|
+
const isListening = () => waiters.length > 0 || Date.now() - lastWait < LISTEN_GRACE;
|
|
76
|
+
const tellListening = () => {
|
|
77
|
+
const on = isListening();
|
|
78
|
+
if (on !== heard)
|
|
79
|
+
o.listening((heard = on));
|
|
80
|
+
};
|
|
81
|
+
// the developer's agent picks up what was sent, and hears when the reviewer is done
|
|
82
|
+
function take() {
|
|
83
|
+
const sent = comments.filter((c) => c.to === "agent" && c.by === "reviewer" && c.status === "sent");
|
|
84
|
+
sent.forEach((c) => (c.status = "working"));
|
|
85
|
+
if (sent.length)
|
|
86
|
+
save();
|
|
87
|
+
const out = { comments: sent.map(request), ...(done && { done }) };
|
|
88
|
+
done = undefined;
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
function wake() {
|
|
92
|
+
if (!waiters.length)
|
|
93
|
+
return;
|
|
94
|
+
const w = waiters.shift();
|
|
95
|
+
clearTimeout(w.timer);
|
|
96
|
+
w.give(take());
|
|
97
|
+
tellListening();
|
|
98
|
+
}
|
|
99
|
+
// a box agent whose run ends without answering has stopped on those comments; the reviewer can send them again
|
|
100
|
+
const inFlight = () => comments.filter((c) => c.to === "agent" && c.by === "reviewer" && c.status === "working");
|
|
101
|
+
function watch() {
|
|
102
|
+
if (watching || !o.running || !o.pr())
|
|
103
|
+
return;
|
|
104
|
+
watching = setInterval(async () => {
|
|
105
|
+
if (!inFlight().length)
|
|
106
|
+
return void (clearInterval(watching), (watching = undefined));
|
|
107
|
+
if (await o.running())
|
|
108
|
+
return;
|
|
109
|
+
// an answer may have landed just before the run ended
|
|
110
|
+
const left = inFlight();
|
|
111
|
+
if (!left.length)
|
|
112
|
+
return;
|
|
113
|
+
left.forEach((c) => (c.status = "stopped"));
|
|
114
|
+
save();
|
|
115
|
+
}, o.watchMs ?? WATCH);
|
|
116
|
+
watching.unref();
|
|
117
|
+
}
|
|
118
|
+
// the box agent gets every comment sent to it so far in one follow-up, once it is free
|
|
119
|
+
async function dispatch() {
|
|
120
|
+
if (!o.ask || dispatching)
|
|
121
|
+
return;
|
|
122
|
+
const sent = comments.filter((c) => c.to === "agent" && c.by === "reviewer" && c.status === "sent");
|
|
123
|
+
if (!sent.length)
|
|
124
|
+
return;
|
|
125
|
+
dispatching = true;
|
|
126
|
+
try {
|
|
127
|
+
const r = await o.ask(boxMessage(sent.map(request), o.url(), o.pr()?.number ?? 0));
|
|
128
|
+
if (r === "started") {
|
|
129
|
+
sent.forEach((c) => (c.status = "working"));
|
|
130
|
+
save();
|
|
131
|
+
watch();
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
clearTimeout(retry);
|
|
135
|
+
retry = setTimeout(() => void dispatch(), RETRY);
|
|
136
|
+
retry.unref();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
dispatching = false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// comments left working when the server last stopped are checked too
|
|
144
|
+
watch();
|
|
145
|
+
return {
|
|
146
|
+
list: () => comments,
|
|
147
|
+
listening: isListening,
|
|
148
|
+
// agent: the agent writes it because the reviewer asked; it is GitHub's and the reviewer's like any other
|
|
149
|
+
async create(input, by) {
|
|
150
|
+
if (comments.length >= MAX_COMMENTS)
|
|
151
|
+
return fail(409, `at most ${MAX_COMMENTS} comments`);
|
|
152
|
+
const pr = o.pr();
|
|
153
|
+
const { mode, ...c } = input;
|
|
154
|
+
if (c.replyTo && !byId(c.replyTo))
|
|
155
|
+
return fail(400, `replyTo: no comment ${c.replyTo}`);
|
|
156
|
+
if (c.to === "agent") {
|
|
157
|
+
const made = add({ ...c, by: "reviewer", status: "sent" });
|
|
158
|
+
save();
|
|
159
|
+
if (pr)
|
|
160
|
+
void dispatch();
|
|
161
|
+
else
|
|
162
|
+
wake();
|
|
163
|
+
return { ok: true, value: made };
|
|
164
|
+
}
|
|
165
|
+
if (!pr)
|
|
166
|
+
return fail(409, "these are local changes with no PR: comments go to your agent");
|
|
167
|
+
const at = lineOf(c);
|
|
168
|
+
const r = await github(async () => {
|
|
169
|
+
const p = await pendingReview(pr.repo, pr.number);
|
|
170
|
+
if (mode === "single") {
|
|
171
|
+
if (p.review)
|
|
172
|
+
throw new Error("you have a review in progress on GitHub: add the comment to it");
|
|
173
|
+
if (c.inReplyTo)
|
|
174
|
+
return { state: "posted", ...(await postSingleReply(pr.repo, pr.number, c.inReplyTo, c.body)) };
|
|
175
|
+
if (!pr.head)
|
|
176
|
+
throw new Error("the PR's head commit is unknown");
|
|
177
|
+
return { state: "posted", ...(await postSingle(pr.repo, pr.number, pr.head, at, c.body)) };
|
|
178
|
+
}
|
|
179
|
+
const review = p.review?.id ?? (await startPendingReview(pr.repo, p.pr, pr.head)).id;
|
|
180
|
+
if (c.inReplyTo) {
|
|
181
|
+
const thread = comments.find((x) => x.github?.id === c.inReplyTo)?.github?.thread ?? (await reviewThreads(pr.repo, pr.number)).find((t) => t.root === c.inReplyTo)?.id;
|
|
182
|
+
if (!thread)
|
|
183
|
+
throw new Error("the thread it replies to is gone");
|
|
184
|
+
return { state: "pending", ...(await addReviewReply(pr.repo, review, thread, c.body)) };
|
|
185
|
+
}
|
|
186
|
+
return { state: "pending", ...(await addReviewThread(pr.repo, review, at, c.body)) };
|
|
187
|
+
});
|
|
188
|
+
if (!r.ok)
|
|
189
|
+
return r;
|
|
190
|
+
const made = add({ ...c, by: "reviewer", ...(by?.agent && { agent: true }), github: r.value });
|
|
191
|
+
save();
|
|
192
|
+
return { ok: true, value: made };
|
|
193
|
+
},
|
|
194
|
+
async edit(id, body) {
|
|
195
|
+
const c = byId(id);
|
|
196
|
+
if (!c || c.by !== "reviewer")
|
|
197
|
+
return fail(404, `no comment ${id} of yours`);
|
|
198
|
+
if (c.to === "agent" && c.status !== "sent" && c.status !== "stopped")
|
|
199
|
+
return fail(409, "the agent is already working on it");
|
|
200
|
+
const pr = o.pr();
|
|
201
|
+
if (c.github && pr) {
|
|
202
|
+
const r = await github(() => editGhComment(pr.repo, c.github.node, body));
|
|
203
|
+
if (!r.ok)
|
|
204
|
+
return r;
|
|
205
|
+
}
|
|
206
|
+
c.body = body;
|
|
207
|
+
save();
|
|
208
|
+
return { ok: true, value: c };
|
|
209
|
+
},
|
|
210
|
+
async drop(id) {
|
|
211
|
+
const c = byId(id);
|
|
212
|
+
if (!c || c.by !== "reviewer")
|
|
213
|
+
return fail(404, `no comment ${id} of yours`);
|
|
214
|
+
if (c.to === "agent" && c.status !== "sent" && c.status !== "stopped")
|
|
215
|
+
return fail(409, "the agent is already working on it");
|
|
216
|
+
const pr = o.pr();
|
|
217
|
+
if (c.github && pr) {
|
|
218
|
+
const r = await github(() => deleteGhComment(pr.repo, c.github.node));
|
|
219
|
+
if (!r.ok)
|
|
220
|
+
return r;
|
|
221
|
+
}
|
|
222
|
+
comments = comments.filter((x) => x !== c);
|
|
223
|
+
save();
|
|
224
|
+
return { ok: true, value: null };
|
|
225
|
+
},
|
|
226
|
+
// sends a comment the agent stopped on again
|
|
227
|
+
retry(id) {
|
|
228
|
+
const c = byId(id);
|
|
229
|
+
if (!c || c.to !== "agent" || c.by !== "reviewer" || c.status !== "stopped")
|
|
230
|
+
return fail(409, `${id} is not a comment the agent stopped on`);
|
|
231
|
+
c.status = "sent";
|
|
232
|
+
save();
|
|
233
|
+
void dispatch();
|
|
234
|
+
return { ok: true, value: c };
|
|
235
|
+
},
|
|
236
|
+
// an agent's answer to a comment sent to it, with the commit it made
|
|
237
|
+
answer(id, a) {
|
|
238
|
+
const c = byId(id);
|
|
239
|
+
if (!c || c.to !== "agent" || c.by !== "reviewer")
|
|
240
|
+
return fail(404, `no comment ${id} sent to the agent`);
|
|
241
|
+
c.status = "done";
|
|
242
|
+
const made = add({ by: "agent", to: "agent", replyTo: id, body: a.body, ...(a.commit && { commit: a.commit }) });
|
|
243
|
+
save();
|
|
244
|
+
return { ok: true, value: made };
|
|
245
|
+
},
|
|
246
|
+
// local reviews: the reviewer is done, with a last word for the agent
|
|
247
|
+
finish(note) {
|
|
248
|
+
done = { ...(note?.trim() && { note: note.trim() }) };
|
|
249
|
+
wake();
|
|
250
|
+
},
|
|
251
|
+
// what was sent since the last call, or what arrives within ms
|
|
252
|
+
wait(ms) {
|
|
253
|
+
lastWait = Date.now();
|
|
254
|
+
if (done || comments.some((c) => c.to === "agent" && c.status === "sent")) {
|
|
255
|
+
tellListening();
|
|
256
|
+
return Promise.resolve(take());
|
|
257
|
+
}
|
|
258
|
+
return new Promise((give) => {
|
|
259
|
+
const w = {
|
|
260
|
+
give,
|
|
261
|
+
timer: setTimeout(() => {
|
|
262
|
+
waiters.splice(waiters.indexOf(w), 1);
|
|
263
|
+
lastWait = Date.now();
|
|
264
|
+
give({ comments: [] });
|
|
265
|
+
setTimeout(tellListening, LISTEN_GRACE + 100).unref();
|
|
266
|
+
}, ms),
|
|
267
|
+
};
|
|
268
|
+
waiters.push(w);
|
|
269
|
+
tellListening();
|
|
270
|
+
});
|
|
271
|
+
},
|
|
272
|
+
// submits the reviewer's pending review with the verdict; the comments in it are posted now
|
|
273
|
+
async submit(v) {
|
|
274
|
+
const pr = o.pr();
|
|
275
|
+
if (!pr)
|
|
276
|
+
return fail(409, "this review has no PR to submit to");
|
|
277
|
+
if (!v.event)
|
|
278
|
+
return fail(400, "choose a verdict before submitting");
|
|
279
|
+
const r = await github(async () => {
|
|
280
|
+
const p = await pendingReview(pr.repo, pr.number);
|
|
281
|
+
return submitReview(pr.repo, { pr: p.pr, review: p.review?.id, head: pr.head, event: v.event, body: v.body?.trim() || undefined });
|
|
282
|
+
});
|
|
283
|
+
if (!r.ok)
|
|
284
|
+
return r;
|
|
285
|
+
comments.forEach((c) => c.github?.state === "pending" && (c.github.state = "posted"));
|
|
286
|
+
save();
|
|
287
|
+
return { ok: true, value: { url: r.value } };
|
|
288
|
+
},
|
|
289
|
+
// brings the store in line with GitHub: pending comments changed, added, deleted or submitted on github.com
|
|
290
|
+
async sync() {
|
|
291
|
+
const pr = o.pr();
|
|
292
|
+
if (!pr)
|
|
293
|
+
return;
|
|
294
|
+
const before = JSON.stringify(comments);
|
|
295
|
+
const p = await pendingReview(pr.repo, pr.number);
|
|
296
|
+
const there = new Map((p.review?.comments ?? []).map((c) => [c.node, c]));
|
|
297
|
+
const gone = comments.filter((c) => c.github?.state === "pending" && !there.has(c.github.node));
|
|
298
|
+
const states = await commentStates(pr.repo, gone.map((c) => c.github.node));
|
|
299
|
+
comments = comments.filter((c) => !(c.github && gone.includes(c) && states[c.github.node] === null));
|
|
300
|
+
for (const c of gone)
|
|
301
|
+
if (states[c.github.node] === "SUBMITTED")
|
|
302
|
+
c.github.state = "posted";
|
|
303
|
+
const mine = new Set(comments.map((c) => c.github?.node));
|
|
304
|
+
for (const c of comments) {
|
|
305
|
+
const g = c.github && there.get(c.github.node);
|
|
306
|
+
if (g)
|
|
307
|
+
c.body = g.body;
|
|
308
|
+
}
|
|
309
|
+
for (const g of there.values()) {
|
|
310
|
+
if (mine.has(g.node) || !g.line)
|
|
311
|
+
continue;
|
|
312
|
+
add({ by: "reviewer", to: "github", body: g.body, path: g.path, side: g.side, line: g.line, ...(g.startLine && { startLine: g.startLine, startSide: g.startSide ?? g.side }), ...(g.replyTo && { inReplyTo: g.replyTo }), github: { state: "pending", node: g.node, id: g.id, ...(g.url && { url: g.url }) } });
|
|
313
|
+
}
|
|
314
|
+
// drafts from an older box that never reached GitHub go into the pending review now
|
|
315
|
+
for (const c of comments.filter((x) => x.to === "github" && !x.github)) {
|
|
316
|
+
const { id: _id, at: _at, by: _by, github: _g, ...rest } = c;
|
|
317
|
+
const made = await this.create({ ...rest, mode: "review" });
|
|
318
|
+
if (made.ok)
|
|
319
|
+
comments = comments.filter((x) => x !== c);
|
|
320
|
+
}
|
|
321
|
+
if (JSON.stringify(comments) !== before)
|
|
322
|
+
save();
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
// the follow-up the box agent gets in a PR review, with everything it needs to act on it
|
|
327
|
+
export function boxMessage(requests, url, number) {
|
|
328
|
+
const one = (r) => [
|
|
329
|
+
`${r.id}, from ${r.where}:`,
|
|
330
|
+
...r.thread.map((t) => ` earlier, ${t.by}: ${t.body.replace(/\n/g, "\n ")}`),
|
|
331
|
+
...(r.quotes ?? []).flatMap((q) => [` quoting ${q.kind === "comment" ? (q.where ?? "a comment") : `what they selected${q.where ? ` in ${q.where}` : ""}`}:`, ...q.text.split("\n").map((l) => ` > ${l}`)]),
|
|
332
|
+
` ${r.body.replace(/\n/g, "\n ")}`,
|
|
333
|
+
].join("\n");
|
|
334
|
+
return `The reviewer wrote to you in the tour's chat:
|
|
335
|
+
|
|
336
|
+
${requests.map(one).join("\n\n")}
|
|
337
|
+
|
|
338
|
+
- Answer what they ask, checking in the box what you need to. When the answer is a change to lines in the diff, you may include it as a \`\`\`suggestion block that replaces exactly those lines.
|
|
339
|
+
- Change code only when they ask you to: change it in ~/workspace, check it in the running app, commit, and push to the PR's branch (gh pr view ${number} --json headRepository,headRepositoryOwner,headRefName says where; a fork takes it only if its author lets maintainers edit).
|
|
340
|
+
- Write your answers as ~/review/app/WRITING.md says: the answer first, then the evidence, short enough to read in under a minute.
|
|
341
|
+
- Answer every message, even when you change nothing: curl -s -X POST -H 'content-type: application/json' -d '{"body":"<what you did, or why not>","commit":"<the sha, if you committed>"}' ${url}/api/comments/<id>/answer
|
|
342
|
+
- If you pushed, refresh the tour without rebuilding it: gh pr diff ${number} > ~/review/app/src/data/pr.diff, update headRefOid and files in pr.json (gh pr view ${number} --json headRefOid,files), restart head if needed, fix the groups' line ranges and whatever in review.tsx is now wrong, rerun the steps the change affects, npm run check. Then note the update with one sentence on what changed: curl -s -X POST -H 'content-type: application/json' -d '{"note":"<what changed>"}' ${url}/api/updates
|
|
343
|
+
- Never comment on GitHub on your own. Only when the reviewer explicitly asks you to leave a comment, add it to their pending review, where only they see it until they submit: curl -s -X POST -H 'content-type: application/json' -d '{"path":"<file>","side":"RIGHT","line":<last line>,"startSide":"RIGHT","startLine":<first line, for a range>,"body":"<the comment>"}' ${url}/api/comments/agent ("side":"LEFT" for lines of the old file; "inReplyTo":<id of a thread's first comment> to reply in a GitHub thread). Add "mode":"single" only if they asked for it to be posted right away. Never review or approve: the reviewer submits their own review.`;
|
|
344
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export type Status = "ok" | "fail" | "neutral";
|
|
2
|
+
export interface ReviewEvent {
|
|
3
|
+
instance: string;
|
|
4
|
+
kind: "request" | "hook" | "ok" | "fail";
|
|
5
|
+
text: string;
|
|
6
|
+
at?: string;
|
|
7
|
+
step?: string;
|
|
8
|
+
flow?: {
|
|
9
|
+
id: string;
|
|
10
|
+
edge?: [string, string];
|
|
11
|
+
node?: string;
|
|
12
|
+
status?: Status;
|
|
13
|
+
label?: string;
|
|
14
|
+
};
|
|
15
|
+
overlay?: {
|
|
16
|
+
title: string;
|
|
17
|
+
body?: string;
|
|
18
|
+
status: Status;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
type Errors = string[];
|
|
22
|
+
export declare function validateEvent(v: unknown): Errors;
|
|
23
|
+
export type Side = "LEFT" | "RIGHT";
|
|
24
|
+
export interface ReviewComment {
|
|
25
|
+
id: string;
|
|
26
|
+
at: string;
|
|
27
|
+
by: "reviewer" | "agent";
|
|
28
|
+
to: "github" | "agent";
|
|
29
|
+
body: string;
|
|
30
|
+
path?: string;
|
|
31
|
+
side?: Side;
|
|
32
|
+
line?: number;
|
|
33
|
+
startSide?: Side;
|
|
34
|
+
startLine?: number;
|
|
35
|
+
topic?: {
|
|
36
|
+
chapter?: string;
|
|
37
|
+
chat?: boolean;
|
|
38
|
+
label: string;
|
|
39
|
+
};
|
|
40
|
+
quotes?: Quote[];
|
|
41
|
+
inReplyTo?: number;
|
|
42
|
+
replyTo?: string;
|
|
43
|
+
agent?: true;
|
|
44
|
+
github?: {
|
|
45
|
+
state: "pending" | "posted";
|
|
46
|
+
node: string;
|
|
47
|
+
id: number;
|
|
48
|
+
url?: string;
|
|
49
|
+
thread?: string;
|
|
50
|
+
};
|
|
51
|
+
status?: "sent" | "working" | "done" | "stopped";
|
|
52
|
+
commit?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface Quote {
|
|
55
|
+
text: string;
|
|
56
|
+
where?: string;
|
|
57
|
+
kind?: "selection" | "comment";
|
|
58
|
+
}
|
|
59
|
+
export type CommentInput = Pick<ReviewComment, "to" | "body" | "path" | "side" | "line" | "startSide" | "startLine" | "topic" | "quotes" | "inReplyTo" | "replyTo"> & {
|
|
60
|
+
mode?: "single" | "review";
|
|
61
|
+
};
|
|
62
|
+
export declare const MAX_COMMENT = 65536;
|
|
63
|
+
export declare function validateComment(v: unknown): Errors;
|
|
64
|
+
export declare function validateAnswer(v: unknown): Errors;
|
|
65
|
+
export declare const EVENTS: readonly ["COMMENT", "APPROVE", "REQUEST_CHANGES"];
|
|
66
|
+
export interface Verdict {
|
|
67
|
+
event?: (typeof EVENTS)[number];
|
|
68
|
+
body?: string;
|
|
69
|
+
}
|
|
70
|
+
export declare function validateVerdict(v: unknown): Errors;
|
|
71
|
+
export declare const STAGES: {
|
|
72
|
+
readonly read: "Reading the change";
|
|
73
|
+
readonly instances: "Starting base and head";
|
|
74
|
+
readonly hooks: "Adding review hooks";
|
|
75
|
+
readonly steps: "Writing the steps";
|
|
76
|
+
readonly shots: "Taking screenshots";
|
|
77
|
+
readonly tour: "Writing the tour";
|
|
78
|
+
readonly check: "Checking every page";
|
|
79
|
+
};
|
|
80
|
+
export type Stage = keyof typeof STAGES;
|
|
81
|
+
export declare function validateProgress(v: unknown): Errors;
|
|
82
|
+
export {};
|