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
package/dist/src/ai/verify.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Pre-flight checks for `config set
|
|
1
|
+
/** Pre-flight checks for `config set`.
|
|
2
2
|
*
|
|
3
3
|
* A wrong API key or an unknown model used to be accepted by `set` and only
|
|
4
4
|
* surfaced during a review, after the pull request had been fetched and
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* user's mistake, so it warns and saves instead of refusing. */
|
|
8
8
|
import { CliError, EXIT_USAGE } from "../cli/error.js";
|
|
9
9
|
import { getEnv } from "../util/runtime.js";
|
|
10
|
+
import { openAiBase } from "./openai.js";
|
|
11
|
+
import { anthropicBase, ANTHROPIC_VERSION } from "./anthropic.js";
|
|
10
12
|
/** The chat endpoint the provider uses, with `/chat/completions` removed.
|
|
11
13
|
* `CM_OPENROUTER_URL` points at a local fake in tests, so the base follows it
|
|
12
14
|
* and the verification hits the same server the review would. */
|
|
@@ -15,11 +17,11 @@ export function openRouterBase() {
|
|
|
15
17
|
"https://openrouter.ai/api/v1/chat/completions";
|
|
16
18
|
return endpoint.replace(/\/chat\/completions\/?$/, "");
|
|
17
19
|
}
|
|
18
|
-
async function getJson(url, apiKey) {
|
|
20
|
+
async function getJson(url, apiKey, headers) {
|
|
19
21
|
let response;
|
|
20
22
|
try {
|
|
21
23
|
response = await fetch(url, {
|
|
22
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
24
|
+
headers: headers ?? { Authorization: `Bearer ${apiKey}` },
|
|
23
25
|
});
|
|
24
26
|
}
|
|
25
27
|
catch (error) {
|
|
@@ -76,3 +78,79 @@ export async function verifyOpenRouter(apiKey, model) {
|
|
|
76
78
|
}
|
|
77
79
|
return { status: "ok" };
|
|
78
80
|
}
|
|
81
|
+
/** Same shape as {@link verifyOpenRouter}: `/models` both authenticates the
|
|
82
|
+
* key and, when `model` is given, confirms OpenAI actually has it. */
|
|
83
|
+
export async function verifyOpenAi(apiKey, model) {
|
|
84
|
+
const base = openAiBase();
|
|
85
|
+
const models = await getJson(`${base}/models`, apiKey);
|
|
86
|
+
if ("unreachable" in models) {
|
|
87
|
+
return { status: "unreachable", reason: models.unreachable };
|
|
88
|
+
}
|
|
89
|
+
if (models.status === 401) {
|
|
90
|
+
return {
|
|
91
|
+
status: "rejected",
|
|
92
|
+
error: new CliError("openai_unauthorized", "OpenAI rejected the API key.", "Check the key at https://platform.openai.com/api-keys and pass it again.", EXIT_USAGE),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (models.status >= 400) {
|
|
96
|
+
return {
|
|
97
|
+
status: "unreachable",
|
|
98
|
+
reason: `OpenAI returned ${models.status}`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (!model)
|
|
102
|
+
return { status: "ok" };
|
|
103
|
+
const rows = models.body?.data;
|
|
104
|
+
if (!Array.isArray(rows)) {
|
|
105
|
+
return { status: "unreachable", reason: "OpenAI returned no model list" };
|
|
106
|
+
}
|
|
107
|
+
if (!rows.some((row) => row?.id === model)) {
|
|
108
|
+
return {
|
|
109
|
+
status: "rejected",
|
|
110
|
+
error: new CliError("openai_unknown_model", `OpenAI does not know the model ${model}.`, "Pick one at https://platform.openai.com/docs/models and set it with co-maintainer set --high-model=...", EXIT_USAGE),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return { status: "ok" };
|
|
114
|
+
}
|
|
115
|
+
/** Anthropic authenticates the key and lists models at `/models`, but with its
|
|
116
|
+
* own headers (`x-api-key` + `anthropic-version`) instead of a Bearer token.
|
|
117
|
+
* Without a model the call only proves the key; with one it also confirms
|
|
118
|
+
* Anthropic serves it. */
|
|
119
|
+
export async function verifyAnthropic(apiKey, model) {
|
|
120
|
+
const base = anthropicBase();
|
|
121
|
+
const models = await getJson(`${base}/models`, apiKey, {
|
|
122
|
+
"x-api-key": apiKey,
|
|
123
|
+
"anthropic-version": ANTHROPIC_VERSION,
|
|
124
|
+
});
|
|
125
|
+
if ("unreachable" in models) {
|
|
126
|
+
return { status: "unreachable", reason: models.unreachable };
|
|
127
|
+
}
|
|
128
|
+
if (models.status === 401 || models.status === 403) {
|
|
129
|
+
return {
|
|
130
|
+
status: "rejected",
|
|
131
|
+
error: new CliError("anthropic_unauthorized", "Anthropic rejected the API key.", "Check the key at https://console.anthropic.com/settings/keys and pass it again.", EXIT_USAGE),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (models.status >= 400) {
|
|
135
|
+
return {
|
|
136
|
+
status: "unreachable",
|
|
137
|
+
reason: `Anthropic returned ${models.status}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (!model)
|
|
141
|
+
return { status: "ok" };
|
|
142
|
+
const rows = models.body?.data;
|
|
143
|
+
if (!Array.isArray(rows)) {
|
|
144
|
+
return {
|
|
145
|
+
status: "unreachable",
|
|
146
|
+
reason: "Anthropic returned no model list",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (!rows.some((row) => row?.id === model)) {
|
|
150
|
+
return {
|
|
151
|
+
status: "rejected",
|
|
152
|
+
error: new CliError("anthropic_unknown_model", `Anthropic does not know the model ${model}.`, "Pick one at https://docs.anthropic.com/en/docs/about-claude/models and set it with co-maintainer set --high-model=...", EXIT_USAGE),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return { status: "ok" };
|
|
156
|
+
}
|
package/dist/src/cli/args.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Options } from "../types.ts";
|
|
2
|
-
|
|
3
|
-
/** Plan §8.7 — `--json` must not prompt. */
|
|
2
|
+
/** `--json` must not prompt. */
|
|
4
3
|
export declare function setCliInteractive(value: boolean): void;
|
|
5
4
|
export declare function parseArgs(args: string[]): Promise<Options>;
|
package/dist/src/cli/args.js
CHANGED
|
@@ -2,6 +2,7 @@ import { prepareConfig } from "../config.js";
|
|
|
2
2
|
import { getEnv } from "../util/runtime.js";
|
|
3
3
|
import { askLine } from "./prompt.js";
|
|
4
4
|
import { die } from "./error.js";
|
|
5
|
+
import { AI_PROVIDERS, providerKeyEnv, rejectRetiredProvider, } from "../ai/provider.js";
|
|
5
6
|
import { detectRemoteRepo } from "../local/git_ops.js";
|
|
6
7
|
import { reviewBlockingFrom } from "../review/blocking.js";
|
|
7
8
|
import { renderCommandHelp, renderGlobalHelp, unknownCommandMessage, unknownOptionMessage, } from "./commands/registry.js";
|
|
@@ -13,10 +14,8 @@ function numberOption(value, name) {
|
|
|
13
14
|
return number;
|
|
14
15
|
}
|
|
15
16
|
const commands = ["probe", "init", "sync", "remake", "review"];
|
|
16
|
-
export const defaultLowModel = "openai/gpt-oss-120b";
|
|
17
|
-
const defaultHighModel = "openai/gpt-5.6-luna";
|
|
18
17
|
let cliInteractive = true;
|
|
19
|
-
/**
|
|
18
|
+
/** `--json` must not prompt. */
|
|
20
19
|
export function setCliInteractive(value) {
|
|
21
20
|
cliInteractive = value;
|
|
22
21
|
}
|
|
@@ -24,10 +23,10 @@ async function ask(label, fallback, required = false) {
|
|
|
24
23
|
if (!cliInteractive) {
|
|
25
24
|
if (fallback !== undefined && fallback !== "")
|
|
26
25
|
return fallback;
|
|
27
|
-
die(`Missing ${label}
|
|
26
|
+
die(`Missing ${label}. Pass it as a CLI option when running without an interactive terminal`);
|
|
28
27
|
}
|
|
29
28
|
if (process.stdin.isTTY !== true) {
|
|
30
|
-
die(`Missing ${label}
|
|
29
|
+
die(`Missing ${label}. Pass it as a CLI option when running without an interactive terminal`);
|
|
31
30
|
}
|
|
32
31
|
const value = await askLine(label, fallback);
|
|
33
32
|
if (required && !value)
|
|
@@ -57,7 +56,7 @@ export async function parseArgs(args) {
|
|
|
57
56
|
process.exit(0);
|
|
58
57
|
}
|
|
59
58
|
// `probe` (and only probe, for now) may omit the repo and let the current
|
|
60
|
-
// directory's git remote name it
|
|
59
|
+
// directory's git remote name it.
|
|
61
60
|
let repoName = repo;
|
|
62
61
|
if ((!repoName || repoName.startsWith("-")) && command === "probe") {
|
|
63
62
|
repoName = await detectRemoteRepo(process.cwd());
|
|
@@ -168,9 +167,10 @@ export async function parseArgs(args) {
|
|
|
168
167
|
}
|
|
169
168
|
const explicitAi = rest.some((arg) => arg.startsWith("--ai="));
|
|
170
169
|
const configuredAiRaw = env("CO_MAINTAINER_AI") ?? repoConfig.ai ?? config.ai;
|
|
170
|
+
rejectRetiredProvider(configuredAiRaw);
|
|
171
171
|
if (configuredAiRaw &&
|
|
172
|
-
!
|
|
173
|
-
die(
|
|
172
|
+
!AI_PROVIDERS.includes(configuredAiRaw)) {
|
|
173
|
+
die(`CO_MAINTAINER_AI/config.ai must be one of: ${AI_PROVIDERS.join(", ")}`);
|
|
174
174
|
}
|
|
175
175
|
const configuredAi = configuredAiRaw;
|
|
176
176
|
const configuredAuthRaw = env("CO_MAINTAINER_AUTH") ?? repoConfig.auth ?? config.auth;
|
|
@@ -187,46 +187,58 @@ export async function parseArgs(args) {
|
|
|
187
187
|
die("--auth=pat requires a GitHub token. Pass --github-pat=..., set GITHUB_TOKEN/GH_TOKEN, " +
|
|
188
188
|
"or run: co-maintainer set --github-pat=...");
|
|
189
189
|
}
|
|
190
|
-
|
|
190
|
+
rejectRetiredProvider(text("ai"));
|
|
191
|
+
let ai = choice("ai", AI_PROVIDERS, configuredAi ?? "none");
|
|
191
192
|
if (command === "review") {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
// Review was pinned to OpenRouter only so Hetzner never wrote one. Hetzner
|
|
194
|
+
// is gone, so review follows the configured provider. With none set it
|
|
195
|
+
// stays on OpenRouter, which every review before this one used.
|
|
196
|
+
if (ai === "none") {
|
|
197
|
+
if (explicitAi) {
|
|
198
|
+
die(`review needs an AI provider. Pass --ai=${AI_PROVIDERS.filter((p) => p !== "none").join("|")}`);
|
|
199
|
+
}
|
|
200
|
+
ai = "openrouter";
|
|
195
201
|
}
|
|
196
|
-
ai = "openrouter";
|
|
197
202
|
}
|
|
198
203
|
else if (!explicitAi && !configuredAi && command !== "probe") {
|
|
199
|
-
const selected = await ask(
|
|
200
|
-
|
|
201
|
-
|
|
204
|
+
const selected = await ask(`AI provider (${AI_PROVIDERS.join("|")})`, "openrouter");
|
|
205
|
+
rejectRetiredProvider(selected);
|
|
206
|
+
if (!AI_PROVIDERS.includes(selected)) {
|
|
207
|
+
die(`AI provider must be one of: ${AI_PROVIDERS.join(", ")}`);
|
|
202
208
|
}
|
|
203
209
|
ai = selected;
|
|
204
210
|
}
|
|
205
211
|
let aiToken = text("token") ??
|
|
206
212
|
env("CO_MAINTAINER_TOKEN") ??
|
|
207
|
-
(ai
|
|
208
|
-
(ai === "hetzner" ? env("HETZNER_API_KEY") : undefined) ??
|
|
213
|
+
providerKeyEnv(ai, env) ??
|
|
209
214
|
config.token;
|
|
215
|
+
// The OPENROUTER_ names would hand an OpenRouter model id to another
|
|
216
|
+
// provider, so they only count when OpenRouter is the one running.
|
|
217
|
+
const openRouterModelEnv = (name) => ai === "openrouter" ? env(name) : undefined;
|
|
210
218
|
let lowModel = text("low-model") ??
|
|
211
|
-
|
|
212
|
-
env("HETZNER_LOW_MODEL") ??
|
|
219
|
+
openRouterModelEnv("OPENROUTER_LOW_MODEL") ??
|
|
213
220
|
env("LOW_MODEL") ??
|
|
214
221
|
repoConfig.lowModel ??
|
|
215
222
|
config.lowModel;
|
|
216
223
|
let highModel = text("high-model") ??
|
|
217
|
-
|
|
218
|
-
env("HETZNER_HIGH_MODEL") ??
|
|
224
|
+
openRouterModelEnv("OPENROUTER_HIGH_MODEL") ??
|
|
219
225
|
env("HIGH_MODEL") ??
|
|
220
226
|
repoConfig.highModel ??
|
|
221
227
|
config.highModel;
|
|
222
228
|
if (command === "review") {
|
|
223
|
-
aiToken ??= await ask(
|
|
224
|
-
|
|
229
|
+
aiToken ??= await ask(`${ai} API key`, undefined, true);
|
|
230
|
+
// Review summarizes large files with the low model, so it needs one just
|
|
231
|
+
// as init and sync do. Without a default to fall back on, ask here too.
|
|
232
|
+
lowModel ??= await ask("low model", undefined, true);
|
|
233
|
+
highModel ??= await ask("high model", undefined, true);
|
|
225
234
|
}
|
|
226
235
|
else if (ai !== "none" && command !== "probe") {
|
|
236
|
+
// No house pick for any provider: models go stale and a shipped build
|
|
237
|
+
// cannot fetch a fresher one, so the model to use is the user's call,
|
|
238
|
+
// set once in config or passed every time (Murat, 2026-09-28).
|
|
227
239
|
aiToken ??= await ask(`${ai} API key`, undefined, true);
|
|
228
|
-
lowModel ??= await ask("low model",
|
|
229
|
-
highModel ??= await ask("high model",
|
|
240
|
+
lowModel ??= await ask("low model", undefined, true);
|
|
241
|
+
highModel ??= await ask("high model", undefined, true);
|
|
230
242
|
}
|
|
231
243
|
if (command !== "review" &&
|
|
232
244
|
ai !== "none" &&
|
|
@@ -37,7 +37,7 @@ export declare function resolveField(name: string): string;
|
|
|
37
37
|
* `defaults` are nested maps, not settings, so they are not listed.
|
|
38
38
|
* `satisfies` makes this the type's own vocabulary: a name that is not a
|
|
39
39
|
* `UserConfig` key is a compile error rather than a silently undefined read. */
|
|
40
|
-
export declare const KNOWN_FIELDS: readonly ["auth", "ai", "lowModel", "highModel", "token", "githubPat", "githubAppId", "githubAppPrivateKey", "githubAppPrivateKeyPath", "githubWebhookSecret", "webhookUrl", "githubOAuthClientId", "githubOAuthClientSecret", "githubOAuthAllowedUser", "passwordAuthDisabled", "githubAuthEnabled", "dashboardPasswordHash", "maxConcurrentJobs", "reviewBlocking", "remoteHost", "remoteToken", "remoteNoticeShownFor", "remoteSyncTimeoutSeconds", "maxConcurrentRemoteReviewsPerToken", "remoteToolOutputMaxChars"];
|
|
40
|
+
export declare const KNOWN_FIELDS: readonly ["auth", "ai", "lowModel", "highModel", "token", "githubPat", "githubAppId", "githubAppPrivateKey", "githubAppPrivateKeyPath", "githubWebhookSecret", "webhookUrl", "githubOAuthClientId", "githubOAuthClientSecret", "githubOAuthAllowedUser", "passwordAuthDisabled", "githubAuthEnabled", "dashboardPasswordHash", "maxConcurrentJobs", "reviewBlocking", "remoteHost", "remoteToken", "remoteNoticeShownFor", "remoteSyncTimeoutSeconds", "maxConcurrentRemoteReviewsPerToken", "remoteToolOutputMaxChars", "remoteByok", "remoteByokDefault", "reviewPolicy"];
|
|
41
41
|
/** `co-maintainer config <sub> ...`. `set` is handled by runSet, which this
|
|
42
42
|
* command delegates to so there is one implementation of the write rules. */
|
|
43
43
|
export declare function runConfig(args: string[]): Promise<void>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** The `config` command: read and edit the user config without opening the
|
|
2
|
-
* file
|
|
2
|
+
* file. `set` is `config set` under its own name, so both share
|
|
3
3
|
* the mapping and masking here.
|
|
4
4
|
*
|
|
5
5
|
* The key list is derived from `UserConfig` at runtime where possible: a
|
|
@@ -18,6 +18,7 @@ export const SECRET_FIELDS = new Set([
|
|
|
18
18
|
"githubOAuthClientSecret",
|
|
19
19
|
"remoteToken",
|
|
20
20
|
"dashboardPasswordHash",
|
|
21
|
+
"remoteByok",
|
|
21
22
|
]);
|
|
22
23
|
/** `lowModel` -> `low-model`. The inverse of {@link configKey}. */
|
|
23
24
|
export function flagName(field) {
|
|
@@ -38,13 +39,15 @@ export function maskSecret(value) {
|
|
|
38
39
|
/** The env var that supplies each field, mirroring args.ts. Only the fields
|
|
39
40
|
* that actually read env are listed. */
|
|
40
41
|
export const ENV_BY_FIELD = {
|
|
41
|
-
token: ["CO_MAINTAINER_TOKEN", "OPENROUTER_API_KEY", "
|
|
42
|
+
token: ["CO_MAINTAINER_TOKEN", "OPENROUTER_API_KEY", "OPENCODE_API_KEY"],
|
|
42
43
|
githubPat: ["GITHUB_TOKEN", "GH_TOKEN"],
|
|
43
44
|
ai: ["CO_MAINTAINER_AI"],
|
|
44
45
|
auth: ["CO_MAINTAINER_AUTH"],
|
|
45
|
-
lowModel: ["OPENROUTER_LOW_MODEL", "
|
|
46
|
-
highModel: ["OPENROUTER_HIGH_MODEL", "
|
|
46
|
+
lowModel: ["OPENROUTER_LOW_MODEL", "LOW_MODEL"],
|
|
47
|
+
highModel: ["OPENROUTER_HIGH_MODEL", "HIGH_MODEL"],
|
|
47
48
|
webhookUrl: ["CM_WEBHOOK_URL"],
|
|
49
|
+
remoteHost: ["CM_REMOTE_HOST"],
|
|
50
|
+
remoteToken: ["CM_REMOTE_TOKEN"],
|
|
48
51
|
reviewBlocking: ["CO_MAINTAINER_REVIEW_BLOCKING"],
|
|
49
52
|
};
|
|
50
53
|
/** Renders one field for people. A secret is masked; the password hash, which
|
|
@@ -151,6 +154,9 @@ export const KNOWN_FIELDS = [
|
|
|
151
154
|
"remoteSyncTimeoutSeconds",
|
|
152
155
|
"maxConcurrentRemoteReviewsPerToken",
|
|
153
156
|
"remoteToolOutputMaxChars",
|
|
157
|
+
"remoteByok",
|
|
158
|
+
"remoteByokDefault",
|
|
159
|
+
"reviewPolicy",
|
|
154
160
|
];
|
|
155
161
|
/** `co-maintainer config <sub> ...`. `set` is handled by runSet, which this
|
|
156
162
|
* command delegates to so there is one implementation of the write rules. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { clientFor, runInitOrRemake } from "../../services/setup.js";
|
|
2
2
|
import { probePlan } from "../../services/probe.js";
|
|
3
3
|
import { log, startHeartbeat } from "../../util/log.js";
|
|
4
|
-
import { estimateInit, readJobHistory } from "../../ai/estimate.js";
|
|
4
|
+
import { estimateCostLine, estimateInit, readJobHistory, } from "../../ai/estimate.js";
|
|
5
5
|
import { loadPrices } from "../../ai/pricing.js";
|
|
6
6
|
import { cacheSet } from "../../store/cache_db.js";
|
|
7
7
|
export async function runProbe(options) {
|
|
@@ -25,7 +25,7 @@ export async function runProbe(options) {
|
|
|
25
25
|
for (const reason of analysis.reasons)
|
|
26
26
|
console.log(` - ${reason}`);
|
|
27
27
|
// The estimate is separate from the recommendation: the flags say what to
|
|
28
|
-
// read, this says what that costs in time and dollars
|
|
28
|
+
// read, this says what that costs in time and dollars. Prices are
|
|
29
29
|
// only fetched when an OpenRouter model would actually be billed, so a
|
|
30
30
|
// plain `--ai=none` probe makes no network call.
|
|
31
31
|
const needsPrices = options.ai === "openrouter" &&
|
|
@@ -50,15 +50,7 @@ export async function runProbe(options) {
|
|
|
50
50
|
console.log(` AI jobs: ${estimate.extract} extract + ${estimate.synth} synth`);
|
|
51
51
|
console.log(` tokens: ${range(estimate.tokensIn)} in · ${range(estimate.tokensOut)} out`);
|
|
52
52
|
console.log(` time: ${secondsRange(estimate.seconds)}s`);
|
|
53
|
-
|
|
54
|
-
console.log(` cost: $${estimate.usd[0].toFixed(4)}-$${estimate.usd[1].toFixed(4)}`);
|
|
55
|
-
}
|
|
56
|
-
else if (needsPrices && prices.source === "unavailable") {
|
|
57
|
-
console.log(" cost: unknown (could not read OpenRouter prices)");
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
console.log(" cost: set --ai=openrouter with both models to estimate dollars");
|
|
61
|
-
}
|
|
53
|
+
console.log(` cost: ${estimateCostLine(estimate, options.ai, needsPrices && prices.source === "unavailable")}`);
|
|
62
54
|
console.log(` basis: ${estimate.basis === "history"
|
|
63
55
|
? "this repository's recorded jobs"
|
|
64
56
|
: "the cm-dx-lab calibration"}; an estimate, not a bill`);
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
/** The command registry (CORE-20).
|
|
2
|
-
*
|
|
3
|
-
* One place that knows every command, its aliases, its usage lines, its flags
|
|
4
|
-
* and its examples. Everything the user reads as help is rendered from here,
|
|
5
|
-
* so a flag cannot drift out of the help and a command cannot be documented
|
|
6
|
-
* twice with different wording. It is also the source CORE-81 will export to
|
|
7
|
-
* the command reference, which is why it is data rather than template strings.
|
|
8
|
-
*/
|
|
9
1
|
export type FlagSpec = {
|
|
10
2
|
/** Without the leading dashes, e.g. `include-codebase`. */
|
|
11
3
|
name: string;
|
|
@@ -57,6 +49,6 @@ export declare function unknownOptionMessage(input: string, command?: string): s
|
|
|
57
49
|
export declare function renderGlobalHelp(): string;
|
|
58
50
|
/** One command's help: usage, flags grouped, examples, docs link. */
|
|
59
51
|
export declare function renderCommandHelp(name: string): string | undefined;
|
|
60
|
-
/** The registry as markdown, so
|
|
52
|
+
/** The registry as markdown, so the command reference can be built from
|
|
61
53
|
* the same data the runtime help uses. */
|
|
62
54
|
export declare function registryToMarkdown(): string;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
/** The command registry
|
|
1
|
+
/** The command registry.
|
|
2
2
|
*
|
|
3
3
|
* One place that knows every command, its aliases, its usage lines, its flags
|
|
4
4
|
* and its examples. Everything the user reads as help is rendered from here,
|
|
5
5
|
* so a flag cannot drift out of the help and a command cannot be documented
|
|
6
|
-
* twice with different wording. It is also the source
|
|
7
|
-
*
|
|
6
|
+
* twice with different wording. It is also the source the command reference is
|
|
7
|
+
* generated from, which is why it is data rather than template strings.
|
|
8
8
|
*/
|
|
9
|
+
import { AI_PROVIDERS } from "../../ai/provider.js";
|
|
9
10
|
const OUTPUT_FLAGS = {
|
|
10
11
|
title: "Output and diagnostics",
|
|
11
12
|
flags: [
|
|
@@ -17,12 +18,24 @@ const OUTPUT_FLAGS = {
|
|
|
17
18
|
{ name: "log-time", description: "Print how long each phase took." },
|
|
18
19
|
],
|
|
19
20
|
};
|
|
21
|
+
/** `review` alone can print for a CI run. */
|
|
22
|
+
const REVIEW_OUTPUT_FLAGS = {
|
|
23
|
+
title: OUTPUT_FLAGS.title,
|
|
24
|
+
flags: [
|
|
25
|
+
...OUTPUT_FLAGS.flags,
|
|
26
|
+
{
|
|
27
|
+
name: "output",
|
|
28
|
+
value: "github",
|
|
29
|
+
description: "Print GitHub workflow annotations and write the job summary instead of prose. Cannot be used with --json.",
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
};
|
|
20
33
|
const AI_FLAGS = {
|
|
21
34
|
title: "AI",
|
|
22
35
|
flags: [
|
|
23
36
|
{
|
|
24
37
|
name: "ai",
|
|
25
|
-
value: "
|
|
38
|
+
value: AI_PROVIDERS.join("|"),
|
|
26
39
|
default: "none",
|
|
27
40
|
description: "Which provider writes the review.",
|
|
28
41
|
},
|
|
@@ -35,13 +48,11 @@ const AI_FLAGS = {
|
|
|
35
48
|
{
|
|
36
49
|
name: "low-model",
|
|
37
50
|
value: "ID",
|
|
38
|
-
default: "openai/gpt-oss-120b",
|
|
39
51
|
description: "Model for extraction work.",
|
|
40
52
|
},
|
|
41
53
|
{
|
|
42
54
|
name: "high-model",
|
|
43
55
|
value: "ID",
|
|
44
|
-
default: "openai/gpt-5.6-luna",
|
|
45
56
|
description: "Model for the review itself.",
|
|
46
57
|
},
|
|
47
58
|
],
|
|
@@ -210,7 +221,7 @@ export const COMMANDS = [
|
|
|
210
221
|
{
|
|
211
222
|
name: "sync",
|
|
212
223
|
// `remake` is the 0.4.13 spelling. It still runs the same handler but is
|
|
213
|
-
// not advertised, so nothing the user reads says "remake"
|
|
224
|
+
// not advertised, so nothing the user reads says "remake".
|
|
214
225
|
aliases: ["remake"],
|
|
215
226
|
summary: "Rebuild the review guides for a repository that already has them.",
|
|
216
227
|
usage: ["co-maintainer sync owner/repo [options]"],
|
|
@@ -317,6 +328,14 @@ export const COMMANDS = [
|
|
|
317
328
|
value: "TOKEN",
|
|
318
329
|
description: "Override the configured remote token for this run (needs --remote).",
|
|
319
330
|
},
|
|
331
|
+
{
|
|
332
|
+
name: "remote-byok",
|
|
333
|
+
description: "Send your own AI key with a remote review (needs --remote).",
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: "no-remote-byok",
|
|
337
|
+
description: "Do not send your own AI key, even if remote-byok-default is on.",
|
|
338
|
+
},
|
|
320
339
|
{
|
|
321
340
|
name: "sync-before-review",
|
|
322
341
|
description: "Rebuild the guides before reviewing.",
|
|
@@ -338,13 +357,15 @@ export const COMMANDS = [
|
|
|
338
357
|
},
|
|
339
358
|
AI_FLAGS,
|
|
340
359
|
GITHUB_FLAGS,
|
|
341
|
-
|
|
360
|
+
REVIEW_OUTPUT_FLAGS,
|
|
342
361
|
],
|
|
343
362
|
examples: [
|
|
344
363
|
"co-maintainer review owner/repo 42 --json",
|
|
345
364
|
"co-maintainer review --json --disable-codegraph",
|
|
346
365
|
"co-maintainer review --remote --json",
|
|
347
366
|
"co-maintainer review --remote --remote-host=https://review.example.com --remote-token=cmr_...",
|
|
367
|
+
"co-maintainer review --remote --remote-byok --json",
|
|
368
|
+
"co-maintainer review --remote --output=github",
|
|
348
369
|
],
|
|
349
370
|
},
|
|
350
371
|
{
|
|
@@ -469,6 +490,33 @@ export const COMMANDS = [
|
|
|
469
490
|
value: "TOKEN",
|
|
470
491
|
description: "The remote review token.",
|
|
471
492
|
},
|
|
493
|
+
{
|
|
494
|
+
name: "remote-byok-policy",
|
|
495
|
+
value: "off|allow|require",
|
|
496
|
+
default: "off",
|
|
497
|
+
description: "Whether a remote client may send its own AI key.",
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
name: "remote-byok",
|
|
501
|
+
value: "KEY",
|
|
502
|
+
description: "Your own AI key to send with a remote review.",
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
name: "remote-byok-default",
|
|
506
|
+
value: "on|off",
|
|
507
|
+
default: "off",
|
|
508
|
+
description: "Send remote-byok on every --remote review by default.",
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
name: "review-policy",
|
|
512
|
+
value: "everyone|trusted-auto|on-request-only",
|
|
513
|
+
description: "The review policy new repositories start with.",
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
name: "review-policy-file",
|
|
517
|
+
value: "PATH",
|
|
518
|
+
description: "Read a custom review policy from a JSON file and use it as that default.",
|
|
519
|
+
},
|
|
472
520
|
{
|
|
473
521
|
name: "review-blocking",
|
|
474
522
|
value: "model|severity",
|
|
@@ -492,7 +540,7 @@ export const COMMANDS = [
|
|
|
492
540
|
},
|
|
493
541
|
{
|
|
494
542
|
name: "no-verify",
|
|
495
|
-
description: "Do not check the key and model against
|
|
543
|
+
description: "Do not check the key and model against the provider.",
|
|
496
544
|
},
|
|
497
545
|
{
|
|
498
546
|
name: "unset",
|
|
@@ -555,6 +603,23 @@ export const COMMANDS = [
|
|
|
555
603
|
],
|
|
556
604
|
examples: ["co-maintainer serve --port=5000"],
|
|
557
605
|
},
|
|
606
|
+
{
|
|
607
|
+
name: "rollback",
|
|
608
|
+
summary: "Go back to the version before the last upgrade, restoring its data.",
|
|
609
|
+
usage: ["co-maintainer rollback [--yes]"],
|
|
610
|
+
groups: [
|
|
611
|
+
{
|
|
612
|
+
title: "Rollback",
|
|
613
|
+
flags: [
|
|
614
|
+
{
|
|
615
|
+
name: "yes",
|
|
616
|
+
description: "Skip the confirmation. Needed when there is no terminal.",
|
|
617
|
+
},
|
|
618
|
+
],
|
|
619
|
+
},
|
|
620
|
+
],
|
|
621
|
+
examples: ["co-maintainer rollback", "co-maintainer rollback --yes"],
|
|
622
|
+
},
|
|
558
623
|
{
|
|
559
624
|
name: "version",
|
|
560
625
|
summary: "Print the version.",
|
|
@@ -692,7 +757,7 @@ export function renderCommandHelp(name) {
|
|
|
692
757
|
function cell(text) {
|
|
693
758
|
return text.replace(/\|/g, "\\|");
|
|
694
759
|
}
|
|
695
|
-
/** The registry as markdown, so
|
|
760
|
+
/** The registry as markdown, so the command reference can be built from
|
|
696
761
|
* the same data the runtime help uses. */
|
|
697
762
|
export function registryToMarkdown() {
|
|
698
763
|
const lines = [];
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { parseReviewArgs } from "../review_args.js";
|
|
2
2
|
import { setCliInteractive } from "../args.js";
|
|
3
|
-
import { emptyAiMetrics, recordAiCost } from "../../services/setup.js";
|
|
3
|
+
import { addAiMetrics, emptyAiMetrics, recordAiCost, } from "../../services/setup.js";
|
|
4
|
+
import { costUsage } from "../../util/cost.js";
|
|
5
|
+
import { costLabel } from "../../util/run_summary.js";
|
|
4
6
|
import { reviewPullRequest } from "../../pr/reviewer.js";
|
|
5
7
|
import { GhClient } from "../../github/gh.js";
|
|
6
8
|
import { log, startHeartbeat, timed, withCliLogsToStderr, } from "../../util/log.js";
|
|
@@ -11,6 +13,7 @@ import { buildPrReviewJson, resolvedFromFirstReview, reviewExitCodeFromResolved,
|
|
|
11
13
|
import { parseFindings } from "../../pr/findings.js";
|
|
12
14
|
import { exitWith } from "../error.js";
|
|
13
15
|
import { printRunSummary, summaryFromMetrics } from "../../util/run_summary.js";
|
|
16
|
+
import { emitGithubOutput } from "../review_github.js";
|
|
14
17
|
import { formatHumanReview, humanFindingsFromResolved, } from "../review_result.js";
|
|
15
18
|
export async function runReviewFromCli(args) {
|
|
16
19
|
const parsed = await parseReviewArgs(args);
|
|
@@ -27,6 +30,7 @@ export async function runReviewFromCli(args) {
|
|
|
27
30
|
export async function runReview(options) {
|
|
28
31
|
await runReviewPr(options, {
|
|
29
32
|
json: false,
|
|
33
|
+
output: undefined,
|
|
30
34
|
disableCodegraph: false,
|
|
31
35
|
allowToolInstall: false,
|
|
32
36
|
remakeBeforeReview: false,
|
|
@@ -44,24 +48,18 @@ async function runReviewPr(options, cli) {
|
|
|
44
48
|
}
|
|
45
49
|
log("review", `reading PR #${options.prNumber} in ${options.repo} via gh`);
|
|
46
50
|
const result = await timed("review GitHub collection and AI", options.logTime, () => reviewPullRequest(new GhClient(options.debug), options, async (response) => {
|
|
47
|
-
aiMetrics
|
|
48
|
-
aiMetrics.tokensIn += response.tokensIn;
|
|
49
|
-
aiMetrics.tokensOut += response.tokensOut;
|
|
50
|
-
if (response.cost === undefined)
|
|
51
|
-
aiMetrics.costKnown = false;
|
|
52
|
-
else
|
|
53
|
-
aiMetrics.cost += response.cost;
|
|
51
|
+
addAiMetrics(aiMetrics, response);
|
|
54
52
|
await recordAiCost(options.repo, "review_pull_request", response);
|
|
55
53
|
}));
|
|
56
54
|
const durationMs = Math.round(performance.now() - operationStarted);
|
|
57
55
|
const usage = {
|
|
58
56
|
tokensIn: aiMetrics.tokensIn,
|
|
59
57
|
tokensOut: aiMetrics.tokensOut,
|
|
60
|
-
|
|
58
|
+
...costUsage(aiMetrics),
|
|
61
59
|
};
|
|
62
60
|
// The engine reports the codegraph state it actually reached, instead of
|
|
63
61
|
// the old `useCodegraph ? "used" : "disabled"` that claimed "used" while
|
|
64
|
-
// stderr said codegraph was missing
|
|
62
|
+
// stderr said codegraph was missing.
|
|
65
63
|
const codegraphState = result.codegraphState;
|
|
66
64
|
const resolved = resolvedFromFirstReview(parseFindings(result.text), new Map());
|
|
67
65
|
if (cli.json) {
|
|
@@ -77,8 +75,15 @@ async function runReviewPr(options, cli) {
|
|
|
77
75
|
reviewBlocking: options.reviewBlocking,
|
|
78
76
|
}));
|
|
79
77
|
}
|
|
78
|
+
else if (cli.output === "github") {
|
|
79
|
+
emitGithubOutput({
|
|
80
|
+
title: `${options.repo} · PR #${options.prNumber}`,
|
|
81
|
+
findings: humanFindingsFromResolved(resolved, options.reviewBlocking),
|
|
82
|
+
run: summaryFromMetrics(aiMetrics, performance.now() - operationStarted),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
80
85
|
else {
|
|
81
|
-
// One presentation layer for local, PR and remote reviews
|
|
86
|
+
// One presentation layer for local, PR and remote reviews.
|
|
82
87
|
console.log("\n" +
|
|
83
88
|
formatHumanReview({
|
|
84
89
|
title: `co-maintainer review · ${options.repo} · PR #${options.prNumber}`,
|
|
@@ -89,7 +94,7 @@ async function runReviewPr(options, cli) {
|
|
|
89
94
|
"\n");
|
|
90
95
|
printRunSummary(summaryFromMetrics(aiMetrics, performance.now() - operationStarted));
|
|
91
96
|
if (options.logTime) {
|
|
92
|
-
log("time", `AI total · calls=${aiMetrics.calls} · input=${aiMetrics.tokensIn} tokens · output=${aiMetrics.tokensOut} tokens · cost=${aiMetrics
|
|
97
|
+
log("time", `AI total · calls=${aiMetrics.calls} · input=${aiMetrics.tokensIn} tokens · output=${aiMetrics.tokensOut} tokens · cost=${costLabel(aiMetrics)}`);
|
|
93
98
|
console.error(`[time] total review · ${((performance.now() - operationStarted) / 1000).toFixed(2)}s`);
|
|
94
99
|
}
|
|
95
100
|
}
|
|
@@ -100,7 +105,7 @@ async function runReviewPr(options, cli) {
|
|
|
100
105
|
finally {
|
|
101
106
|
// A heartbeat left running on the error path keeps the event loop alive
|
|
102
107
|
// forever, so the process would never drain and the exit code would never
|
|
103
|
-
// be applied.
|
|
108
|
+
// be applied.
|
|
104
109
|
stopHeartbeat();
|
|
105
110
|
}
|
|
106
111
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runRollback(args: string[]): Promise<void>;
|