co-maintainer 0.4.0-beta.1
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 -0
- package/README.md +105 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +9 -0
- package/dist/mod.d.ts +4 -0
- package/dist/mod.js +2 -0
- package/dist/package.json +54 -0
- package/dist/src/ai/batch.d.ts +16 -0
- package/dist/src/ai/batch.js +117 -0
- package/dist/src/ai/fake.d.ts +7 -0
- package/dist/src/ai/fake.js +40 -0
- package/dist/src/ai/hetzner.d.ts +16 -0
- package/dist/src/ai/hetzner.js +69 -0
- package/dist/src/ai/mermaid.d.ts +27 -0
- package/dist/src/ai/mermaid.js +457 -0
- package/dist/src/ai/mermaid_loop.d.ts +12 -0
- package/dist/src/ai/mermaid_loop.js +100 -0
- package/dist/src/ai/openrouter.d.ts +9 -0
- package/dist/src/ai/openrouter.js +29 -0
- package/dist/src/ai/provider.d.ts +4 -0
- package/dist/src/ai/provider.js +63 -0
- package/dist/src/cli/args.d.ts +5 -0
- package/dist/src/cli/args.js +252 -0
- package/dist/src/cli/commands/probe.d.ts +2 -0
- package/dist/src/cli/commands/probe.js +122 -0
- package/dist/src/cli/commands/review.d.ts +3 -0
- package/dist/src/cli/commands/review.js +84 -0
- package/dist/src/cli/commands/serve.d.ts +18 -0
- package/dist/src/cli/commands/serve.js +183 -0
- package/dist/src/cli/commands/set.d.ts +7 -0
- package/dist/src/cli/commands/set.js +171 -0
- package/dist/src/cli/main.d.ts +1 -0
- package/dist/src/cli/main.js +27 -0
- package/dist/src/cli/prompt.d.ts +4 -0
- package/dist/src/cli/prompt.js +26 -0
- package/dist/src/cli/review_args.d.ts +28 -0
- package/dist/src/cli/review_args.js +84 -0
- package/dist/src/cli/review_output.d.ts +2 -0
- package/dist/src/cli/review_output.js +10 -0
- package/dist/src/cli/review_result.d.ts +81 -0
- package/dist/src/cli/review_result.js +281 -0
- package/dist/src/config.d.ts +108 -0
- package/dist/src/config.js +138 -0
- package/dist/src/git/default_branch.d.ts +5 -0
- package/dist/src/git/default_branch.js +22 -0
- package/dist/src/github/app.d.ts +43 -0
- package/dist/src/github/app.js +120 -0
- package/dist/src/github/client.d.ts +17 -0
- package/dist/src/github/client.js +66 -0
- package/dist/src/github/collect.d.ts +7 -0
- package/dist/src/github/collect.js +368 -0
- package/dist/src/github/gh.d.ts +8 -0
- package/dist/src/github/gh.js +63 -0
- package/dist/src/github/jwt.d.ts +12 -0
- package/dist/src/github/jwt.js +84 -0
- package/dist/src/github/oauth.d.ts +9 -0
- package/dist/src/github/oauth.js +51 -0
- package/dist/src/github/pat.d.ts +10 -0
- package/dist/src/github/pat.js +60 -0
- package/dist/src/knowledge/facts.d.ts +3 -0
- package/dist/src/knowledge/facts.js +325 -0
- package/dist/src/knowledge/guide.d.ts +6 -0
- package/dist/src/knowledge/guide.js +82 -0
- package/dist/src/knowledge/probe.d.ts +12 -0
- package/dist/src/knowledge/probe.js +144 -0
- package/dist/src/knowledge/sections.d.ts +2 -0
- package/dist/src/knowledge/sections.js +13 -0
- package/dist/src/knowledge/skill.d.ts +10 -0
- package/dist/src/knowledge/skill.js +106 -0
- package/dist/src/knowledge/synthesis.d.ts +7 -0
- package/dist/src/knowledge/synthesis.js +315 -0
- package/dist/src/knowledge/types.d.ts +49 -0
- package/dist/src/knowledge/types.js +1 -0
- package/dist/src/knowledge/validate.d.ts +6 -0
- package/dist/src/knowledge/validate.js +134 -0
- package/dist/src/local/carry_over_store.d.ts +18 -0
- package/dist/src/local/carry_over_store.js +40 -0
- package/dist/src/local/codegraph_exclude.d.ts +3 -0
- package/dist/src/local/codegraph_exclude.js +26 -0
- package/dist/src/local/codegraph_prepare.d.ts +17 -0
- package/dist/src/local/codegraph_prepare.js +51 -0
- package/dist/src/local/git_ops.d.ts +12 -0
- package/dist/src/local/git_ops.js +98 -0
- package/dist/src/local/git_parse.d.ts +27 -0
- package/dist/src/local/git_parse.js +131 -0
- package/dist/src/local/git_revision.d.ts +15 -0
- package/dist/src/local/git_revision.js +141 -0
- package/dist/src/local/git_untracked.d.ts +21 -0
- package/dist/src/local/git_untracked.js +79 -0
- package/dist/src/local/review_local.d.ts +4 -0
- package/dist/src/local/review_local.js +273 -0
- package/dist/src/local/review_lock.d.ts +7 -0
- package/dist/src/local/review_lock.js +45 -0
- package/dist/src/pr/checkout.d.ts +25 -0
- package/dist/src/pr/checkout.js +136 -0
- package/dist/src/pr/codegraph_tool_args.d.ts +8 -0
- package/dist/src/pr/codegraph_tool_args.js +34 -0
- package/dist/src/pr/codegraph_tools.d.ts +15 -0
- package/dist/src/pr/codegraph_tools.js +408 -0
- package/dist/src/pr/diff_summary.d.ts +6 -0
- package/dist/src/pr/diff_summary.js +52 -0
- package/dist/src/pr/findings.d.ts +14 -0
- package/dist/src/pr/findings.js +94 -0
- package/dist/src/pr/github_revision.d.ts +4 -0
- package/dist/src/pr/github_revision.js +39 -0
- package/dist/src/pr/hunks.d.ts +18 -0
- package/dist/src/pr/hunks.js +73 -0
- package/dist/src/pr/reviewer.d.ts +40 -0
- package/dist/src/pr/reviewer.js +582 -0
- package/dist/src/pr/rounds.d.ts +17 -0
- package/dist/src/pr/rounds.js +43 -0
- package/dist/src/pr/scope.d.ts +35 -0
- package/dist/src/pr/scope.js +84 -0
- package/dist/src/pr/snapshot.d.ts +11 -0
- package/dist/src/pr/snapshot.js +1 -0
- package/dist/src/pr/suggestion.d.ts +17 -0
- package/dist/src/pr/suggestion.js +70 -0
- package/dist/src/remote/client.d.ts +4 -0
- package/dist/src/remote/client.js +199 -0
- package/dist/src/remote/client_tools.d.ts +13 -0
- package/dist/src/remote/client_tools.js +30 -0
- package/dist/src/remote/fixtures.d.ts +1 -0
- package/dist/src/remote/fixtures.js +30 -0
- package/dist/src/remote/revision_from_submit.d.ts +2 -0
- package/dist/src/remote/revision_from_submit.js +26 -0
- package/dist/src/remote/schema.d.ts +14 -0
- package/dist/src/remote/schema.js +30 -0
- package/dist/src/remote/server/body.d.ts +18 -0
- package/dist/src/remote/server/body.js +73 -0
- package/dist/src/remote/server/routes.d.ts +1 -0
- package/dist/src/remote/server/routes.js +150 -0
- package/dist/src/remote/server/sessions.d.ts +16 -0
- package/dist/src/remote/server/sessions.js +48 -0
- package/dist/src/remote/server/sync.d.ts +3 -0
- package/dist/src/remote/server/sync.js +90 -0
- package/dist/src/remote/tool_bridge.d.ts +15 -0
- package/dist/src/remote/tool_bridge.js +84 -0
- package/dist/src/remote/validate.d.ts +8 -0
- package/dist/src/remote/validate.js +248 -0
- package/dist/src/review/carry_over.d.ts +63 -0
- package/dist/src/review/carry_over.js +372 -0
- package/dist/src/review/engine.d.ts +13 -0
- package/dist/src/review/engine.js +4 -0
- package/dist/src/review/guides.d.ts +12 -0
- package/dist/src/review/guides.js +44 -0
- package/dist/src/review/revision.d.ts +26 -0
- package/dist/src/review/revision.js +57 -0
- package/dist/src/server/api/activity.d.ts +1 -0
- package/dist/src/server/api/activity.js +12 -0
- package/dist/src/server/api/analytics.d.ts +1 -0
- package/dist/src/server/api/analytics.js +9 -0
- package/dist/src/server/api/installations.d.ts +4 -0
- package/dist/src/server/api/installations.js +24 -0
- package/dist/src/server/api/jobs.d.ts +1 -0
- package/dist/src/server/api/jobs.js +86 -0
- package/dist/src/server/api/json_body.d.ts +1 -0
- package/dist/src/server/api/json_body.js +14 -0
- package/dist/src/server/api/remote_tokens.d.ts +1 -0
- package/dist/src/server/api/remote_tokens.js +71 -0
- package/dist/src/server/api/repos.d.ts +4 -0
- package/dist/src/server/api/repos.js +136 -0
- package/dist/src/server/api/settings.d.ts +1 -0
- package/dist/src/server/api/settings.js +210 -0
- package/dist/src/server/app.d.ts +27 -0
- package/dist/src/server/app.js +147 -0
- package/dist/src/server/assets.d.ts +3 -0
- package/dist/src/server/assets.js +21 -0
- package/dist/src/server/auth.d.ts +44 -0
- package/dist/src/server/auth.js +188 -0
- package/dist/src/server/errors.d.ts +8 -0
- package/dist/src/server/errors.js +10 -0
- package/dist/src/server/http.d.ts +8 -0
- package/dist/src/server/http.js +117 -0
- package/dist/src/server/logo.d.ts +1 -0
- package/dist/src/server/logo.js +2 -0
- package/dist/src/server/pages/activity.d.ts +4 -0
- package/dist/src/server/pages/activity.js +200 -0
- package/dist/src/server/pages/add_repo.d.ts +9 -0
- package/dist/src/server/pages/add_repo.js +56 -0
- package/dist/src/server/pages/analytics.d.ts +2 -0
- package/dist/src/server/pages/analytics.js +193 -0
- package/dist/src/server/pages/client.d.ts +2 -0
- package/dist/src/server/pages/client.js +2 -0
- package/dist/src/server/pages/home.d.ts +16 -0
- package/dist/src/server/pages/home.js +87 -0
- package/dist/src/server/pages/knowledge.d.ts +2 -0
- package/dist/src/server/pages/knowledge.js +64 -0
- package/dist/src/server/pages/layout.d.ts +32 -0
- package/dist/src/server/pages/layout.js +230 -0
- package/dist/src/server/pages/pr.d.ts +2 -0
- package/dist/src/server/pages/pr.js +109 -0
- package/dist/src/server/pages/repo.d.ts +2 -0
- package/dist/src/server/pages/repo.js +92 -0
- package/dist/src/server/pages/repo_prs.d.ts +2 -0
- package/dist/src/server/pages/repo_prs.js +75 -0
- package/dist/src/server/pages/repo_remote.d.ts +2 -0
- package/dist/src/server/pages/repo_remote.js +38 -0
- package/dist/src/server/pages/repo_settings.d.ts +3 -0
- package/dist/src/server/pages/repo_settings.js +127 -0
- package/dist/src/server/pages/router.d.ts +17 -0
- package/dist/src/server/pages/router.js +312 -0
- package/dist/src/server/pages/settings.d.ts +2 -0
- package/dist/src/server/pages/settings.js +337 -0
- package/dist/src/server/pages/setup.d.ts +5 -0
- package/dist/src/server/pages/setup.js +38 -0
- package/dist/src/server/pages/styles.d.ts +2 -0
- package/dist/src/server/pages/styles.js +2 -0
- package/dist/src/server/webhook/index.d.ts +1 -0
- package/dist/src/server/webhook/index.js +64 -0
- package/dist/src/server/webhook/signature.d.ts +7 -0
- package/dist/src/server/webhook/signature.js +41 -0
- package/dist/src/services/credentials.d.ts +19 -0
- package/dist/src/services/credentials.js +146 -0
- package/dist/src/services/dashboard.d.ts +165 -0
- package/dist/src/services/dashboard.js +200 -0
- package/dist/src/services/drift.d.ts +13 -0
- package/dist/src/services/drift.js +76 -0
- package/dist/src/services/jobs.d.ts +34 -0
- package/dist/src/services/jobs.js +222 -0
- package/dist/src/services/remote_review.d.ts +10 -0
- package/dist/src/services/remote_review.js +274 -0
- package/dist/src/services/remote_review_abort.d.ts +3 -0
- package/dist/src/services/remote_review_abort.js +11 -0
- package/dist/src/services/remote_token_jobs.d.ts +1 -0
- package/dist/src/services/remote_token_jobs.js +20 -0
- package/dist/src/services/remote_tokens.d.ts +13 -0
- package/dist/src/services/remote_tokens.js +57 -0
- package/dist/src/services/replies.d.ts +7 -0
- package/dist/src/services/replies.js +248 -0
- package/dist/src/services/review.d.ts +35 -0
- package/dist/src/services/review.js +671 -0
- package/dist/src/services/setup.d.ts +29 -0
- package/dist/src/services/setup.js +364 -0
- package/dist/src/services/webhook.d.ts +22 -0
- package/dist/src/services/webhook.js +401 -0
- package/dist/src/store/app_db.d.ts +10 -0
- package/dist/src/store/app_db.js +85 -0
- package/dist/src/store/cache_db.d.ts +4 -0
- package/dist/src/store/cache_db.js +63 -0
- package/dist/src/store/deliveries.d.ts +12 -0
- package/dist/src/store/deliveries.js +23 -0
- package/dist/src/store/drift.d.ts +4 -0
- package/dist/src/store/drift.js +22 -0
- package/dist/src/store/findings.d.ts +31 -0
- package/dist/src/store/findings.js +64 -0
- package/dist/src/store/installations.d.ts +9 -0
- package/dist/src/store/installations.js +35 -0
- package/dist/src/store/job_logs.d.ts +5 -0
- package/dist/src/store/job_logs.js +17 -0
- package/dist/src/store/jobs.d.ts +27 -0
- package/dist/src/store/jobs.js +86 -0
- package/dist/src/store/migrations.d.ts +5 -0
- package/dist/src/store/migrations.js +258 -0
- package/dist/src/store/remote_review_inputs.d.ts +11 -0
- package/dist/src/store/remote_review_inputs.js +25 -0
- package/dist/src/store/remote_tokens.d.ts +10 -0
- package/dist/src/store/remote_tokens.js +45 -0
- package/dist/src/store/replies.d.ts +24 -0
- package/dist/src/store/replies.js +79 -0
- package/dist/src/store/repos.d.ts +11 -0
- package/dist/src/store/repos.js +59 -0
- package/dist/src/store/reviews.d.ts +86 -0
- package/dist/src/store/reviews.js +220 -0
- package/dist/src/store/rows.d.ts +179 -0
- package/dist/src/store/rows.js +4 -0
- package/dist/src/store/sessions.d.ts +8 -0
- package/dist/src/store/sessions.js +34 -0
- package/dist/src/store/skill_state.d.ts +3 -0
- package/dist/src/store/skill_state.js +24 -0
- package/dist/src/store/sqlite.d.ts +39 -0
- package/dist/src/store/sqlite.js +77 -0
- package/dist/src/store/subjects.d.ts +15 -0
- package/dist/src/store/subjects.js +78 -0
- package/dist/src/tools/codegraph.d.ts +58 -0
- package/dist/src/tools/codegraph.js +165 -0
- package/dist/src/tools/codegraph_exec.d.ts +13 -0
- package/dist/src/tools/codegraph_exec.js +66 -0
- package/dist/src/types.d.ts +89 -0
- package/dist/src/types.js +1 -0
- package/dist/src/util/atomic.d.ts +4 -0
- package/dist/src/util/atomic.js +78 -0
- package/dist/src/util/keyed_lock.d.ts +3 -0
- package/dist/src/util/keyed_lock.js +33 -0
- package/dist/src/util/log.d.ts +11 -0
- package/dist/src/util/log.js +42 -0
- package/dist/src/util/redact.d.ts +5 -0
- package/dist/src/util/redact.js +30 -0
- package/dist/src/util/runtime.d.ts +90 -0
- package/dist/src/util/runtime.js +206 -0
- package/dist/src/util/time.d.ts +9 -0
- package/dist/src/util/time.js +36 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +5 -0
- package/package.json +54 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { prepareConfig } from "../config.js";
|
|
2
|
+
import { getEnv } from "../util/runtime.js";
|
|
3
|
+
import { askLine } from "./prompt.js";
|
|
4
|
+
function die(message) {
|
|
5
|
+
throw new Error(message);
|
|
6
|
+
}
|
|
7
|
+
function numberOption(value, name) {
|
|
8
|
+
const number = Number(value);
|
|
9
|
+
if (!Number.isInteger(number) || number < 0) {
|
|
10
|
+
die(`${name} must be a non-negative integer`);
|
|
11
|
+
}
|
|
12
|
+
return number;
|
|
13
|
+
}
|
|
14
|
+
const commands = ["probe", "init", "remake", "review"];
|
|
15
|
+
export const defaultLowModel = "openai/gpt-oss-120b";
|
|
16
|
+
const defaultHighModel = "openai/gpt-5.6-luna";
|
|
17
|
+
let cliInteractive = true;
|
|
18
|
+
/** Plan §8.7 — `--json` must not prompt. */
|
|
19
|
+
export function setCliInteractive(value) {
|
|
20
|
+
cliInteractive = value;
|
|
21
|
+
}
|
|
22
|
+
async function ask(label, fallback, required = false) {
|
|
23
|
+
if (!cliInteractive) {
|
|
24
|
+
if (fallback !== undefined && fallback !== "")
|
|
25
|
+
return fallback;
|
|
26
|
+
die(`Missing ${label}; pass it as a CLI option when running without an interactive terminal`);
|
|
27
|
+
}
|
|
28
|
+
if (process.stdin.isTTY !== true) {
|
|
29
|
+
die(`Missing ${label}; pass it as a CLI option when running without an interactive terminal`);
|
|
30
|
+
}
|
|
31
|
+
const value = await askLine(label, fallback);
|
|
32
|
+
if (required && !value)
|
|
33
|
+
die(`${label} is required`);
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
export async function parseArgs(args) {
|
|
37
|
+
const [command, repo, ...rest] = args;
|
|
38
|
+
if (!command ||
|
|
39
|
+
command === "help" ||
|
|
40
|
+
command === "--help" ||
|
|
41
|
+
command === "-h" ||
|
|
42
|
+
repo === "--help" ||
|
|
43
|
+
repo === "-h") {
|
|
44
|
+
console.log("Usage: co-maintainer <probe|init|remake|review> owner/repo [options]");
|
|
45
|
+
console.log(" co-maintainer review [options]");
|
|
46
|
+
console.log(" co-maintainer review owner/repo PR_NUMBER [options]");
|
|
47
|
+
console.log(" co-maintainer set --token=... --ai=... --low-model=... --high-model=... --auth=...");
|
|
48
|
+
console.log(" co-maintainer serve --port=N");
|
|
49
|
+
console.log(" --webhook-url=https://host/github/webhook [or CM_WEBHOOK_URL]");
|
|
50
|
+
console.log(" --env=PATH --debug --log-time --gh-concurrent=N --ai-concurrent=N");
|
|
51
|
+
console.log("Options: --include-codebase --include-pull-requests --include-pull-request-changes");
|
|
52
|
+
console.log(" --include-commit-history --include-how-repo-works");
|
|
53
|
+
console.log(" --max-commits=N --max-pr-months=N --max-pull-request-change-lines=N --max-comment=N");
|
|
54
|
+
console.log(" --auth=gh|pat --github-pat=... --ai=none|openrouter|hetzner --token=... --low-model=... --high-model=...");
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
if (!commands.includes(command)) {
|
|
58
|
+
die(`Unknown command: ${command}`);
|
|
59
|
+
}
|
|
60
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
61
|
+
console.log(`Usage: co-maintainer ${command} ...`);
|
|
62
|
+
console.log("Run co-maintainer --help for all options.");
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
if (repo === "--help" || repo === "-h") {
|
|
66
|
+
console.log(`Usage: co-maintainer ${command} ...`);
|
|
67
|
+
console.log("Run co-maintainer --help for all options.");
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
if (!repo || !/^[^/]+\/[^/]+$/.test(repo)) {
|
|
71
|
+
die("Repository must look like owner/repo");
|
|
72
|
+
}
|
|
73
|
+
const { config, envPath } = prepareConfig(args);
|
|
74
|
+
const env = (name) => getEnv(name);
|
|
75
|
+
const configDefault = config.defaults ?? {};
|
|
76
|
+
// Remembered from a prior `init`/`remake` on this exact repo — never a
|
|
77
|
+
// secret, so it can safely fill in everything except the API token.
|
|
78
|
+
const repoConfig = config.repos?.[repo] ?? {};
|
|
79
|
+
let prNumber;
|
|
80
|
+
if (command === "review") {
|
|
81
|
+
const rawNumber = rest[0];
|
|
82
|
+
if (rawNumber && /^\d+$/.test(rawNumber)) {
|
|
83
|
+
rest.shift();
|
|
84
|
+
prNumber = Number(rawNumber);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const includeNames = [
|
|
88
|
+
"include-codebase",
|
|
89
|
+
"include-pull-requests",
|
|
90
|
+
"include-pull-request-changes",
|
|
91
|
+
"include-commit-history",
|
|
92
|
+
"include-how-repo-works",
|
|
93
|
+
];
|
|
94
|
+
const explicitlyIncluded = rest.some((arg) => includeNames.some((name) => arg === `--${name}`));
|
|
95
|
+
const includeKeys = {
|
|
96
|
+
"include-codebase": "includeCodebase",
|
|
97
|
+
"include-pull-requests": "includePullRequests",
|
|
98
|
+
"include-pull-request-changes": "includePullRequestChanges",
|
|
99
|
+
"include-commit-history": "includeCommitHistory",
|
|
100
|
+
"include-how-repo-works": "includeHowRepoWorks",
|
|
101
|
+
};
|
|
102
|
+
const enabled = (name) => explicitlyIncluded
|
|
103
|
+
? rest.includes(`--${name}`)
|
|
104
|
+
: (repoConfig[includeKeys[name]] ?? true);
|
|
105
|
+
const value = (name) => {
|
|
106
|
+
const arg = rest.find((item) => item.startsWith(`--${name}=`));
|
|
107
|
+
return arg
|
|
108
|
+
? numberOption(arg.slice(name.length + 3), `--${name}`)
|
|
109
|
+
: undefined;
|
|
110
|
+
};
|
|
111
|
+
const text = (name) => rest.find((item) => item.startsWith(`--${name}=`))?.slice(name.length + 3);
|
|
112
|
+
const choice = (name, allowed, fallback) => {
|
|
113
|
+
const prefix = `--${name}=`;
|
|
114
|
+
const raw = rest
|
|
115
|
+
.find((arg) => arg.startsWith(prefix))
|
|
116
|
+
?.slice(prefix.length);
|
|
117
|
+
if (!raw)
|
|
118
|
+
return fallback;
|
|
119
|
+
if (!allowed.includes(raw)) {
|
|
120
|
+
die(`${name} must be one of: ${allowed.join(", ")}`);
|
|
121
|
+
}
|
|
122
|
+
return raw;
|
|
123
|
+
};
|
|
124
|
+
for (const arg of rest) {
|
|
125
|
+
const known = includeNames.some((name) => arg === `--${name}`) ||
|
|
126
|
+
[
|
|
127
|
+
"max-commits",
|
|
128
|
+
"max-pr-months",
|
|
129
|
+
"max-pull-request-change-lines",
|
|
130
|
+
"max-comment",
|
|
131
|
+
"improve-matrix",
|
|
132
|
+
"env",
|
|
133
|
+
"gh-concurrent",
|
|
134
|
+
"ai-concurrent",
|
|
135
|
+
"auth",
|
|
136
|
+
"ai",
|
|
137
|
+
"token",
|
|
138
|
+
"github-pat",
|
|
139
|
+
"low-model",
|
|
140
|
+
"high-model",
|
|
141
|
+
].some((name) => arg.startsWith(`--${name}=`));
|
|
142
|
+
if (arg === "--codegraph")
|
|
143
|
+
die("Unknown option: --codegraph");
|
|
144
|
+
if (arg === "--debug" ||
|
|
145
|
+
arg === "--log-time" ||
|
|
146
|
+
arg === "--review-upstream" ||
|
|
147
|
+
arg === "--disable-codegraph")
|
|
148
|
+
continue;
|
|
149
|
+
if (arg.startsWith("--") && !known)
|
|
150
|
+
die(`Unknown option: ${arg}`);
|
|
151
|
+
}
|
|
152
|
+
const explicitAi = rest.some((arg) => arg.startsWith("--ai="));
|
|
153
|
+
const configuredAiRaw = env("CO_MAINTAINER_AI") ?? repoConfig.ai ?? config.ai;
|
|
154
|
+
if (configuredAiRaw &&
|
|
155
|
+
!["none", "openrouter", "hetzner"].includes(configuredAiRaw)) {
|
|
156
|
+
die("CO_MAINTAINER_AI/config.ai must be none, openrouter, or hetzner");
|
|
157
|
+
}
|
|
158
|
+
const configuredAi = configuredAiRaw;
|
|
159
|
+
const configuredAuthRaw = env("CO_MAINTAINER_AUTH") ?? repoConfig.auth ?? config.auth;
|
|
160
|
+
if (configuredAuthRaw && !["gh", "pat"].includes(configuredAuthRaw)) {
|
|
161
|
+
die("CO_MAINTAINER_AUTH/config.auth must be gh or pat");
|
|
162
|
+
}
|
|
163
|
+
const configuredAuth = configuredAuthRaw;
|
|
164
|
+
const auth = choice("auth", ["gh", "pat"], configuredAuth ?? "gh");
|
|
165
|
+
const githubPat = text("github-pat") ??
|
|
166
|
+
env("GITHUB_TOKEN") ??
|
|
167
|
+
env("GH_TOKEN") ??
|
|
168
|
+
config.githubPat;
|
|
169
|
+
if (auth === "pat" && !githubPat) {
|
|
170
|
+
die("--auth=pat requires a GitHub token; pass --github-pat=..., set GITHUB_TOKEN/GH_TOKEN, " +
|
|
171
|
+
"or run: co-maintainer set --github-pat=...");
|
|
172
|
+
}
|
|
173
|
+
let ai = choice("ai", ["none", "openrouter", "hetzner"], configuredAi ?? "none");
|
|
174
|
+
if (command === "review") {
|
|
175
|
+
if (explicitAi &&
|
|
176
|
+
choice("ai", ["none", "openrouter", "hetzner"], "none") !== "openrouter") {
|
|
177
|
+
die("review supports OpenRouter only");
|
|
178
|
+
}
|
|
179
|
+
ai = "openrouter";
|
|
180
|
+
}
|
|
181
|
+
else if (!explicitAi && !configuredAi && command !== "probe") {
|
|
182
|
+
const selected = await ask("AI provider (openrouter|hetzner)", "openrouter");
|
|
183
|
+
if (!["openrouter", "hetzner"].includes(selected)) {
|
|
184
|
+
die("AI provider must be openrouter or hetzner");
|
|
185
|
+
}
|
|
186
|
+
ai = selected;
|
|
187
|
+
}
|
|
188
|
+
let aiToken = text("token") ??
|
|
189
|
+
env("CO_MAINTAINER_TOKEN") ??
|
|
190
|
+
(ai === "openrouter" ? env("OPENROUTER_API_KEY") : undefined) ??
|
|
191
|
+
(ai === "hetzner" ? env("HETZNER_API_KEY") : undefined) ??
|
|
192
|
+
config.token;
|
|
193
|
+
let lowModel = text("low-model") ??
|
|
194
|
+
env("OPENROUTER_LOW_MODEL") ??
|
|
195
|
+
env("HETZNER_LOW_MODEL") ??
|
|
196
|
+
env("LOW_MODEL") ??
|
|
197
|
+
repoConfig.lowModel ??
|
|
198
|
+
config.lowModel;
|
|
199
|
+
let highModel = text("high-model") ??
|
|
200
|
+
env("OPENROUTER_HIGH_MODEL") ??
|
|
201
|
+
env("HETZNER_HIGH_MODEL") ??
|
|
202
|
+
env("HIGH_MODEL") ??
|
|
203
|
+
repoConfig.highModel ??
|
|
204
|
+
config.highModel;
|
|
205
|
+
if (command === "review") {
|
|
206
|
+
aiToken ??= await ask("openrouter API key", undefined, true);
|
|
207
|
+
highModel ??= await ask("high model", defaultHighModel, true);
|
|
208
|
+
}
|
|
209
|
+
else if (ai !== "none" && command !== "probe") {
|
|
210
|
+
aiToken ??= await ask(`${ai} API key`, undefined, true);
|
|
211
|
+
lowModel ??= await ask("low model", defaultLowModel, true);
|
|
212
|
+
highModel ??= await ask("high model", defaultHighModel, true);
|
|
213
|
+
}
|
|
214
|
+
if (command !== "review" &&
|
|
215
|
+
ai !== "none" &&
|
|
216
|
+
(!lowModel || !highModel || !aiToken)) {
|
|
217
|
+
die("--token, --low-model, and --high-model are required when AI is enabled");
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
command: command,
|
|
221
|
+
repo,
|
|
222
|
+
prNumber,
|
|
223
|
+
debug: rest.includes("--debug"),
|
|
224
|
+
logTime: rest.includes("--log-time"),
|
|
225
|
+
reviewUpstream: rest.includes("--review-upstream"),
|
|
226
|
+
useCodegraph: command === "review" ? !rest.includes("--disable-codegraph") : false,
|
|
227
|
+
envPath,
|
|
228
|
+
improveMatrix: value("improve-matrix") ?? 1,
|
|
229
|
+
ghConcurrent: Math.max(1, value("gh-concurrent") ?? 1),
|
|
230
|
+
aiConcurrent: Math.max(1, value("ai-concurrent") ?? 3),
|
|
231
|
+
auth,
|
|
232
|
+
githubPat,
|
|
233
|
+
ai,
|
|
234
|
+
aiToken,
|
|
235
|
+
lowModel,
|
|
236
|
+
highModel,
|
|
237
|
+
synthesisVersion: 16,
|
|
238
|
+
includeCodebase: enabled("include-codebase"),
|
|
239
|
+
includePullRequests: enabled("include-pull-requests"),
|
|
240
|
+
includePullRequestChanges: enabled("include-pull-request-changes"),
|
|
241
|
+
includeCommitHistory: enabled("include-commit-history"),
|
|
242
|
+
includeHowRepoWorks: enabled("include-how-repo-works"),
|
|
243
|
+
maxCommits: value("max-commits") ?? repoConfig.maxCommits ?? configDefault.maxCommits,
|
|
244
|
+
maxPrMonths: value("max-pr-months") ??
|
|
245
|
+
repoConfig.maxPrMonths ??
|
|
246
|
+
configDefault.maxPrMonths,
|
|
247
|
+
maxPullRequestChangeLines: value("max-pull-request-change-lines") ??
|
|
248
|
+
repoConfig.maxPullRequestChangeLines ??
|
|
249
|
+
configDefault.maxPullRequestChangeLines,
|
|
250
|
+
maxComments: value("max-comment") ?? repoConfig.maxComments,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { clientFor } from "../../services/setup.js";
|
|
2
|
+
import { analyzeProbe } from "../../knowledge/probe.js";
|
|
3
|
+
import { cacheSet } from "../../store/cache_db.js";
|
|
4
|
+
import { log, startHeartbeat, timed } from "../../util/log.js";
|
|
5
|
+
export async function runProbe(options) {
|
|
6
|
+
const operationStarted = performance.now();
|
|
7
|
+
const stopHeartbeat = startHeartbeat("probing repository");
|
|
8
|
+
const client = clientFor(options);
|
|
9
|
+
log("probe", `reading ${options.repo} via ${options.auth}`);
|
|
10
|
+
const meta = await timed("probe repository metadata", options.logTime, () => client.request(`repos/${options.repo}`));
|
|
11
|
+
let latestReleaseAt = "";
|
|
12
|
+
log("probe", "reading release metadata");
|
|
13
|
+
try {
|
|
14
|
+
const releases = await timed("probe release metadata", options.logTime, () => client.pages(`repos/${options.repo}/releases?per_page=1`, 1));
|
|
15
|
+
latestReleaseAt = String(releases[0]?.published_at ?? releases[0]?.created_at ?? "");
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// Release metadata is an optional probe signal.
|
|
19
|
+
}
|
|
20
|
+
log("probe", "reading pull request list");
|
|
21
|
+
const pulls = await timed("probe pull request listing", options.logTime, () => client.pages(`repos/${options.repo}/pulls?state=all&sort=updated&direction=desc`, undefined, (page, fetched) => log("probe", `pull request listing · page ${page} · fetched ${fetched} · total unknown`)));
|
|
22
|
+
const yearBuckets = new Map();
|
|
23
|
+
for (const pull of pulls) {
|
|
24
|
+
const year = new Date(String(pull.updated_at)).getFullYear();
|
|
25
|
+
yearBuckets.set(year, [...(yearBuckets.get(year) ?? []), pull]);
|
|
26
|
+
}
|
|
27
|
+
const sampleTargets = new Map();
|
|
28
|
+
for (const pull of pulls.slice(0, 30)) {
|
|
29
|
+
sampleTargets.set(Number(pull.number), pull);
|
|
30
|
+
}
|
|
31
|
+
for (const yearPulls of yearBuckets.values()) {
|
|
32
|
+
for (const pull of yearPulls.slice(0, 3)) {
|
|
33
|
+
sampleTargets.set(Number(pull.number), pull);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const samplePulls = [...sampleTargets.values()];
|
|
37
|
+
const detailSamples = [];
|
|
38
|
+
log("probe", `sampling ${samplePulls.length} pull request details · concurrency=${options.ghConcurrent}`);
|
|
39
|
+
await timed("probe PR detail sampling", options.logTime, async () => {
|
|
40
|
+
const details = new Array(samplePulls.length);
|
|
41
|
+
let cursor = 0;
|
|
42
|
+
let completed = 0;
|
|
43
|
+
const worker = async () => {
|
|
44
|
+
while (cursor < samplePulls.length) {
|
|
45
|
+
const index = cursor++;
|
|
46
|
+
const pull = samplePulls[index];
|
|
47
|
+
try {
|
|
48
|
+
details[index] = await client.request(`repos/${options.repo}/pulls/${Number(pull.number)}`);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// A missing detail should not invalidate the rest of the probe.
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
completed++;
|
|
55
|
+
log("probe", `PR detail sampling · ${completed}/${samplePulls.length} · ${Math.round((completed / samplePulls.length) * 100)}%`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
await Promise.all(Array.from({ length: Math.min(options.ghConcurrent, samplePulls.length) }, worker));
|
|
60
|
+
detailSamples.push(...details.filter((detail) => !!detail));
|
|
61
|
+
});
|
|
62
|
+
const branch = String(meta.default_branch ?? "main");
|
|
63
|
+
log("probe", `sampled ${detailSamples.length} PR details; reading ${branch} commit history`);
|
|
64
|
+
const commits = await timed("probe commit history", options.logTime, () => client.pages(`repos/${options.repo}/commits?sha=${encodeURIComponent(branch)}`, undefined, (page, fetched) => log("probe", `commit history · page ${page} · fetched ${fetched} · total unknown`)));
|
|
65
|
+
const analysis = await timed("probe analysis", options.logTime, async () => analyzeProbe({ ...meta, latest_release_at: latestReleaseAt }, pulls, detailSamples, commits));
|
|
66
|
+
const recommendation = ["--include-codebase"];
|
|
67
|
+
if (analysis.includePullRequests) {
|
|
68
|
+
recommendation.push("--include-pull-requests");
|
|
69
|
+
}
|
|
70
|
+
if (analysis.includePullRequestChanges) {
|
|
71
|
+
recommendation.push("--include-pull-request-changes");
|
|
72
|
+
}
|
|
73
|
+
if (analysis.includeCommitHistory) {
|
|
74
|
+
recommendation.push("--include-commit-history");
|
|
75
|
+
}
|
|
76
|
+
if (pulls.length || Boolean(meta.has_issues)) {
|
|
77
|
+
recommendation.push("--include-how-repo-works");
|
|
78
|
+
}
|
|
79
|
+
if (analysis.maxPullRequestChangeLines) {
|
|
80
|
+
recommendation.push(`--max-pull-request-change-lines=${analysis.maxPullRequestChangeLines}`);
|
|
81
|
+
}
|
|
82
|
+
if (analysis.maxPrMonths) {
|
|
83
|
+
recommendation.push(`--max-pr-months=${analysis.maxPrMonths}`);
|
|
84
|
+
}
|
|
85
|
+
if (analysis.maxCommits) {
|
|
86
|
+
recommendation.push(`--max-commits=${analysis.maxCommits}`);
|
|
87
|
+
}
|
|
88
|
+
const command = [
|
|
89
|
+
"co-maintainer",
|
|
90
|
+
"init",
|
|
91
|
+
options.repo,
|
|
92
|
+
...recommendation,
|
|
93
|
+
].join(" ");
|
|
94
|
+
const report = {
|
|
95
|
+
repo: options.repo,
|
|
96
|
+
...analysis.report,
|
|
97
|
+
recommendations: {
|
|
98
|
+
includePullRequests: analysis.includePullRequests,
|
|
99
|
+
includePullRequestChanges: analysis.includePullRequestChanges,
|
|
100
|
+
includeCommitHistory: analysis.includeCommitHistory,
|
|
101
|
+
maxPrMonths: analysis.maxPrMonths ?? "all",
|
|
102
|
+
maxCommits: analysis.maxCommits ?? "all",
|
|
103
|
+
maxPullRequestChangeLines: analysis.maxPullRequestChangeLines ?? "all",
|
|
104
|
+
},
|
|
105
|
+
reasons: analysis.reasons,
|
|
106
|
+
recommendedCommand: command,
|
|
107
|
+
createdAt: new Date().toISOString(),
|
|
108
|
+
};
|
|
109
|
+
await timed("probe report write", options.logTime, () => cacheSet("probe", options.repo, JSON.stringify(report)));
|
|
110
|
+
console.log(`\nrecommended\n ${command}`);
|
|
111
|
+
console.log("\nresearch");
|
|
112
|
+
console.log(` PRs: ${analysis.report.pullRequests} · sampled: ${analysis.report.sampledPullRequests}`);
|
|
113
|
+
console.log(` commits: ${analysis.report.commits} · useful: ${analysis.report.usefulCommits}`);
|
|
114
|
+
console.log(` windows: PR months=${analysis.maxPrMonths ?? "all"} · commits=${analysis.maxCommits ?? "all"} · diff lines=${analysis.maxPullRequestChangeLines ?? "all"}`);
|
|
115
|
+
for (const reason of analysis.reasons)
|
|
116
|
+
console.log(` - ${reason}`);
|
|
117
|
+
console.log(" This is a recommendation only; probe does not create or modify a skill.");
|
|
118
|
+
if (options.logTime) {
|
|
119
|
+
console.log(` total time: ${((performance.now() - operationStarted) / 1000).toFixed(2)}s`);
|
|
120
|
+
}
|
|
121
|
+
stopHeartbeat();
|
|
122
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { parseReviewArgs } from "../review_args.js";
|
|
2
|
+
import { setCliInteractive } from "../args.js";
|
|
3
|
+
import { emptyAiMetrics, recordAiCost } from "../../services/setup.js";
|
|
4
|
+
import { reviewPullRequest } from "../../pr/reviewer.js";
|
|
5
|
+
import { GhClient } from "../../github/gh.js";
|
|
6
|
+
import { log, startHeartbeat, timed, withCliLogsToStderr, } from "../../util/log.js";
|
|
7
|
+
import { runLocalReview } from "../../local/review_local.js";
|
|
8
|
+
import { runRemoteReview } from "../../remote/client.js";
|
|
9
|
+
import { printLocalReview, reviewExitCode } from "../review_output.js";
|
|
10
|
+
import { buildPrReviewJson, resolvedFromFirstReview, reviewExitCodeFromResolved, } from "../review_result.js";
|
|
11
|
+
import { parseFindings } from "../../pr/findings.js";
|
|
12
|
+
export async function runReviewFromCli(args) {
|
|
13
|
+
const parsed = await parseReviewArgs(args);
|
|
14
|
+
if (parsed.mode === "local") {
|
|
15
|
+
await runLocalReview(parsed);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (parsed.mode === "remote") {
|
|
19
|
+
await runRemoteReview(parsed);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
await runReviewPr(parsed.options, parsed);
|
|
23
|
+
}
|
|
24
|
+
export async function runReview(options) {
|
|
25
|
+
await runReviewPr(options, {
|
|
26
|
+
json: false,
|
|
27
|
+
disableCodegraph: false,
|
|
28
|
+
allowToolInstall: false,
|
|
29
|
+
remakeBeforeReview: false,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async function runReviewPr(options, cli) {
|
|
33
|
+
setCliInteractive(!cli.json);
|
|
34
|
+
await withCliLogsToStderr(async () => {
|
|
35
|
+
const operationStarted = performance.now();
|
|
36
|
+
const aiMetrics = emptyAiMetrics();
|
|
37
|
+
const stopHeartbeat = startHeartbeat("reviewing pull request");
|
|
38
|
+
if (options.auth !== "gh") {
|
|
39
|
+
throw new Error("review supports gh authentication only");
|
|
40
|
+
}
|
|
41
|
+
log("review", `reading PR #${options.prNumber} in ${options.repo} via gh`);
|
|
42
|
+
const result = await timed("review GitHub collection and AI", options.logTime, () => reviewPullRequest(new GhClient(), options, async (response) => {
|
|
43
|
+
aiMetrics.calls++;
|
|
44
|
+
aiMetrics.tokensIn += response.tokensIn;
|
|
45
|
+
aiMetrics.tokensOut += response.tokensOut;
|
|
46
|
+
if (response.cost === undefined)
|
|
47
|
+
aiMetrics.costKnown = false;
|
|
48
|
+
else
|
|
49
|
+
aiMetrics.cost += response.cost;
|
|
50
|
+
await recordAiCost(options.repo, "review_pull_request", response);
|
|
51
|
+
}));
|
|
52
|
+
const durationMs = Math.round(performance.now() - operationStarted);
|
|
53
|
+
const usage = {
|
|
54
|
+
tokensIn: aiMetrics.tokensIn,
|
|
55
|
+
tokensOut: aiMetrics.tokensOut,
|
|
56
|
+
costUsd: aiMetrics.costKnown ? aiMetrics.cost : null,
|
|
57
|
+
};
|
|
58
|
+
const codegraphState = options.useCodegraph ? "used" : "disabled";
|
|
59
|
+
const findings = resolvedFromFirstReview(parseFindings(result.text), new Map());
|
|
60
|
+
if (cli.json) {
|
|
61
|
+
console.log(buildPrReviewJson({
|
|
62
|
+
repo: options.repo,
|
|
63
|
+
prNumber: options.prNumber,
|
|
64
|
+
markdown: result.text,
|
|
65
|
+
guideBuiltAt: result.guideBuiltAt,
|
|
66
|
+
codegraphState,
|
|
67
|
+
codegraphReason: null,
|
|
68
|
+
usage,
|
|
69
|
+
durationMs,
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
printLocalReview(`co-maintainer review · ${options.repo} · PR #${options.prNumber}`, result.text);
|
|
74
|
+
if (options.logTime) {
|
|
75
|
+
log("time", `AI total · calls=${aiMetrics.calls} · input=${aiMetrics.tokensIn} tokens · output=${aiMetrics.tokensOut} tokens · cost=${aiMetrics.costKnown ? aiMetrics.cost.toFixed(4) : "unknown"}`);
|
|
76
|
+
console.error(`[time] total review · ${((performance.now() - operationStarted) / 1000).toFixed(2)}s`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
stopHeartbeat();
|
|
80
|
+
process.exit(cli.json
|
|
81
|
+
? reviewExitCodeFromResolved(findings)
|
|
82
|
+
: reviewExitCode(result.text));
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { UserConfig } from "../../config.ts";
|
|
2
|
+
import type { AuthMethods } from "../../server/auth.ts";
|
|
3
|
+
import { type Platform } from "../../util/runtime.ts";
|
|
4
|
+
/** `undefined` on Linux, otherwise one line naming the platform (a pure
|
|
5
|
+
* function so it is testable without actually being off Linux). */
|
|
6
|
+
export declare function platformWarning(os: Platform): string | undefined;
|
|
7
|
+
/** `--disable-auth=password` / `--enable-auth=github` always win over the
|
|
8
|
+
* config defaults `co-maintainer set` persisted; dies if the result would
|
|
9
|
+
* leave no sign-in method active. Pure so it is testable without a server. */
|
|
10
|
+
export declare function resolveAuthMethods(args: string[], config: Pick<UserConfig, "passwordAuthDisabled" | "githubAuthEnabled">): AuthMethods;
|
|
11
|
+
export declare function resolveWebhookUrl(args: string[], port: number, configured?: string): string;
|
|
12
|
+
/** `co-maintainer serve --port=N [--password=...] [--disable-auth=password]
|
|
13
|
+
* [--enable-auth=github] [--inject-500]`. HMAC-verified `POST /github/webhook`
|
|
14
|
+
* plus a dashboard gated by password and/or GitHub sign-in. Requires the
|
|
15
|
+
* GitHub App via `co-maintainer set` (webhook secret is optional, only the
|
|
16
|
+
* App ID and private key gate startup); GitHub sign-in additionally needs
|
|
17
|
+
* `--github-oauth-client-id`/`-client-secret`/`-allowed-user` set. */
|
|
18
|
+
export declare function runServe(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { readConfig } from "../../config.js";
|
|
2
|
+
import { appDbPath, closeAppDb, openAppDb } from "../../store/app_db.js";
|
|
3
|
+
import { createApp } from "../../server/app.js";
|
|
4
|
+
import { recoverOrphans, startWorkerLoop, stopWorkerLoop, } from "../../services/jobs.js";
|
|
5
|
+
import { registerSetupJobHandler } from "../../services/setup.js";
|
|
6
|
+
import { registerReviewJobHandler } from "../../services/review.js";
|
|
7
|
+
import { recoverReplyRequests, registerReplyJobHandler, } from "../../services/replies.js";
|
|
8
|
+
import { registerRemoteReviewHandler } from "../../services/remote_review.js";
|
|
9
|
+
import { startRemoteWatchdog } from "../../remote/server/sessions.js";
|
|
10
|
+
import { serveHttp } from "../../server/http.js";
|
|
11
|
+
import { currentPlatform, getEnv } from "../../util/runtime.js";
|
|
12
|
+
/** `undefined` on Linux, otherwise one line naming the platform (a pure
|
|
13
|
+
* function so it is testable without actually being off Linux). */
|
|
14
|
+
export function platformWarning(os) {
|
|
15
|
+
if (os === "linux")
|
|
16
|
+
return undefined;
|
|
17
|
+
return `running on ${os}. Linux (WSL included) is the recommended platform for serve — see PLAN.md Decision 5.`;
|
|
18
|
+
}
|
|
19
|
+
function die(message) {
|
|
20
|
+
throw new Error(message);
|
|
21
|
+
}
|
|
22
|
+
function generatePassword() {
|
|
23
|
+
const bytes = new Uint8Array(12);
|
|
24
|
+
crypto.getRandomValues(bytes);
|
|
25
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
26
|
+
}
|
|
27
|
+
/** `--disable-auth=password` / `--enable-auth=github` always win over the
|
|
28
|
+
* config defaults `co-maintainer set` persisted; dies if the result would
|
|
29
|
+
* leave no sign-in method active. Pure so it is testable without a server. */
|
|
30
|
+
export function resolveAuthMethods(args, config) {
|
|
31
|
+
const disableAuth = args
|
|
32
|
+
.find((arg) => arg.startsWith("--disable-auth="))
|
|
33
|
+
?.slice("--disable-auth=".length);
|
|
34
|
+
if (disableAuth && disableAuth !== "password") {
|
|
35
|
+
die("--disable-auth only supports: password");
|
|
36
|
+
}
|
|
37
|
+
const enableAuth = args
|
|
38
|
+
.find((arg) => arg.startsWith("--enable-auth="))
|
|
39
|
+
?.slice("--enable-auth=".length);
|
|
40
|
+
if (enableAuth && enableAuth !== "github") {
|
|
41
|
+
die("--enable-auth only supports: github");
|
|
42
|
+
}
|
|
43
|
+
const password = disableAuth ? false : !config.passwordAuthDisabled;
|
|
44
|
+
const github = enableAuth ? true : Boolean(config.githubAuthEnabled);
|
|
45
|
+
if (!password && !github) {
|
|
46
|
+
die("at least one sign-in method is required; drop --disable-auth=password or pass --enable-auth=github");
|
|
47
|
+
}
|
|
48
|
+
return { password, github };
|
|
49
|
+
}
|
|
50
|
+
export function resolveWebhookUrl(args, port, configured) {
|
|
51
|
+
const flag = args.find((arg) => arg.startsWith("--webhook-url="));
|
|
52
|
+
const raw = flag?.slice("--webhook-url=".length) ??
|
|
53
|
+
getEnv("CM_WEBHOOK_URL") ??
|
|
54
|
+
configured ??
|
|
55
|
+
`http://localhost:${port}/github/webhook`;
|
|
56
|
+
let url;
|
|
57
|
+
try {
|
|
58
|
+
url = new URL(raw);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
die("--webhook-url must be an absolute http(s) URL, e.g. " +
|
|
62
|
+
"--webhook-url=https://example.com/github/webhook");
|
|
63
|
+
}
|
|
64
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
65
|
+
die("--webhook-url must use http or https");
|
|
66
|
+
}
|
|
67
|
+
return url.toString();
|
|
68
|
+
}
|
|
69
|
+
/** `co-maintainer serve --port=N [--password=...] [--disable-auth=password]
|
|
70
|
+
* [--enable-auth=github] [--inject-500]`. HMAC-verified `POST /github/webhook`
|
|
71
|
+
* plus a dashboard gated by password and/or GitHub sign-in. Requires the
|
|
72
|
+
* GitHub App via `co-maintainer set` (webhook secret is optional, only the
|
|
73
|
+
* App ID and private key gate startup); GitHub sign-in additionally needs
|
|
74
|
+
* `--github-oauth-client-id`/`-client-secret`/`-allowed-user` set. */
|
|
75
|
+
export async function runServe(args) {
|
|
76
|
+
const portArg = args
|
|
77
|
+
.find((arg) => arg.startsWith("--port="))
|
|
78
|
+
?.slice("--port=".length);
|
|
79
|
+
if (!portArg)
|
|
80
|
+
die("--port is required, e.g. --port=5000");
|
|
81
|
+
const port = Number(portArg);
|
|
82
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
83
|
+
die("--port must be an integer between 1 and 65535");
|
|
84
|
+
}
|
|
85
|
+
const config = readConfig();
|
|
86
|
+
const webhookUrl = resolveWebhookUrl(args, port, config.webhookUrl);
|
|
87
|
+
const missing = [
|
|
88
|
+
!config.githubAppId && "--github-app-id=...",
|
|
89
|
+
!config.githubAppPrivateKey &&
|
|
90
|
+
"--github-app-private-key=... (or --github-app-private-key-file=path)",
|
|
91
|
+
].filter(Boolean);
|
|
92
|
+
if (missing.length > 0) {
|
|
93
|
+
die(`serve requires the GitHub App to be configured first; run:\n` +
|
|
94
|
+
` co-maintainer set ${missing.join(" ")}`);
|
|
95
|
+
}
|
|
96
|
+
const auth = resolveAuthMethods(args, config);
|
|
97
|
+
let githubOAuth;
|
|
98
|
+
if (auth.github) {
|
|
99
|
+
if (!config.githubOAuthClientId ||
|
|
100
|
+
!config.githubOAuthClientSecret ||
|
|
101
|
+
!config.githubOAuthAllowedUser) {
|
|
102
|
+
die("GitHub sign-in requires OAuth credentials; run:\n" +
|
|
103
|
+
" co-maintainer set --github-oauth-client-id=... --github-oauth-client-secret=... --github-oauth-allowed-user=...");
|
|
104
|
+
}
|
|
105
|
+
githubOAuth = {
|
|
106
|
+
clientId: config.githubOAuthClientId,
|
|
107
|
+
clientSecret: config.githubOAuthClientSecret,
|
|
108
|
+
allowedUser: config.githubOAuthAllowedUser,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const warning = platformWarning(currentPlatform());
|
|
112
|
+
if (warning)
|
|
113
|
+
console.log(`[serve] warning: ${warning}`);
|
|
114
|
+
await openAppDb();
|
|
115
|
+
console.log(`[serve] app.db ready at ${appDbPath()}`);
|
|
116
|
+
registerSetupJobHandler();
|
|
117
|
+
registerReviewJobHandler();
|
|
118
|
+
registerRemoteReviewHandler();
|
|
119
|
+
startRemoteWatchdog();
|
|
120
|
+
registerReplyJobHandler();
|
|
121
|
+
const recovered = await recoverOrphans();
|
|
122
|
+
if (recovered > 0) {
|
|
123
|
+
console.log(`[serve] recovered ${recovered} orphaned job(s) from a previous run`);
|
|
124
|
+
}
|
|
125
|
+
const recoveredReplies = recoverReplyRequests();
|
|
126
|
+
if (recoveredReplies > 0) {
|
|
127
|
+
console.log(`[serve] requeued ${recoveredReplies} unfinished conversation repl${recoveredReplies === 1 ? "y" : "ies"}`);
|
|
128
|
+
}
|
|
129
|
+
startWorkerLoop();
|
|
130
|
+
const password = auth.password
|
|
131
|
+
? (args
|
|
132
|
+
.find((arg) => arg.startsWith("--password="))
|
|
133
|
+
?.slice("--password=".length) ?? generatePassword())
|
|
134
|
+
: "";
|
|
135
|
+
if (auth.password)
|
|
136
|
+
console.log(`[serve] dashboard password: ${password}`);
|
|
137
|
+
if (auth.github)
|
|
138
|
+
console.log(`[serve] GitHub sign-in enabled for ${githubOAuth.allowedUser}`);
|
|
139
|
+
const inject500 = args.includes("--inject-500") || getEnv("CM_INJECT_500") === "1";
|
|
140
|
+
if (inject500) {
|
|
141
|
+
console.log("[serve] --inject-500 is on. Mutating /api requests return 500.");
|
|
142
|
+
}
|
|
143
|
+
const app = createApp({
|
|
144
|
+
password,
|
|
145
|
+
githubApp: config.githubAppId && config.githubAppPrivateKey
|
|
146
|
+
? {
|
|
147
|
+
appId: config.githubAppId,
|
|
148
|
+
privateKeyPem: config.githubAppPrivateKey,
|
|
149
|
+
}
|
|
150
|
+
: undefined,
|
|
151
|
+
webhookSecret: config.githubWebhookSecret,
|
|
152
|
+
webhookUrl,
|
|
153
|
+
inject500,
|
|
154
|
+
auth,
|
|
155
|
+
githubOAuth,
|
|
156
|
+
});
|
|
157
|
+
const server = serveHttp((req, remoteAddr) => app.fetch(req, remoteAddr), port);
|
|
158
|
+
console.log(`[serve] listening on http://localhost:${port}`);
|
|
159
|
+
console.log(`[serve] dashboard at http://localhost:${port}/`);
|
|
160
|
+
console.log(`[serve] webhook URL: ${webhookUrl}`);
|
|
161
|
+
let shuttingDown = false;
|
|
162
|
+
const shutdown = async (signal) => {
|
|
163
|
+
if (shuttingDown)
|
|
164
|
+
return;
|
|
165
|
+
shuttingDown = true;
|
|
166
|
+
console.log(`[serve] received ${signal}, stopping new requests and closing app.db`);
|
|
167
|
+
await stopWorkerLoop();
|
|
168
|
+
await server.shutdown();
|
|
169
|
+
await closeAppDb();
|
|
170
|
+
console.log("[serve] shut down cleanly");
|
|
171
|
+
process.exit(0);
|
|
172
|
+
};
|
|
173
|
+
// Ctrl+C in an interactive terminal fires this reliably everywhere,
|
|
174
|
+
// including Windows. A SIGTERM sent by another process (a process
|
|
175
|
+
// manager, `kill`, `taskkill`) is reliable on Linux but not on Windows,
|
|
176
|
+
// where it forcibly terminates the process before any handler can run —
|
|
177
|
+
// see the P3 Live note in PLAN.md. Registering it anyway costs nothing
|
|
178
|
+
// and is correct wherever it does work.
|
|
179
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
180
|
+
process.on(signal, () => void shutdown(signal));
|
|
181
|
+
}
|
|
182
|
+
await server.finished;
|
|
183
|
+
}
|