co-maintainer 0.5.0 → 0.5.1-beta.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/LICENSE +21 -21
- package/README.md +5 -3
- package/dist/package.json +1 -1
- package/dist/src/ai/anthropic.d.ts +55 -0
- package/dist/src/ai/anthropic.js +262 -0
- package/dist/src/ai/batch.d.ts +2 -2
- package/dist/src/ai/batch.js +1 -1
- package/dist/src/ai/estimate.d.ts +4 -0
- package/dist/src/ai/estimate.js +17 -2
- package/dist/src/ai/mermaid_loop.js +7 -4
- package/dist/src/ai/openai.d.ts +45 -0
- package/dist/src/ai/openai.js +206 -0
- package/dist/src/ai/opencode.d.ts +10 -0
- package/dist/src/ai/opencode.js +76 -0
- package/dist/src/ai/openrouter.d.ts +23 -7
- package/dist/src/ai/openrouter.js +39 -23
- package/dist/src/ai/pricing.js +1 -1
- package/dist/src/ai/provider.d.ts +9 -0
- package/dist/src/ai/provider.js +42 -3
- package/dist/src/ai/verify.d.ts +9 -1
- package/dist/src/ai/verify.js +81 -3
- package/dist/src/cli/args.d.ts +1 -2
- package/dist/src/cli/args.js +38 -26
- package/dist/src/cli/commands/config.d.ts +1 -1
- package/dist/src/cli/commands/config.js +10 -4
- package/dist/src/cli/commands/probe.js +3 -11
- package/dist/src/cli/commands/registry.d.ts +1 -9
- package/dist/src/cli/commands/registry.js +75 -10
- package/dist/src/cli/commands/review.js +18 -13
- package/dist/src/cli/commands/rollback.d.ts +1 -0
- package/dist/src/cli/commands/rollback.js +42 -0
- package/dist/src/cli/commands/serve.js +2 -3
- package/dist/src/cli/commands/set.js +71 -7
- package/dist/src/cli/commands/view.js +10 -9
- package/dist/src/cli/error.d.ts +4 -4
- package/dist/src/cli/error.js +4 -4
- package/dist/src/cli/main.d.ts +4 -4
- package/dist/src/cli/main.js +13 -8
- package/dist/src/cli/prompt.js +1 -1
- package/dist/src/cli/review_args.d.ts +9 -3
- package/dist/src/cli/review_args.js +28 -5
- package/dist/src/cli/review_github.d.ts +31 -0
- package/dist/src/cli/review_github.js +128 -0
- package/dist/src/cli/review_result.d.ts +27 -16
- package/dist/src/cli/review_result.js +7 -7
- package/dist/src/config.d.ts +28 -6
- package/dist/src/config.js +15 -1
- package/dist/src/git/default_branch.d.ts +1 -1
- package/dist/src/git/default_branch.js +1 -1
- package/dist/src/github/app.js +2 -2
- package/dist/src/github/app_manifest.d.ts +1 -7
- package/dist/src/github/app_manifest.js +2 -7
- package/dist/src/github/client.d.ts +4 -1
- package/dist/src/github/client.js +14 -2
- package/dist/src/github/gh.js +10 -7
- package/dist/src/github/pat.js +4 -4
- package/dist/src/github/permissions.d.ts +36 -0
- package/dist/src/github/permissions.js +334 -0
- package/dist/src/knowledge/facts.js +1 -1
- package/dist/src/knowledge/guide.d.ts +3 -3
- package/dist/src/knowledge/guide.js +5 -5
- package/dist/src/knowledge/probe.js +1 -2
- package/dist/src/knowledge/sections.d.ts +3 -3
- package/dist/src/knowledge/sections.js +3 -3
- package/dist/src/knowledge/skill.d.ts +1 -1
- package/dist/src/knowledge/skill.js +4 -4
- package/dist/src/knowledge/synthesis.d.ts +1 -1
- package/dist/src/knowledge/synthesis.js +5 -5
- package/dist/src/knowledge/types.d.ts +1 -1
- package/dist/src/local/codegraph_exclude.d.ts +1 -1
- package/dist/src/local/codegraph_exclude.js +1 -1
- package/dist/src/local/codegraph_prepare.js +1 -1
- package/dist/src/local/git_ops.d.ts +1 -1
- package/dist/src/local/git_ops.js +1 -1
- package/dist/src/local/git_parse.d.ts +2 -2
- package/dist/src/local/git_parse.js +2 -2
- package/dist/src/local/git_revision.d.ts +1 -1
- package/dist/src/local/git_revision.js +1 -1
- package/dist/src/local/git_untracked.d.ts +1 -1
- package/dist/src/local/git_untracked.js +1 -1
- package/dist/src/local/review_local.js +17 -14
- package/dist/src/local/review_lock.d.ts +1 -1
- package/dist/src/local/review_lock.js +1 -1
- package/dist/src/pr/checkout.js +2 -2
- package/dist/src/pr/findings.js +1 -1
- package/dist/src/pr/findings_json.d.ts +2 -2
- package/dist/src/pr/findings_json.js +1 -1
- package/dist/src/pr/github_revision.js +1 -1
- package/dist/src/pr/review_copy.d.ts +2 -2
- package/dist/src/pr/review_copy.js +2 -2
- package/dist/src/pr/reviewer.d.ts +6 -0
- package/dist/src/pr/reviewer.js +49 -39
- package/dist/src/remote/client.d.ts +4 -0
- package/dist/src/remote/client.js +84 -23
- package/dist/src/remote/fixtures.js +1 -1
- package/dist/src/remote/http.d.ts +10 -1
- package/dist/src/remote/http.js +15 -3
- package/dist/src/remote/schema.d.ts +3 -3
- package/dist/src/remote/schema.js +3 -3
- package/dist/src/remote/server/body.d.ts +1 -1
- package/dist/src/remote/server/body.js +1 -1
- package/dist/src/remote/server/byok_keys.d.ts +15 -0
- package/dist/src/remote/server/byok_keys.js +40 -0
- package/dist/src/remote/server/guides.js +2 -2
- package/dist/src/remote/server/routes.js +8 -1
- package/dist/src/remote/validate.js +15 -1
- package/dist/src/review/blocking.d.ts +2 -2
- package/dist/src/review/blocking.js +2 -2
- package/dist/src/review/carry_over.d.ts +2 -2
- package/dist/src/review/carry_over.js +2 -2
- package/dist/src/review/guides.js +1 -1
- package/dist/src/server/api/remote_tokens.js +2 -0
- package/dist/src/server/api/repos.js +51 -0
- package/dist/src/server/api/settings.js +10 -0
- package/dist/src/server/errors.d.ts +1 -1
- package/dist/src/server/http.js +1 -1
- package/dist/src/server/pages/activity.d.ts +2 -0
- package/dist/src/server/pages/activity.js +18 -4
- package/dist/src/server/pages/add_repo.js +61 -1
- package/dist/src/server/pages/analytics.js +18 -8
- package/dist/src/server/pages/cost.d.ts +21 -0
- package/dist/src/server/pages/cost.js +72 -0
- package/dist/src/server/pages/pr.js +5 -4
- package/dist/src/server/pages/repo.js +3 -2
- package/dist/src/server/pages/repo_prs.js +3 -2
- package/dist/src/server/pages/repo_remote.js +3 -2
- package/dist/src/server/pages/repo_settings.js +326 -12
- package/dist/src/server/pages/router.js +3 -2
- package/dist/src/server/pages/settings.js +50 -6
- package/dist/src/server/pages/styles.d.ts +1 -1
- package/dist/src/server/pages/styles.js +1 -1
- package/dist/src/services/credentials.js +8 -11
- package/dist/src/services/dashboard.d.ts +22 -9
- package/dist/src/services/dashboard.js +16 -2
- package/dist/src/services/probe.js +1 -1
- package/dist/src/services/remote_review.js +142 -35
- package/dist/src/services/remote_review_abort.d.ts +5 -1
- package/dist/src/services/remote_review_abort.js +9 -1
- package/dist/src/services/review.d.ts +1 -1
- package/dist/src/services/review.js +19 -5
- package/dist/src/services/review_policy.d.ts +116 -0
- package/dist/src/services/review_policy.js +489 -0
- package/dist/src/services/setup.d.ts +4 -3
- package/dist/src/services/setup.js +18 -16
- package/dist/src/services/setup_checklist.js +2 -2
- package/dist/src/services/webhook.d.ts +23 -0
- package/dist/src/services/webhook.js +141 -34
- package/dist/src/store/app_db.d.ts +5 -0
- package/dist/src/store/app_db.js +69 -13
- package/dist/src/store/backup.d.ts +31 -0
- package/dist/src/store/backup.js +158 -0
- package/dist/src/store/migrations.d.ts +1 -1
- package/dist/src/store/migrations.js +21 -1
- package/dist/src/store/remote_review_inputs.d.ts +3 -0
- package/dist/src/store/remote_review_inputs.js +3 -3
- package/dist/src/store/remote_tokens.d.ts +1 -1
- package/dist/src/store/remote_tokens.js +1 -1
- package/dist/src/store/repos.d.ts +2 -2
- package/dist/src/store/repos.js +1 -1
- package/dist/src/store/reviews.d.ts +21 -13
- package/dist/src/store/reviews.js +60 -15
- package/dist/src/store/rows.d.ts +10 -0
- package/dist/src/tools/codegraph.d.ts +4 -4
- package/dist/src/tools/codegraph.js +5 -5
- package/dist/src/tools/codegraph_exec.d.ts +1 -1
- package/dist/src/tools/codegraph_exec.js +1 -1
- package/dist/src/types.d.ts +12 -6
- package/dist/src/util/cost.d.ts +52 -0
- package/dist/src/util/cost.js +70 -0
- package/dist/src/util/log.d.ts +1 -1
- package/dist/src/util/log.js +2 -2
- package/dist/src/util/redact.js +2 -0
- package/dist/src/util/run_summary.d.ts +7 -2
- package/dist/src/util/run_summary.js +28 -2
- package/dist/src/util/time.d.ts +1 -1
- package/dist/src/util/time.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/** `review --output=github`.
|
|
2
|
+
*
|
|
3
|
+
* Findings become GitHub workflow commands (`::error` and `::warning`) so a
|
|
4
|
+
* pull request shows them as annotations, and a markdown table goes to the job
|
|
5
|
+
* summary. This file is pure apart from `appendStepSummary`.
|
|
6
|
+
*
|
|
7
|
+
* A finding's text comes from a model reading an untrusted pull request, so it
|
|
8
|
+
* is never printed raw: the runner reads a workflow command from any line that
|
|
9
|
+
* starts with `::`, and a body that carried such a line could mask a secret or
|
|
10
|
+
* end the step's commands. Every finding is one escaped line, and nothing else
|
|
11
|
+
* from the review is printed to stdout in this mode. */
|
|
12
|
+
import { appendFileSync } from "node:fs";
|
|
13
|
+
import { formatRunSummary } from "../util/run_summary.js";
|
|
14
|
+
import { humanShortTitle, sortHumanFindings, } from "./review_result.js";
|
|
15
|
+
/** GitHub shows at most this many error and this many warning annotations per
|
|
16
|
+
* step and drops the rest without a word. */
|
|
17
|
+
export const MAX_ANNOTATIONS_PER_LEVEL = 10;
|
|
18
|
+
/** The message part of a workflow command. GitHub's own toolkit escapes exactly
|
|
19
|
+
* these three, and a `:` or `,` here would show as `%3A` and `%2C` in the text. */
|
|
20
|
+
export function escapeData(value) {
|
|
21
|
+
return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
22
|
+
}
|
|
23
|
+
/** A property value (`file`, `title`) also cannot hold `:` or `,`, which end it. */
|
|
24
|
+
export function escapeProperty(value) {
|
|
25
|
+
return escapeData(value).replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
26
|
+
}
|
|
27
|
+
function annotation(row) {
|
|
28
|
+
const properties = [];
|
|
29
|
+
if (row.path) {
|
|
30
|
+
properties.push(`file=${escapeProperty(row.path)}`);
|
|
31
|
+
const from = row.lineFrom ?? 0;
|
|
32
|
+
if (from > 0) {
|
|
33
|
+
properties.push(`line=${from}`);
|
|
34
|
+
const to = row.lineTo ?? from;
|
|
35
|
+
if (to > from)
|
|
36
|
+
properties.push(`endLine=${to}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const short = humanShortTitle(row);
|
|
40
|
+
properties.push(`title=${escapeProperty(short ? `${row.severity}: ${short}` : `${row.severity} finding`)}`);
|
|
41
|
+
const level = row.blocking ? "error" : "warning";
|
|
42
|
+
return `::${level} ${properties.join(",")}::${escapeData(row.body || row.title)}`;
|
|
43
|
+
}
|
|
44
|
+
function active(findings) {
|
|
45
|
+
return sortHumanFindings(findings).filter((row) => row.state !== "closed");
|
|
46
|
+
}
|
|
47
|
+
/** One workflow command per finding still standing, errors first. A finding
|
|
48
|
+
* that was closed since the last review needs no annotation. */
|
|
49
|
+
export function githubAnnotations(findings) {
|
|
50
|
+
const rows = active(findings);
|
|
51
|
+
const errors = rows.filter((row) => row.blocking);
|
|
52
|
+
const warnings = rows.filter((row) => !row.blocking);
|
|
53
|
+
const shown = [
|
|
54
|
+
...errors.slice(0, MAX_ANNOTATIONS_PER_LEVEL),
|
|
55
|
+
...warnings.slice(0, MAX_ANNOTATIONS_PER_LEVEL),
|
|
56
|
+
];
|
|
57
|
+
const lines = shown.map(annotation);
|
|
58
|
+
const left = rows.length - shown.length;
|
|
59
|
+
if (left > 0) {
|
|
60
|
+
lines.push(`::notice title=co-maintainer::${escapeData(`${left} more ${left === 1 ? "finding is" : "findings are"} not annotated, GitHub shows ${MAX_ANNOTATIONS_PER_LEVEL} errors and ${MAX_ANNOTATIONS_PER_LEVEL} warnings per step. The job summary lists all of them.`)}`);
|
|
61
|
+
}
|
|
62
|
+
return lines;
|
|
63
|
+
}
|
|
64
|
+
/** Markdown for a table cell: nothing the model wrote can become a link, an
|
|
65
|
+
* image, a tag or an extra column. */
|
|
66
|
+
function cell(value, limit = 300) {
|
|
67
|
+
const flat = value.replace(/\s+/g, " ").trim();
|
|
68
|
+
const cut = flat.length > limit ? `${flat.slice(0, limit - 1)}…` : flat;
|
|
69
|
+
return cut.replace(/[\\`*_{}[\]<>|]/g, "\\$&");
|
|
70
|
+
}
|
|
71
|
+
function where(row) {
|
|
72
|
+
if (!row.path)
|
|
73
|
+
return "";
|
|
74
|
+
const from = row.lineFrom ?? 0;
|
|
75
|
+
const to = row.lineTo ?? from;
|
|
76
|
+
const span = from <= 0 ? "" : from === to ? `:${from}` : `:${from}-${to}`;
|
|
77
|
+
return cell(`${row.path}${span}`, 120);
|
|
78
|
+
}
|
|
79
|
+
export function githubSummary(input) {
|
|
80
|
+
const rows = active(input.findings);
|
|
81
|
+
const closed = input.findings.filter((row) => row.state === "closed").length;
|
|
82
|
+
const blocking = rows.filter((row) => row.blocking).length;
|
|
83
|
+
const lines = ["## co-maintainer review", "", cell(input.title, 200), ""];
|
|
84
|
+
if (rows.length === 0) {
|
|
85
|
+
lines.push("No actionable findings.");
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
lines.push("| Severity | State | Where | Finding |", "|---|---|---|---|");
|
|
89
|
+
for (const row of rows) {
|
|
90
|
+
const short = humanShortTitle(row);
|
|
91
|
+
const text = [short, row.body.split("\n")[0] ?? ""]
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.join(": ");
|
|
94
|
+
lines.push(`| ${cell(row.severity, 10)}${row.blocking ? " (blocking)" : ""} | ${row.state} | ${where(row)} | ${cell(text || row.title)} |`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
lines.push("", `**Summary:** ${rows.filter((r) => r.state === "new").length} new · ${rows.filter((r) => r.state === "open").length} open · ${closed} closed · ${blocking} blocking`);
|
|
98
|
+
if (input.run)
|
|
99
|
+
lines.push("", formatRunSummary(input.run));
|
|
100
|
+
return lines.join("\n");
|
|
101
|
+
}
|
|
102
|
+
/** Appends to `$GITHUB_STEP_SUMMARY` when it is set. A summary that cannot be
|
|
103
|
+
* written is reported on stderr and never fails the review. */
|
|
104
|
+
export function appendStepSummary(markdown, path = process.env.GITHUB_STEP_SUMMARY) {
|
|
105
|
+
if (!path)
|
|
106
|
+
return;
|
|
107
|
+
try {
|
|
108
|
+
appendFileSync(path, `${markdown}\n`);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
console.error(`Could not write the job summary: ${String(error)}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** The whole `--output=github` output of one finished review. */
|
|
115
|
+
export function emitGithubOutput(input) {
|
|
116
|
+
for (const line of githubAnnotations(input.findings))
|
|
117
|
+
console.log(line);
|
|
118
|
+
appendStepSummary(githubSummary(input));
|
|
119
|
+
}
|
|
120
|
+
/** Progress lines that come from somewhere else (the server's job log) are
|
|
121
|
+
* printed on stderr, and the runner reads workflow commands from stderr too.
|
|
122
|
+
* A line that would start one is given a prefix so it stays plain text. */
|
|
123
|
+
export function neutralizeCommands(text) {
|
|
124
|
+
return text
|
|
125
|
+
.split("\n")
|
|
126
|
+
.map((line) => (line.trimStart().startsWith("::") ? `[log] ${line}` : line))
|
|
127
|
+
.join("\n");
|
|
128
|
+
}
|
|
@@ -2,6 +2,7 @@ import { type ParsedFinding } from "../pr/findings.ts";
|
|
|
2
2
|
import { type ResolvedFinding } from "../review/carry_over.ts";
|
|
3
3
|
import { type ReviewBlocking } from "../review/blocking.ts";
|
|
4
4
|
import type { Revision } from "../review/revision.ts";
|
|
5
|
+
import type { BilledTo, CostReason, CostStatus } from "../util/cost.ts";
|
|
5
6
|
export type ReviewWarning = {
|
|
6
7
|
code: string;
|
|
7
8
|
message: string;
|
|
@@ -44,7 +45,7 @@ export declare function summaryCounts(findings: ResolvedFinding[], mode?: Review
|
|
|
44
45
|
export declare function reviewExitCodeFromResolved(findings: ResolvedFinding[], mode?: ReviewBlocking): number;
|
|
45
46
|
/** One finding as the human renderer prints it. Local, PR and remote reviews
|
|
46
47
|
* all normalize into this shape before printing, so the three modes cannot
|
|
47
|
-
* drift into different products
|
|
48
|
+
* drift into different products. */
|
|
48
49
|
export type HumanFinding = {
|
|
49
50
|
state: "new" | "open" | "closed";
|
|
50
51
|
severity: string;
|
|
@@ -61,12 +62,20 @@ export type HumanFinding = {
|
|
|
61
62
|
} | null;
|
|
62
63
|
};
|
|
63
64
|
/** Local and PR findings: blocking is decided here from the configured rule,
|
|
64
|
-
* exactly as the exit code decides it
|
|
65
|
+
* exactly as the exit code decides it. */
|
|
65
66
|
export declare function humanFindingsFromResolved(findings: ResolvedFinding[], mode?: ReviewBlocking): HumanFinding[];
|
|
66
67
|
/** Remote findings already carry the server's blocking decision, so the label
|
|
67
68
|
* uses that field rather than re-deriving the rule on a client that may not
|
|
68
|
-
* share the server's config
|
|
69
|
+
* share the server's config. */
|
|
69
70
|
export declare function humanFindingsFromJson(findings: JsonReviewFinding[]): HumanFinding[];
|
|
71
|
+
/** The finding heading, as the shared header prints it. A parsed heading is
|
|
72
|
+
* `[P2 · non-blocking] \`path\`: \`symbol\``. The label and the location are
|
|
73
|
+
* already printed beside it, so only the symbol survives. An older heading
|
|
74
|
+
* that used an em dash is still accepted, so comments posted before 0.5.0
|
|
75
|
+
* stay readable. A heading with no symbol leaves nothing: printing the label
|
|
76
|
+
* or the path again would repeat what the line already says. */
|
|
77
|
+
export declare function humanShortTitle(row: HumanFinding): string;
|
|
78
|
+
export declare function sortHumanFindings(rows: HumanFinding[]): HumanFinding[];
|
|
70
79
|
export type HumanReviewInput = {
|
|
71
80
|
/** Header line: repo, subject and mode. */
|
|
72
81
|
title: string;
|
|
@@ -74,13 +83,13 @@ export type HumanReviewInput = {
|
|
|
74
83
|
stats?: string;
|
|
75
84
|
guideBuiltAt: string | null;
|
|
76
85
|
/** `null` when the run cannot say (a server older than 0.5.0 sends no
|
|
77
|
-
* codegraph block). A wrong "used" is
|
|
86
|
+
* codegraph block). A wrong "used" is misleading, so unknown is
|
|
78
87
|
* reported as unknown rather than guessed. */
|
|
79
88
|
codegraphState: "used" | "disabled" | "unavailable" | null;
|
|
80
89
|
findings: HumanFinding[];
|
|
81
90
|
warnings?: ReviewWarning[];
|
|
82
91
|
};
|
|
83
|
-
/** The single human-readable review format
|
|
92
|
+
/** The single human-readable review format. Local,
|
|
84
93
|
* PR and remote reviews print this: a header naming the guide build date and
|
|
85
94
|
* the codegraph state, the findings grouped by state, then one summary line.
|
|
86
95
|
*
|
|
@@ -90,11 +99,21 @@ export type HumanReviewInput = {
|
|
|
90
99
|
export declare function formatHumanReview(input: HumanReviewInput): string;
|
|
91
100
|
/** Local review output: the shared format plus the working-tree file stats. */
|
|
92
101
|
export declare function formatHumanLocalReview(header: string, revision: Revision, guideBuiltAt: string | null, codegraphState: "used" | "disabled" | "unavailable", findings: ResolvedFinding[], warnings: ReviewWarning[], mode?: ReviewBlocking): string;
|
|
93
|
-
/** The exit code for a remote review
|
|
102
|
+
/** The exit code for a remote review. The server already decided
|
|
94
103
|
* each finding's `blocking` under its own configured rule, so the client
|
|
95
104
|
* trusts that field instead of re-deriving the rule from a title: the client
|
|
96
105
|
* may not even have the same config as the server. */
|
|
97
106
|
export declare function reviewExitCodeFromJsonFindings(findings: JsonReviewFinding[]): number;
|
|
107
|
+
/** `costUsd` is `null` when the cost is unknown, and `costStatus` and
|
|
108
|
+
* `costNote` say why. */
|
|
109
|
+
export type ReviewUsage = {
|
|
110
|
+
tokensIn: number;
|
|
111
|
+
tokensOut: number;
|
|
112
|
+
costUsd: number | null;
|
|
113
|
+
costStatus?: CostStatus;
|
|
114
|
+
costNote?: CostReason | null;
|
|
115
|
+
billedTo?: BilledTo;
|
|
116
|
+
};
|
|
98
117
|
export type LocalReviewJsonInput = {
|
|
99
118
|
repo: string;
|
|
100
119
|
branch: string;
|
|
@@ -105,11 +124,7 @@ export type LocalReviewJsonInput = {
|
|
|
105
124
|
codegraphReason: string | null;
|
|
106
125
|
findings: ResolvedFinding[];
|
|
107
126
|
warnings: ReviewWarning[];
|
|
108
|
-
usage:
|
|
109
|
-
tokensIn: number;
|
|
110
|
-
tokensOut: number;
|
|
111
|
-
costUsd: number | null;
|
|
112
|
-
};
|
|
127
|
+
usage: ReviewUsage;
|
|
113
128
|
durationMs: number;
|
|
114
129
|
reviewBlocking?: ReviewBlocking;
|
|
115
130
|
};
|
|
@@ -120,11 +135,7 @@ export type PrReviewJsonInput = {
|
|
|
120
135
|
guideBuiltAt: string | null;
|
|
121
136
|
codegraphState: "used" | "disabled" | "unavailable";
|
|
122
137
|
codegraphReason: string | null;
|
|
123
|
-
usage:
|
|
124
|
-
tokensIn: number;
|
|
125
|
-
tokensOut: number;
|
|
126
|
-
costUsd: number | null;
|
|
127
|
-
};
|
|
138
|
+
usage: ReviewUsage;
|
|
128
139
|
durationMs: number;
|
|
129
140
|
reviewBlocking?: ReviewBlocking;
|
|
130
141
|
};
|
|
@@ -106,7 +106,7 @@ export function reviewExitCodeFromResolved(findings, mode = DEFAULT_REVIEW_BLOCK
|
|
|
106
106
|
return summaryCounts(findings, mode).blocking > 0 ? 1 : 0;
|
|
107
107
|
}
|
|
108
108
|
/** Local and PR findings: blocking is decided here from the configured rule,
|
|
109
|
-
* exactly as the exit code decides it
|
|
109
|
+
* exactly as the exit code decides it. */
|
|
110
110
|
export function humanFindingsFromResolved(findings, mode = DEFAULT_REVIEW_BLOCKING) {
|
|
111
111
|
return findings.map((row) => {
|
|
112
112
|
const suggestion = readSuggestion(row.bodyMd);
|
|
@@ -131,7 +131,7 @@ export function humanFindingsFromResolved(findings, mode = DEFAULT_REVIEW_BLOCKI
|
|
|
131
131
|
}
|
|
132
132
|
/** Remote findings already carry the server's blocking decision, so the label
|
|
133
133
|
* uses that field rather than re-deriving the rule on a client that may not
|
|
134
|
-
* share the server's config
|
|
134
|
+
* share the server's config. */
|
|
135
135
|
export function humanFindingsFromJson(findings) {
|
|
136
136
|
return findings.map((row) => ({
|
|
137
137
|
state: row.state,
|
|
@@ -158,8 +158,8 @@ function humanLocation(row) {
|
|
|
158
158
|
* already printed beside it, so only the symbol survives. An older heading
|
|
159
159
|
* that used an em dash is still accepted, so comments posted before 0.5.0
|
|
160
160
|
* stay readable. A heading with no symbol leaves nothing: printing the label
|
|
161
|
-
* or the path again would repeat what the line already says
|
|
162
|
-
function humanShortTitle(row) {
|
|
161
|
+
* or the path again would repeat what the line already says. */
|
|
162
|
+
export function humanShortTitle(row) {
|
|
163
163
|
const withoutLabel = row.title.replace(/^\[P\d\s*·\s*[^\]]*\]\s*/, "");
|
|
164
164
|
const dash = withoutLabel.indexOf(" — ");
|
|
165
165
|
const tail = dash === -1 ? withoutLabel : withoutLabel.slice(dash + 3);
|
|
@@ -168,7 +168,7 @@ function humanShortTitle(row) {
|
|
|
168
168
|
const clean = symbol.replace(/^`|`$/g, "").trim();
|
|
169
169
|
return clean === "" || clean === row.path ? "" : clean;
|
|
170
170
|
}
|
|
171
|
-
function sortHumanFindings(rows) {
|
|
171
|
+
export function sortHumanFindings(rows) {
|
|
172
172
|
const stateOrder = { new: 0, open: 1, closed: 2 };
|
|
173
173
|
return [...rows].sort((a, b) => {
|
|
174
174
|
const ds = stateOrder[a.state] - stateOrder[b.state];
|
|
@@ -185,7 +185,7 @@ function sortHumanFindings(rows) {
|
|
|
185
185
|
return (a.lineFrom ?? 0) - (b.lineFrom ?? 0);
|
|
186
186
|
});
|
|
187
187
|
}
|
|
188
|
-
/** The single human-readable review format
|
|
188
|
+
/** The single human-readable review format. Local,
|
|
189
189
|
* PR and remote reviews print this: a header naming the guide build date and
|
|
190
190
|
* the codegraph state, the findings grouped by state, then one summary line.
|
|
191
191
|
*
|
|
@@ -271,7 +271,7 @@ export function formatHumanLocalReview(header, revision, guideBuiltAt, codegraph
|
|
|
271
271
|
warnings,
|
|
272
272
|
});
|
|
273
273
|
}
|
|
274
|
-
/** The exit code for a remote review
|
|
274
|
+
/** The exit code for a remote review. The server already decided
|
|
275
275
|
* each finding's `blocking` under its own configured rule, so the client
|
|
276
276
|
* trusts that field instead of re-deriving the rule from a title: the client
|
|
277
277
|
* may not even have the same config as the server. */
|
package/dist/src/config.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { type Platform } from "./util/runtime.ts";
|
|
|
5
5
|
* flags and no interactive prompts, reusing what `init` was told. */
|
|
6
6
|
export type RepoConfig = {
|
|
7
7
|
auth?: "gh" | "pat";
|
|
8
|
-
ai?: "none" | "openrouter" | "
|
|
8
|
+
ai?: "none" | "openrouter" | "openai" | "anthropic" | "opencode-zen" | "opencode-go";
|
|
9
9
|
lowModel?: string;
|
|
10
10
|
highModel?: string;
|
|
11
11
|
maxCommits?: number;
|
|
@@ -24,7 +24,7 @@ export type RepoConfig = {
|
|
|
24
24
|
};
|
|
25
25
|
export type UserConfig = {
|
|
26
26
|
auth?: "gh" | "pat";
|
|
27
|
-
ai?: "none" | "openrouter" | "
|
|
27
|
+
ai?: "none" | "openrouter" | "openai" | "anthropic" | "opencode-zen" | "opencode-go";
|
|
28
28
|
lowModel?: string;
|
|
29
29
|
highModel?: string;
|
|
30
30
|
/** Written only by `co-maintainer set --token=...`. Every other write
|
|
@@ -61,22 +61,44 @@ export type UserConfig = {
|
|
|
61
61
|
};
|
|
62
62
|
/** Cap on jobs running at once across all types. Unset means no limit. */
|
|
63
63
|
maxConcurrentJobs?: number;
|
|
64
|
-
/** What a review uses to decide a blocking finding
|
|
64
|
+
/** What a review uses to decide a blocking finding. `model` lets
|
|
65
65
|
* the model's own severity decide, `severity` uses a fixed threshold. */
|
|
66
66
|
reviewBlocking?: "model" | "severity";
|
|
67
67
|
/** Remote review CLI target (`co-maintainer set`). */
|
|
68
68
|
remoteHost?: string;
|
|
69
69
|
/** Bearer token for `/api/remote/*`; only written by `set`. */
|
|
70
70
|
remoteToken?: string;
|
|
71
|
-
/** Hosts for which the unpublished-code notice was shown
|
|
71
|
+
/** Hosts for which the unpublished-code notice was shown. */
|
|
72
72
|
remoteNoticeShownFor?: string[];
|
|
73
|
-
/** Remote client watchdog timeout; dashboard may override
|
|
73
|
+
/** Remote client watchdog timeout; dashboard may override. */
|
|
74
74
|
remoteSyncTimeoutSeconds?: number;
|
|
75
75
|
maxConcurrentRemoteReviewsPerToken?: number;
|
|
76
76
|
remoteToolOutputMaxChars?: number;
|
|
77
|
+
/** Whether remote clients may bring their own AI key. `off`
|
|
78
|
+
* rejects one, `allow` accepts one and falls back to the server key when
|
|
79
|
+
* absent, `require` refuses a review that does not carry one. The
|
|
80
|
+
* `CM_REMOTE_BYOK_POLICY` env var overrides this, which is how a hosted
|
|
81
|
+
* deployment forces the setting. */
|
|
82
|
+
remoteByokPolicy?: ByokPolicy;
|
|
83
|
+
/** The review policy new repositories start with: a template
|
|
84
|
+
* name or a policy object. Unset means the old per repository switches. */
|
|
85
|
+
reviewPolicy?: string | Record<string, unknown>;
|
|
86
|
+
/** The client's own AI key for remote review, sent only when
|
|
87
|
+
* `--remote-byok` (or `remoteByokDefault`) turns it on for a run. Only
|
|
88
|
+
* ever written by `set`. */
|
|
89
|
+
remoteByok?: string;
|
|
90
|
+
/** Whether `co-maintainer review --remote` sends `remoteByok` by default.
|
|
91
|
+
* `--no-remote-byok` turns it off for one run without unsetting this. */
|
|
92
|
+
remoteByokDefault?: boolean;
|
|
77
93
|
repos?: Record<string, RepoConfig>;
|
|
78
94
|
};
|
|
79
|
-
|
|
95
|
+
export type ByokPolicy = "off" | "allow" | "require";
|
|
96
|
+
/** Who pays for a remote review's AI calls. `CM_REMOTE_BYOK_POLICY`
|
|
97
|
+
* overrides the saved config, so a hosted server cannot be talked out of its
|
|
98
|
+
* policy by a config edit. An unset or unreadable value falls back to the
|
|
99
|
+
* config, then to `off`. */
|
|
100
|
+
export declare function remoteByokPolicy(config?: Pick<UserConfig, "remoteByokPolicy">): ByokPolicy;
|
|
101
|
+
/** Platform/env injection keeps the OS branches testable: the
|
|
80
102
|
* migration's `"windows"` vs `"win32"` trap is invisible otherwise. */
|
|
81
103
|
export declare function getConfigDir(os?: Platform, env?: (name: string) => string | undefined): string;
|
|
82
104
|
export declare function getCacheDir(os?: Platform, env?: (name: string) => string | undefined): string;
|
package/dist/src/config.js
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { chmod, currentPlatform, getEnv, isNotFound, mkdir, readTextFileSync, setEnv, writeTextFile, } from "./util/runtime.js";
|
|
2
|
+
/** Who pays for a remote review's AI calls. `CM_REMOTE_BYOK_POLICY`
|
|
3
|
+
* overrides the saved config, so a hosted server cannot be talked out of its
|
|
4
|
+
* policy by a config edit. An unset or unreadable value falls back to the
|
|
5
|
+
* config, then to `off`. */
|
|
6
|
+
export function remoteByokPolicy(config = readConfig()) {
|
|
7
|
+
// An env value that is not one of the three known policies is treated as
|
|
8
|
+
// unreadable and falls back to the config, so a typo cannot silently turn a
|
|
9
|
+
// configured `require` into `off`. An explicit `off` still overrides.
|
|
10
|
+
const env = getEnv("CM_REMOTE_BYOK_POLICY");
|
|
11
|
+
const raw = env === "off" || env === "allow" || env === "require"
|
|
12
|
+
? env
|
|
13
|
+
: config.remoteByokPolicy;
|
|
14
|
+
return raw === "allow" || raw === "require" ? raw : "off";
|
|
15
|
+
}
|
|
2
16
|
function homeDir(env) {
|
|
3
17
|
const home = env("HOME") ?? env("USERPROFILE");
|
|
4
18
|
if (!home)
|
|
5
19
|
throw new Error("Could not determine the home directory");
|
|
6
20
|
return home;
|
|
7
21
|
}
|
|
8
|
-
/** Platform/env injection keeps the OS branches testable
|
|
22
|
+
/** Platform/env injection keeps the OS branches testable: the
|
|
9
23
|
* migration's `"windows"` vs `"win32"` trap is invisible otherwise. */
|
|
10
24
|
export function getConfigDir(os = currentPlatform(), env = getEnv) {
|
|
11
25
|
const home = homeDir(env);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Run } from "../pr/checkout.ts";
|
|
2
|
-
/** `refs/remotes/<remote>/…` for scope and PR algorithms
|
|
2
|
+
/** `refs/remotes/<remote>/…` for scope and PR algorithms. */
|
|
3
3
|
export declare function detectDefaultBranchRef(cwd: string, remote: string, run: Run): Promise<string | undefined>;
|
|
4
4
|
/** Short branch name (`main`) for `refs/remotes/<remote>/<branch>`. */
|
|
5
5
|
export declare function resolveDefaultBranchName(cwd: string, remote: string, run: Run): Promise<string | undefined>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** `refs/remotes/<remote>/…` for scope and PR algorithms
|
|
1
|
+
/** `refs/remotes/<remote>/…` for scope and PR algorithms. */
|
|
2
2
|
export async function detectDefaultBranchRef(cwd, remote, run) {
|
|
3
3
|
const symref = await run("git", ["symbolic-ref", "-q", `refs/remotes/${remote}/HEAD`], cwd);
|
|
4
4
|
if (symref.code === 0 && symref.stdout.trim()) {
|
package/dist/src/github/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { githubFetch,
|
|
1
|
+
import { githubFetch, httpError, paginate } from "./client.js";
|
|
2
2
|
import { importAppPrivateKey, signAppJwt } from "./jwt.js";
|
|
3
3
|
const API = "https://api.github.com";
|
|
4
4
|
function headers(token) {
|
|
@@ -14,7 +14,7 @@ async function call(endpoint, token, init = {}) {
|
|
|
14
14
|
headers: { ...headers(token), ...(init.headers ?? {}) },
|
|
15
15
|
});
|
|
16
16
|
if (!response.ok) {
|
|
17
|
-
throw
|
|
17
|
+
throw await httpError(response, init.method ?? "GET", endpoint, "App");
|
|
18
18
|
}
|
|
19
19
|
if (response.status === 204)
|
|
20
20
|
return undefined;
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
export declare const APP_MANIFEST_PERMISSIONS:
|
|
2
|
-
readonly contents: "read";
|
|
3
|
-
readonly issues: "write";
|
|
4
|
-
readonly pull_requests: "write";
|
|
5
|
-
readonly checks: "write";
|
|
6
|
-
readonly metadata: "read";
|
|
7
|
-
};
|
|
1
|
+
export declare const APP_MANIFEST_PERMISSIONS: Record<string, import("./permissions.ts").Level>;
|
|
8
2
|
export declare const APP_MANIFEST_EVENTS: readonly ["pull_request", "pull_request_review", "pull_request_review_comment", "issue_comment"];
|
|
9
3
|
/** GitHub App names are globally unique and capped at 34 characters. */
|
|
10
4
|
export declare const APP_NAME_MAX = 34;
|
|
@@ -15,14 +15,9 @@
|
|
|
15
15
|
* GitHub delivers both to every App without a subscription, which is why
|
|
16
16
|
* `webhook.ts` handles them at all. */
|
|
17
17
|
import { githubFetch } from "./client.js";
|
|
18
|
+
import { APP_PERMISSIONS } from "./permissions.js";
|
|
18
19
|
import { webhookReachabilityProblem } from "../util/webhook_reachability.js";
|
|
19
|
-
export const APP_MANIFEST_PERMISSIONS =
|
|
20
|
-
contents: "read",
|
|
21
|
-
issues: "write",
|
|
22
|
-
pull_requests: "write",
|
|
23
|
-
checks: "write",
|
|
24
|
-
metadata: "read",
|
|
25
|
-
};
|
|
20
|
+
export const APP_MANIFEST_PERMISSIONS = APP_PERMISSIONS;
|
|
26
21
|
export const APP_MANIFEST_EVENTS = [
|
|
27
22
|
"pull_request",
|
|
28
23
|
"pull_request_review",
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
* not use `githubFetch`; both paths share `paginate` and `waitForRateLimit`. */
|
|
4
4
|
export declare class GitHubHttpError extends Error {
|
|
5
5
|
readonly status: number;
|
|
6
|
-
constructor(status: number, body: string);
|
|
6
|
+
constructor(status: number, body: string, hint?: string);
|
|
7
7
|
}
|
|
8
|
+
/** A failed response as an error. A 403 or 404 names the endpoint and the
|
|
9
|
+
* permission it needs, because GitHub's own body rarely says which one. */
|
|
10
|
+
export declare function httpError(response: Response, method: string, endpoint: string, who: "token" | "App"): Promise<GitHubHttpError>;
|
|
8
11
|
export declare function isAccessDenied(error: unknown): boolean;
|
|
9
12
|
/** Primary probes wake at the reset, but at least every half hour. */
|
|
10
13
|
export declare const RATE_LIMIT_PROBE_MS: number;
|
|
@@ -3,14 +3,26 @@
|
|
|
3
3
|
* not use `githubFetch`; both paths share `paginate` and `waitForRateLimit`. */
|
|
4
4
|
import { log } from "../util/log.js";
|
|
5
5
|
import { networkFailure } from "../cli/error.js";
|
|
6
|
+
import { missingHint, refusalHint } from "./permissions.js";
|
|
6
7
|
export class GitHubHttpError extends Error {
|
|
7
8
|
status;
|
|
8
|
-
constructor(status, body) {
|
|
9
|
-
super(`GitHub API ${status}: ${body}`);
|
|
9
|
+
constructor(status, body, hint) {
|
|
10
|
+
super(`GitHub API ${status}: ${body}${hint ? ` ${hint}` : ""}`);
|
|
10
11
|
this.status = status;
|
|
11
12
|
this.name = "GitHubHttpError";
|
|
12
13
|
}
|
|
13
14
|
}
|
|
15
|
+
/** A failed response as an error. A 403 or 404 names the endpoint and the
|
|
16
|
+
* permission it needs, because GitHub's own body rarely says which one. */
|
|
17
|
+
export async function httpError(response, method, endpoint, who) {
|
|
18
|
+
const body = await response.text();
|
|
19
|
+
const hint = response.status === 403
|
|
20
|
+
? refusalHint(method, endpoint, who)
|
|
21
|
+
: response.status === 404
|
|
22
|
+
? missingHint(method, endpoint, who)
|
|
23
|
+
: undefined;
|
|
24
|
+
return new GitHubHttpError(response.status, body, hint);
|
|
25
|
+
}
|
|
14
26
|
export function isAccessDenied(error) {
|
|
15
27
|
return (error instanceof GitHubHttpError &&
|
|
16
28
|
(error.status === 403 || error.status === 404));
|
package/dist/src/github/gh.js
CHANGED
|
@@ -17,12 +17,12 @@ function ghSpawn() {
|
|
|
17
17
|
return { command: bin, prefix: script ? [script] : [] };
|
|
18
18
|
}
|
|
19
19
|
/** Turns a failed `gh api` call into a message that says what broke, why, and
|
|
20
|
-
* what to do next
|
|
20
|
+
* what to do next. The four shapes that came up in practice:
|
|
21
21
|
* `gh` missing from PATH, the repo missing or unreadable, an empty stderr, and
|
|
22
22
|
* everything else. The first line is the only part a human reads, so it always
|
|
23
23
|
* names the situation; the raw gh text moves to the hint so it stays available
|
|
24
24
|
* without drowning the message. */
|
|
25
|
-
function ghFailure(endpoint, stderr) {
|
|
25
|
+
function ghFailure(method, endpoint, stderr) {
|
|
26
26
|
if (/ENOENT|command not found|not recognized/i.test(stderr)) {
|
|
27
27
|
return new CliError("gh_not_installed", "GitHub CLI (gh) was not found.", "Install it from https://cli.github.com or use --auth=pat.", EXIT_USAGE);
|
|
28
28
|
}
|
|
@@ -32,6 +32,9 @@ function ghFailure(endpoint, stderr) {
|
|
|
32
32
|
const subject = match ? `${match[1]}/${match[2]}` : endpoint;
|
|
33
33
|
return new CliError("repo_not_found", `${subject} was not found, or your GitHub account cannot read it.`, "Check the name and run gh auth status.", EXIT_USAGE);
|
|
34
34
|
}
|
|
35
|
+
if (/HTTP 403/i.test(stderr)) {
|
|
36
|
+
return new CliError("github_forbidden", `GitHub refused ${method} ${endpoint}.`, `Your gh login needs the repo scope. Run gh auth refresh -s repo. gh said: ${stderr}`, EXIT_USAGE);
|
|
37
|
+
}
|
|
35
38
|
// An empty stderr used to collapse to just the endpoint, which told the user
|
|
36
39
|
// nothing about what happened.
|
|
37
40
|
if (stderr.length === 0) {
|
|
@@ -129,7 +132,7 @@ export class GhClient {
|
|
|
129
132
|
this.debug = debug;
|
|
130
133
|
}
|
|
131
134
|
request(endpoint) {
|
|
132
|
-
return this.call(endpoint, () => {
|
|
135
|
+
return this.call("GET", endpoint, () => {
|
|
133
136
|
const { command, prefix } = ghSpawn();
|
|
134
137
|
return commandOutput(command, {
|
|
135
138
|
args: [...prefix, "api", endpoint],
|
|
@@ -151,7 +154,7 @@ export class GhClient {
|
|
|
151
154
|
return paginate((e) => this.request(e), endpoint, limit, progress);
|
|
152
155
|
}
|
|
153
156
|
send(endpoint, method, body) {
|
|
154
|
-
return this.call(endpoint, () => {
|
|
157
|
+
return this.call(method, endpoint, () => {
|
|
155
158
|
const { command, prefix } = ghSpawn();
|
|
156
159
|
return commandWithInput(command, {
|
|
157
160
|
args: [...prefix, "api", "-X", method, endpoint, "--input", "-"],
|
|
@@ -161,13 +164,13 @@ export class GhClient {
|
|
|
161
164
|
}, JSON.stringify(body));
|
|
162
165
|
});
|
|
163
166
|
}
|
|
164
|
-
async call(endpoint, run) {
|
|
167
|
+
async call(method, endpoint, run) {
|
|
165
168
|
noteGhCall(this.debug);
|
|
166
169
|
const started = Date.now();
|
|
167
170
|
let spun = false;
|
|
168
171
|
for (;;) {
|
|
169
172
|
// A missing `gh` binary rejects the spawn instead of returning a failed
|
|
170
|
-
// result, so the ENOENT case has to be caught here too
|
|
173
|
+
// result, so the ENOENT case has to be caught here too.
|
|
171
174
|
let result;
|
|
172
175
|
try {
|
|
173
176
|
result = await run();
|
|
@@ -193,7 +196,7 @@ export class GhClient {
|
|
|
193
196
|
? bucket.remaining === 0
|
|
194
197
|
: /rate limit/i.test(error);
|
|
195
198
|
if (!limited)
|
|
196
|
-
throw ghFailure(endpoint, error);
|
|
199
|
+
throw ghFailure(method, endpoint, error);
|
|
197
200
|
const resetAt = bucket?.resetAt ?? new Date(Date.now() + RATE_LIMIT_PROBE_MS);
|
|
198
201
|
const delay = probeDelay(resetAt, Date.now());
|
|
199
202
|
if (delay === 0) {
|
package/dist/src/github/pat.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { githubFetch,
|
|
1
|
+
import { githubFetch, httpError, paginate } from "./client.js";
|
|
2
2
|
export class PatClient {
|
|
3
3
|
token;
|
|
4
4
|
constructor(token) {
|
|
@@ -15,7 +15,7 @@ export class PatClient {
|
|
|
15
15
|
},
|
|
16
16
|
});
|
|
17
17
|
if (!response.ok) {
|
|
18
|
-
throw
|
|
18
|
+
throw await httpError(response, "GET", endpoint, "token");
|
|
19
19
|
}
|
|
20
20
|
return (await response.json());
|
|
21
21
|
}
|
|
@@ -31,7 +31,7 @@ export class PatClient {
|
|
|
31
31
|
body: JSON.stringify(body),
|
|
32
32
|
});
|
|
33
33
|
if (!response.ok) {
|
|
34
|
-
throw
|
|
34
|
+
throw await httpError(response, "POST", endpoint, "token");
|
|
35
35
|
}
|
|
36
36
|
return (await response.json());
|
|
37
37
|
}
|
|
@@ -50,7 +50,7 @@ export class PatClient {
|
|
|
50
50
|
body: JSON.stringify(body),
|
|
51
51
|
});
|
|
52
52
|
if (!response.ok) {
|
|
53
|
-
throw
|
|
53
|
+
throw await httpError(response, "PATCH", endpoint, "token");
|
|
54
54
|
}
|
|
55
55
|
return (await response.json());
|
|
56
56
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** The one list of GitHub calls this codebase makes and what each needs.
|
|
2
|
+
*
|
|
3
|
+
* The App manifest, the App permission check on save, the 403 and 404 hints
|
|
4
|
+
* and the permission tables in the docs are all read from `ENDPOINTS`.
|
|
5
|
+
* `permissions.test.ts` scans the source for every endpoint it calls and fails
|
|
6
|
+
* when one is missing here, so a new call cannot ship without its permission. */
|
|
7
|
+
export type Level = "read" | "write";
|
|
8
|
+
export type Permission = "metadata" | "contents" | "pull_requests" | "issues" | "checks";
|
|
9
|
+
/** `gh` is a signed-in GitHub CLI, `pat` a personal access token, `app` a
|
|
10
|
+
* GitHub App installation token. */
|
|
11
|
+
export type Identity = "gh" | "pat" | "app";
|
|
12
|
+
export type Endpoint = {
|
|
13
|
+
method: "GET" | "POST" | "PATCH";
|
|
14
|
+
/** `{repo}` is `owner/name`, `{path}` may hold slashes, any other
|
|
15
|
+
* placeholder is one path segment. */
|
|
16
|
+
pattern: string;
|
|
17
|
+
permission?: Permission;
|
|
18
|
+
level?: Level;
|
|
19
|
+
identities: readonly Identity[];
|
|
20
|
+
feature: string;
|
|
21
|
+
};
|
|
22
|
+
/** Key order is the order the manifest and the doc tables list them in. */
|
|
23
|
+
export declare const PERMISSION_LABEL: Record<Permission, string>;
|
|
24
|
+
export declare const ENDPOINTS: readonly Endpoint[];
|
|
25
|
+
/** What the App must be granted, in the shape the manifest and the save-time
|
|
26
|
+
* check want. */
|
|
27
|
+
export declare const APP_PERMISSIONS: Record<string, Level>;
|
|
28
|
+
/** The entry for a real request such as `repos/o/r/pulls/4/files?page=2`. */
|
|
29
|
+
export declare function endpointFor(method: string, endpoint: string): Endpoint | undefined;
|
|
30
|
+
/** `who` is what the failing credential is called in the sentence. */
|
|
31
|
+
export declare function refusalHint(method: string, endpoint: string, who: "token" | "App"): string;
|
|
32
|
+
export declare function missingHint(method: string, endpoint: string, who: "token" | "App"): string;
|
|
33
|
+
export type TableKind = "app" | "pat-fine" | "pat-classic" | "gh" | "oauth";
|
|
34
|
+
/** A markdown table for the docs. `build_docs.ts` writes it between the
|
|
35
|
+
* `permissions:<kind>` markers and a test fails when the page is stale. */
|
|
36
|
+
export declare function permissionTable(kind: TableKind): string;
|