co-maintainer 0.4.13 → 0.5.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 -21
- package/README.md +57 -38
- package/dist/main.js +2 -3
- package/dist/package.json +21 -15
- package/dist/src/ai/batch.d.ts +24 -1
- package/dist/src/ai/batch.js +75 -23
- package/dist/src/ai/estimate.d.ts +40 -0
- package/dist/src/ai/estimate.js +113 -0
- package/dist/src/ai/fake.d.ts +1 -1
- package/dist/src/ai/fake.js +1 -1
- package/dist/src/ai/hetzner.js +1 -1
- package/dist/src/ai/openrouter.d.ts +13 -0
- package/dist/src/ai/openrouter.js +66 -12
- package/dist/src/ai/pricing.d.ts +14 -0
- package/dist/src/ai/pricing.js +77 -0
- package/dist/src/ai/provider.js +8 -0
- package/dist/src/ai/verify.d.ts +25 -0
- package/dist/src/ai/verify.js +78 -0
- package/dist/src/cli/args.js +25 -29
- package/dist/src/cli/commands/config.d.ts +44 -0
- package/dist/src/cli/commands/config.js +220 -0
- package/dist/src/cli/commands/probe.js +70 -103
- package/dist/src/cli/commands/registry.d.ts +57 -0
- package/dist/src/cli/commands/registry.js +713 -0
- package/dist/src/cli/commands/review.js +66 -43
- package/dist/src/cli/commands/serve.d.ts +4 -2
- package/dist/src/cli/commands/serve.js +14 -10
- package/dist/src/cli/commands/set.d.ts +2 -1
- package/dist/src/cli/commands/set.js +63 -17
- package/dist/src/cli/commands/view.d.ts +1 -0
- package/dist/src/cli/commands/view.js +180 -0
- package/dist/src/cli/error.d.ts +39 -0
- package/dist/src/cli/error.js +64 -0
- package/dist/src/cli/main.d.ts +7 -0
- package/dist/src/cli/main.js +82 -1
- package/dist/src/cli/prompt.js +21 -7
- package/dist/src/cli/review_args.d.ts +4 -0
- package/dist/src/cli/review_args.js +22 -9
- package/dist/src/cli/review_output.d.ts +2 -2
- package/dist/src/cli/review_output.js +7 -6
- package/dist/src/cli/review_result.d.ts +59 -8
- package/dist/src/cli/review_result.js +145 -103
- package/dist/src/config.d.ts +15 -0
- package/dist/src/config.js +31 -0
- package/dist/src/github/app.d.ts +13 -0
- package/dist/src/github/app.js +23 -0
- package/dist/src/github/app_manifest.d.ts +50 -0
- package/dist/src/github/app_manifest.js +138 -0
- package/dist/src/github/client.js +7 -1
- package/dist/src/github/collect.js +1 -1
- package/dist/src/github/gh.js +75 -18
- package/dist/src/knowledge/facts.js +8 -3
- package/dist/src/knowledge/guide.d.ts +8 -0
- package/dist/src/knowledge/guide.js +32 -8
- package/dist/src/knowledge/probe.js +13 -3
- package/dist/src/knowledge/sections.d.ts +9 -0
- package/dist/src/knowledge/sections.js +16 -0
- package/dist/src/knowledge/skill.d.ts +5 -0
- package/dist/src/knowledge/skill.js +68 -21
- package/dist/src/knowledge/synthesis.d.ts +17 -1
- package/dist/src/knowledge/synthesis.js +70 -15
- package/dist/src/knowledge/types.d.ts +5 -0
- package/dist/src/local/codegraph_prepare.d.ts +3 -2
- package/dist/src/local/codegraph_prepare.js +4 -2
- package/dist/src/local/git_ops.d.ts +5 -4
- package/dist/src/local/git_ops.js +8 -9
- package/dist/src/local/review_local.js +36 -14
- package/dist/src/pr/checkout.js +14 -3
- package/dist/src/pr/codegraph_tools.js +3 -3
- package/dist/src/pr/diff_summary.js +3 -3
- package/dist/src/pr/findings.js +16 -5
- package/dist/src/pr/findings_json.d.ts +26 -0
- package/dist/src/pr/findings_json.js +187 -0
- package/dist/src/pr/review_copy.d.ts +20 -0
- package/dist/src/pr/review_copy.js +40 -0
- package/dist/src/pr/reviewer.d.ts +2 -1
- package/dist/src/pr/reviewer.js +84 -91
- package/dist/src/remote/client.js +42 -42
- package/dist/src/remote/http.d.ts +5 -0
- package/dist/src/remote/http.js +49 -0
- package/dist/src/remote/server/guides.d.ts +22 -0
- package/dist/src/remote/server/guides.js +81 -0
- package/dist/src/remote/server/routes.js +24 -0
- package/dist/src/review/blocking.d.ts +35 -0
- package/dist/src/review/blocking.js +45 -0
- package/dist/src/review/carry_over.d.ts +12 -1
- package/dist/src/review/carry_over.js +35 -9
- package/dist/src/review/engine.d.ts +1 -0
- package/dist/src/review/guides.js +28 -1
- package/dist/src/server/api/installations.js +1 -1
- package/dist/src/server/api/repos.js +86 -3
- package/dist/src/server/api/settings.js +3 -3
- package/dist/src/server/app.d.ts +3 -0
- package/dist/src/server/app.js +12 -5
- package/dist/src/server/pages/activity.d.ts +1 -0
- package/dist/src/server/pages/activity.js +2 -2
- package/dist/src/server/pages/add_repo.js +73 -5
- package/dist/src/server/pages/client.d.ts +1 -1
- package/dist/src/server/pages/client.js +1 -1
- package/dist/src/server/pages/home.d.ts +6 -1
- package/dist/src/server/pages/home.js +25 -2
- package/dist/src/server/pages/knowledge.js +1 -1
- package/dist/src/server/pages/layout.js +2 -2
- package/dist/src/server/pages/repo.d.ts +1 -1
- package/dist/src/server/pages/repo.js +21 -2
- package/dist/src/server/pages/repo_prs.d.ts +5 -1
- package/dist/src/server/pages/repo_prs.js +36 -1
- package/dist/src/server/pages/repo_remote.js +2 -2
- package/dist/src/server/pages/repo_settings.js +2 -2
- package/dist/src/server/pages/router.d.ts +6 -0
- package/dist/src/server/pages/router.js +129 -9
- package/dist/src/server/pages/settings.d.ts +1 -1
- package/dist/src/server/pages/settings.js +87 -9
- package/dist/src/server/pages/styles.d.ts +1 -1
- package/dist/src/server/pages/styles.js +1 -1
- package/dist/src/services/probe.d.ts +28 -0
- package/dist/src/services/probe.js +133 -0
- package/dist/src/services/remake_cron.d.ts +1 -1
- package/dist/src/services/remake_cron.js +3 -3
- package/dist/src/services/remote_review.js +14 -4
- package/dist/src/services/review.d.ts +11 -0
- package/dist/src/services/review.js +39 -11
- package/dist/src/services/setup.js +37 -19
- package/dist/src/services/setup_checklist.d.ts +10 -0
- package/dist/src/services/setup_checklist.js +87 -0
- package/dist/src/store/app_db.js +5 -2
- package/dist/src/store/cache_db.d.ts +4 -0
- package/dist/src/store/cache_db.js +27 -4
- package/dist/src/store/deliveries.d.ts +7 -0
- package/dist/src/store/deliveries.js +15 -0
- package/dist/src/tools/codegraph.d.ts +30 -0
- package/dist/src/tools/codegraph.js +49 -12
- package/dist/src/types.d.ts +10 -0
- package/dist/src/util/log.d.ts +3 -0
- package/dist/src/util/log.js +12 -0
- package/dist/src/util/run_summary.d.ts +33 -0
- package/dist/src/util/run_summary.js +47 -0
- package/dist/src/util/webhook_reachability.d.ts +11 -0
- package/dist/src/util/webhook_reachability.js +64 -0
- package/package.json +21 -15
|
@@ -1,16 +1,90 @@
|
|
|
1
1
|
import { errorResponse } from "../errors.js";
|
|
2
|
-
import { findInstallationForRepo } from "../../github/app.js";
|
|
3
|
-
import { activateRepo, deactivateRepo, listActiveRepos, updateRepoSettings, } from "../../store/repos.js";
|
|
4
|
-
import { writeRepoConfig } from "../../config.js";
|
|
2
|
+
import { AppClient, findInstallationForRepo } from "../../github/app.js";
|
|
3
|
+
import { activateRepo, deactivateRepo, getRepo, listActiveRepos, updateRepoSettings, } from "../../store/repos.js";
|
|
4
|
+
import { readConfig, writeRepoConfig } from "../../config.js";
|
|
5
5
|
import { parseRemakeCron } from "../../services/remake_cron.js";
|
|
6
6
|
import { enqueueSetup } from "../../services/setup.js";
|
|
7
7
|
import { enqueueManualReview } from "../../services/review.js";
|
|
8
|
+
import { probePlan, recommendationPatch } from "../../services/probe.js";
|
|
9
|
+
import { estimateInit, readJobHistory } from "../../ai/estimate.js";
|
|
10
|
+
import { loadPrices } from "../../ai/pricing.js";
|
|
8
11
|
import { prDetail, repoKnowledge, RepoNotFound, repoOverview, repoPulls, requireActiveRepo, } from "../../services/dashboard.js";
|
|
9
12
|
const REPO = /^\/api\/repos\/([^/]+)\/([^/]+)(?:\/(remake|knowledge|pulls)(?:\/(\d+)(?:\/(review))?)?)?$/;
|
|
10
13
|
export async function handleReposRoute(request, url, githubApp) {
|
|
11
14
|
if (url.pathname === "/api/repos" && request.method === "GET") {
|
|
12
15
|
return Response.json({ items: listActiveRepos() });
|
|
13
16
|
}
|
|
17
|
+
if (url.pathname === "/api/repos/preview" && request.method === "POST") {
|
|
18
|
+
if (!githubApp) {
|
|
19
|
+
return errorResponse(422, "no_github_app", "Configure the GitHub App in Settings to preview a repository.");
|
|
20
|
+
}
|
|
21
|
+
let body;
|
|
22
|
+
try {
|
|
23
|
+
body = await request.json();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return errorResponse(400, "bad_request", "expected a JSON body");
|
|
27
|
+
}
|
|
28
|
+
const repo = String(body.repo ?? "");
|
|
29
|
+
if (!/^[^/]+\/[^/]+$/.test(repo)) {
|
|
30
|
+
return errorResponse(400, "bad_request", "repo must look like owner/repo");
|
|
31
|
+
}
|
|
32
|
+
let installationId;
|
|
33
|
+
try {
|
|
34
|
+
installationId = await findInstallationForRepo(githubApp.appId, githubApp.privateKeyPem, repo);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return errorResponse(422, "app_lookup_failed", "Could not verify the GitHub App's repository access.");
|
|
38
|
+
}
|
|
39
|
+
if (installationId === undefined) {
|
|
40
|
+
return errorResponse(422, "app_access_denied", `The GitHub App cannot access ${repo}.`);
|
|
41
|
+
}
|
|
42
|
+
if (getRepo(repo)?.active) {
|
|
43
|
+
return errorResponse(409, "already_added", `${repo} is already added.`);
|
|
44
|
+
}
|
|
45
|
+
const client = new AppClient(githubApp.appId, githubApp.privateKeyPem, installationId);
|
|
46
|
+
try {
|
|
47
|
+
const plan = await probePlan(client, repo, {
|
|
48
|
+
ghConcurrent: 1,
|
|
49
|
+
log: () => { },
|
|
50
|
+
});
|
|
51
|
+
const config = readConfig();
|
|
52
|
+
const needsPrices = config.ai === "openrouter" &&
|
|
53
|
+
Boolean(config.lowModel && config.highModel);
|
|
54
|
+
const [prices, history] = await Promise.all([
|
|
55
|
+
needsPrices
|
|
56
|
+
? loadPrices()
|
|
57
|
+
: Promise.resolve({
|
|
58
|
+
prices: new Map(),
|
|
59
|
+
source: "unavailable",
|
|
60
|
+
}),
|
|
61
|
+
readJobHistory(repo),
|
|
62
|
+
]);
|
|
63
|
+
const estimate = estimateInit({
|
|
64
|
+
pullRequests: plan.analysis.includePullRequests
|
|
65
|
+
? Number(plan.analysis.report.pullRequests ?? 0)
|
|
66
|
+
: 0,
|
|
67
|
+
includeCodebase: plan.analysis.includePullRequests,
|
|
68
|
+
lowModel: config.lowModel,
|
|
69
|
+
highModel: config.highModel,
|
|
70
|
+
prices: prices.prices,
|
|
71
|
+
history,
|
|
72
|
+
});
|
|
73
|
+
return Response.json({
|
|
74
|
+
repo,
|
|
75
|
+
command: plan.command,
|
|
76
|
+
recommendation: plan.recommendation,
|
|
77
|
+
patch: recommendationPatch(plan.recommendation),
|
|
78
|
+
reasons: plan.analysis.reasons,
|
|
79
|
+
report: plan.report,
|
|
80
|
+
estimate,
|
|
81
|
+
estimateBasis: estimate.basis,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
return errorResponse(422, "probe_failed", error instanceof Error ? error.message : String(error));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
14
88
|
if (url.pathname === "/api/repos" && request.method === "POST") {
|
|
15
89
|
let body;
|
|
16
90
|
try {
|
|
@@ -35,6 +109,15 @@ export async function handleReposRoute(request, url, githubApp) {
|
|
|
35
109
|
return errorResponse(422, "app_access_denied", `The GitHub App cannot access ${repo}.`);
|
|
36
110
|
}
|
|
37
111
|
}
|
|
112
|
+
// `patch` is the confirmed preview. It is optional so a caller that
|
|
113
|
+
// never opened the preview keeps the old behavior; when present it is
|
|
114
|
+
// written before `activateRepo` so the enqueued init reads it.
|
|
115
|
+
const patch = body.patch && typeof body.patch === "object"
|
|
116
|
+
? body.patch
|
|
117
|
+
: undefined;
|
|
118
|
+
if (patch && Object.keys(patch).length > 0) {
|
|
119
|
+
await writeRepoConfig(repo, patch);
|
|
120
|
+
}
|
|
38
121
|
activateRepo(repo, installationId);
|
|
39
122
|
try {
|
|
40
123
|
const { id } = enqueueSetup(repo, "init");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { errorResponse } from "../errors.js";
|
|
2
|
-
import { readConfig, writeUserConfig } from "../../config.js";
|
|
2
|
+
import { readConfig, resolveAppPrivateKey, writeUserConfig, } from "../../config.js";
|
|
3
3
|
import { testAppAccess, testGithubAccess } from "../../services/credentials.js";
|
|
4
4
|
import { checkPassword, readSessionToken, revokeOtherSessions, } from "../auth.js";
|
|
5
5
|
import { passwordProblem } from "../../util/password.js";
|
|
@@ -201,7 +201,7 @@ export async function handleSettingsRoute(request, url, webhookUrl, passwords, i
|
|
|
201
201
|
if (touchingApp) {
|
|
202
202
|
const app = await testAppAccess({
|
|
203
203
|
appId: merged.githubAppId ?? "",
|
|
204
|
-
privateKeyPem: merged
|
|
204
|
+
privateKeyPem: resolveAppPrivateKey(merged) ?? "",
|
|
205
205
|
});
|
|
206
206
|
if (!app.ok) {
|
|
207
207
|
return errorResponse(422, "app_access", app.message);
|
|
@@ -218,7 +218,7 @@ export async function handleSettingsRoute(request, url, webhookUrl, passwords, i
|
|
|
218
218
|
});
|
|
219
219
|
const app = await testAppAccess({
|
|
220
220
|
appId: config.githubAppId ?? "",
|
|
221
|
-
privateKeyPem: config
|
|
221
|
+
privateKeyPem: resolveAppPrivateKey(config) ?? "",
|
|
222
222
|
});
|
|
223
223
|
return Response.json({
|
|
224
224
|
ai: { ok: Boolean(config.ai && config.ai !== "none" && config.token) },
|
package/dist/src/server/app.d.ts
CHANGED
|
@@ -9,6 +9,9 @@ export type AppDeps = {
|
|
|
9
9
|
/** Behind a reverse proxy this process trusts, the client address and the
|
|
10
10
|
* scheme come from `x-forwarded-for` and `x-forwarded-proto`. */
|
|
11
11
|
trustProxy?: boolean;
|
|
12
|
+
/** `CM_LOGIN_HINT`, shown on the sign-in page in place of the default
|
|
13
|
+
* "printed when serve started" line. */
|
|
14
|
+
loginHint?: string;
|
|
12
15
|
inject500?: boolean;
|
|
13
16
|
/** Defaults to password-only when omitted, matching every caller that
|
|
14
17
|
* pre-dates GitHub sign-in. */
|
package/dist/src/server/app.js
CHANGED
|
@@ -5,7 +5,7 @@ import { errorResponse } from "./errors.js";
|
|
|
5
5
|
import { hasCsrfHeader, login, logout, readSessionToken, sessionCookieHeader, verifySession, } from "./auth.js";
|
|
6
6
|
import { memoryPasswordStore } from "./auth.js";
|
|
7
7
|
import { clientAddress, forwardedHttps } from "./proxy_headers.js";
|
|
8
|
-
import { readConfig } from "../config.js";
|
|
8
|
+
import { readConfig, resolveAppPrivateKey } from "../config.js";
|
|
9
9
|
import { listJobs } from "../store/jobs.js";
|
|
10
10
|
import { handleJobsRoute } from "./api/jobs.js";
|
|
11
11
|
import { handleReposRoute } from "./api/repos.js";
|
|
@@ -21,7 +21,7 @@ function setupStatus() {
|
|
|
21
21
|
const config = readConfig();
|
|
22
22
|
const ai = Boolean(config.ai && config.ai !== "none" && config.token);
|
|
23
23
|
const github = Boolean(config.auth === "gh" || (config.auth === "pat" && config.githubPat));
|
|
24
|
-
const app = Boolean(config.githubAppId && config
|
|
24
|
+
const app = Boolean(config.githubAppId && resolveAppPrivateKey(config));
|
|
25
25
|
const missing = [
|
|
26
26
|
!ai && "ai",
|
|
27
27
|
!github && "github",
|
|
@@ -84,11 +84,12 @@ function health() {
|
|
|
84
84
|
* without restarting `serve`. */
|
|
85
85
|
function liveGithubConfig() {
|
|
86
86
|
const config = readConfig();
|
|
87
|
+
const privateKeyPem = resolveAppPrivateKey(config);
|
|
87
88
|
return {
|
|
88
|
-
githubApp: config.githubAppId &&
|
|
89
|
+
githubApp: config.githubAppId && privateKeyPem
|
|
89
90
|
? {
|
|
90
91
|
appId: config.githubAppId,
|
|
91
|
-
privateKeyPem
|
|
92
|
+
privateKeyPem,
|
|
92
93
|
}
|
|
93
94
|
: undefined,
|
|
94
95
|
webhookSecret: config.githubWebhookSecret,
|
|
@@ -112,7 +113,13 @@ export function createApp(deps) {
|
|
|
112
113
|
if (url.pathname === "/github/webhook" && request.method === "POST") {
|
|
113
114
|
return await handleWebhookRequest(request, webhookSecret);
|
|
114
115
|
}
|
|
115
|
-
const page = await handlePageRequest(request, {
|
|
116
|
+
const page = await handlePageRequest(request, {
|
|
117
|
+
...deps,
|
|
118
|
+
githubApp,
|
|
119
|
+
passwordStore: passwords,
|
|
120
|
+
secureCookie,
|
|
121
|
+
trustProxy: deps.trustProxy,
|
|
122
|
+
}, remoteAddr);
|
|
116
123
|
if (page)
|
|
117
124
|
return page;
|
|
118
125
|
if (url.pathname.startsWith("/api/remote/")) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { activityFeed, runningJobs, skippedDeliveries } from "../../services/dashboard.ts";
|
|
2
2
|
import type { JobLogRow, JobRow } from "../../store/rows.ts";
|
|
3
|
+
export declare function jobLabel(job: JobRow): string;
|
|
3
4
|
export declare function renderActivity(username: string, running: ReturnType<typeof runningJobs>, feed: ReturnType<typeof activityFeed>, skipped: ReturnType<typeof skippedDeliveries>): Response;
|
|
4
5
|
export declare function renderJob(username: string, job: JobRow, logs: JobLogRow[]): Response;
|
|
@@ -20,11 +20,11 @@ function skipReason(reason) {
|
|
|
20
20
|
return reason ?? "Skipped";
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
-
function jobLabel(job) {
|
|
23
|
+
export function jobLabel(job) {
|
|
24
24
|
if (job.type === "init")
|
|
25
25
|
return "Set up repository";
|
|
26
26
|
if (job.type === "remake")
|
|
27
|
-
return "
|
|
27
|
+
return "Synced knowledge";
|
|
28
28
|
if (job.type === "review") {
|
|
29
29
|
return job.pr_number ? `Review #${job.pr_number}` : "Review";
|
|
30
30
|
}
|
|
@@ -26,7 +26,7 @@ export function renderAddRepo(username, picker) {
|
|
|
26
26
|
<div class="crumbs"><a href="/">Repositories</a> / Add</div>
|
|
27
27
|
<div class="pagehead">
|
|
28
28
|
<div><h1>Add repository</h1>
|
|
29
|
-
<p class="lead">Pick a repository this App can access.</p></div>
|
|
29
|
+
<p class="lead">Pick a repository this App can access. Nothing is read or written until you confirm the plan.</p></div>
|
|
30
30
|
</div>
|
|
31
31
|
${notice}
|
|
32
32
|
<div class="card" data-async>
|
|
@@ -37,18 +37,86 @@ export function renderAddRepo(username, picker) {
|
|
|
37
37
|
<label>Repository</label>
|
|
38
38
|
${select}
|
|
39
39
|
</div>
|
|
40
|
-
<button class="primary" id="
|
|
40
|
+
<button class="primary" id="preview"${canAdd ? "" : " disabled"}>Preview</button>
|
|
41
41
|
</div>
|
|
42
|
+
<div id="plan" hidden></div>
|
|
42
43
|
</div></div>
|
|
43
44
|
</div>
|
|
44
45
|
<script>
|
|
45
|
-
|
|
46
|
+
var plan = null;
|
|
47
|
+
function showPlan(data) {
|
|
48
|
+
plan = data;
|
|
49
|
+
var box = document.getElementById("plan");
|
|
50
|
+
box.hidden = false;
|
|
51
|
+
box.replaceChildren();
|
|
52
|
+
var heading = document.createElement("h2");
|
|
53
|
+
heading.textContent = "Plan for " + data.repo;
|
|
54
|
+
box.appendChild(heading);
|
|
55
|
+
var cmd = document.createElement("p");
|
|
56
|
+
cmd.className = "mono";
|
|
57
|
+
cmd.style.wordBreak = "break-all";
|
|
58
|
+
cmd.textContent = data.command;
|
|
59
|
+
box.appendChild(cmd);
|
|
60
|
+
var est = data.estimate;
|
|
61
|
+
var facts = document.createElement("p");
|
|
62
|
+
facts.className = "muted";
|
|
63
|
+
facts.textContent =
|
|
64
|
+
"AI jobs: " + est.extract + " extract + " + est.synth + " synth" +
|
|
65
|
+
". Tokens: " + est.tokensIn[0].toLocaleString("en-US") + "-" +
|
|
66
|
+
est.tokensIn[1].toLocaleString("en-US") + " in, " +
|
|
67
|
+
est.tokensOut[0].toLocaleString("en-US") + "-" +
|
|
68
|
+
est.tokensOut[1].toLocaleString("en-US") + " out" +
|
|
69
|
+
". Time: " + Math.round(est.seconds[0]) + "-" + Math.round(est.seconds[1]) + "s" +
|
|
70
|
+
(est.usd
|
|
71
|
+
? ". Cost: $" + est.usd[0].toFixed(4) + "-$" + est.usd[1].toFixed(4)
|
|
72
|
+
: ". Cost: unknown") +
|
|
73
|
+
". Basis: " + (est.estimateBasis === "history"
|
|
74
|
+
? "this repository's recorded jobs"
|
|
75
|
+
: "the cm-dx-lab calibration") + ", an estimate, not a bill.";
|
|
76
|
+
box.appendChild(facts);
|
|
77
|
+
if (data.reasons && data.reasons.length) {
|
|
78
|
+
var ul = document.createElement("ul");
|
|
79
|
+
ul.className = "muted";
|
|
80
|
+
data.reasons.forEach(function(reason) {
|
|
81
|
+
var li = document.createElement("li");
|
|
82
|
+
li.textContent = reason;
|
|
83
|
+
ul.appendChild(li);
|
|
84
|
+
});
|
|
85
|
+
box.appendChild(ul);
|
|
86
|
+
}
|
|
87
|
+
var row = document.createElement("div");
|
|
88
|
+
row.style.display = "flex";
|
|
89
|
+
row.style.gap = "8px";
|
|
90
|
+
row.style.marginTop = "14px";
|
|
91
|
+
var confirm = document.createElement("button");
|
|
92
|
+
confirm.className = "primary";
|
|
93
|
+
confirm.id = "confirm";
|
|
94
|
+
confirm.textContent = "Add and start init";
|
|
95
|
+
var cancel = document.createElement("button");
|
|
96
|
+
cancel.className = "btn";
|
|
97
|
+
cancel.type = "button";
|
|
98
|
+
cancel.textContent = "Cancel";
|
|
99
|
+
cancel.addEventListener("click", function() {
|
|
100
|
+
plan = null;
|
|
101
|
+
box.hidden = true;
|
|
102
|
+
box.replaceChildren();
|
|
103
|
+
});
|
|
104
|
+
row.appendChild(confirm);
|
|
105
|
+
row.appendChild(cancel);
|
|
106
|
+
box.appendChild(row);
|
|
107
|
+
confirm.addEventListener("click", function() {
|
|
108
|
+
var repo = document.getElementById("repo").value.trim();
|
|
109
|
+
if (!repo || !plan) return;
|
|
110
|
+
postAndGo("/api/repos", { repo: repo, patch: plan.patch }, "/activity", confirm);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
document.getElementById("preview").addEventListener("click", function() {
|
|
46
114
|
var btn = this;
|
|
47
115
|
var repo = document.getElementById("repo").value.trim();
|
|
48
116
|
if (!repo) return;
|
|
49
117
|
run(btn, btn.closest("[data-async]"), async function() {
|
|
50
|
-
await api("POST", "/api/repos", { repo: repo });
|
|
51
|
-
|
|
118
|
+
var data = await api("POST", "/api/repos/preview", { repo: repo });
|
|
119
|
+
showPlan(data);
|
|
52
120
|
});
|
|
53
121
|
});
|
|
54
122
|
</script>`,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Generated from src/server/pages/client.js by scripts/embed_assets.mjs — do not edit. */
|
|
2
|
-
export declare const client = "function toast(message) {\n const host = document.getElementById(\"toasts\");\n if (!host) return;\n const el = document.createElement(\"div\");\n el.className = \"toast\";\n el.setAttribute(\"role\", \"status\");\n el.textContent = message;\n host.appendChild(el);\n setTimeout(function () {\n el.remove();\n }, 5000);\n}\n\nfunction fail(card, message, retry) {\n toast(message);\n if (!card) return;\n let box = card.querySelector(\"[data-fail]\");\n if (!box) {\n box = document.createElement(\"div\");\n box.setAttribute(\"data-fail\", \"1\");\n box.className = \"fail\";\n card.appendChild(box);\n }\n box.replaceChildren();\n const p = document.createElement(\"p\");\n p.textContent = message;\n const btn = document.createElement(\"button\");\n btn.className = \"btn sm\";\n btn.type = \"button\";\n btn.textContent = \"Retry\";\n btn.addEventListener(\"click\", retry);\n box.appendChild(p);\n box.appendChild(btn);\n}\n\nfunction clearFail(card) {\n const box = card && card.querySelector(\"[data-fail]\");\n if (box) box.remove();\n}\n\nfunction skeleton(card, on) {\n if (!card) return;\n card.classList.toggle(\"busy\", on);\n}\n\nasync function api(method, url, body) {\n const res = await fetch(url, {\n method: method,\n headers: {\n \"content-type\": \"application/json\",\n \"x-requested-with\": \"co-maintainer\",\n },\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n if (!res.ok) {\n let message = \"Request failed\";\n try {\n const data = await res.json();\n if (data.error && data.error.message) message = data.error.message;\n } catch {\n // body was not JSON; keep the generic message\n }\n throw new Error(message);\n }\n if (res.status === 204) return null;\n return await res.json();\n}\n\nasync function run(btn, card, fn) {\n if (btn) btn.disabled = true;\n skeleton(card, true);\n clearFail(card);\n try {\n await fn();\n } catch (err) {\n fail(card, err.message, function () {\n run(btn, card, fn);\n });\n } finally {\n if (btn) btn.disabled = false;\n skeleton(card, false);\n }\n}\n\nasync function postAndGo(url, body, next, btn) {\n const card = btn && btn.closest(\"[data-async]\");\n await run(btn, card, async function () {\n await api(\"POST\", url, body);\n location.href = next;\n });\n}\n\n// Called from the inline scripts on the repo list and repo pages, which deno lint\n// cannot see, so it looks unused from inside this file.\n// deno-lint-ignore no-unused-vars\nfunction bindToggle(btn, url, field) {\n btn.addEventListener(\"click\", async function () {\n if (btn.disabled) return;\n const on = !btn.classList.contains(\"on\");\n btn.classList.toggle(\"on\", on);\n btn.setAttribute(\"data-on\", on ? \"1\" : \"0\");\n btn.disabled = true;\n const card = btn.closest(\"[data-async]\");\n try {\n const payload = {};\n payload[field] = on;\n await api(\"PATCH\", url, payload);\n } catch (err) {\n btn.classList.toggle(\"on\", !on);\n btn.setAttribute(\"data-on\", on ? \"0\" : \"1\");\n fail(card, err.message, function () {\n btn.click();\n });\n } finally {\n btn.disabled = false;\n }\n });\n}\n\
|
|
2
|
+
export declare const client = "function toast(message) {\n const host = document.getElementById(\"toasts\");\n if (!host) return;\n const el = document.createElement(\"div\");\n el.className = \"toast\";\n el.setAttribute(\"role\", \"status\");\n el.textContent = message;\n host.appendChild(el);\n setTimeout(function () {\n el.remove();\n }, 5000);\n}\n\nfunction fail(card, message, retry) {\n toast(message);\n if (!card) return;\n let box = card.querySelector(\"[data-fail]\");\n if (!box) {\n box = document.createElement(\"div\");\n box.setAttribute(\"data-fail\", \"1\");\n box.className = \"fail\";\n card.appendChild(box);\n }\n box.replaceChildren();\n const p = document.createElement(\"p\");\n p.textContent = message;\n const btn = document.createElement(\"button\");\n btn.className = \"btn sm\";\n btn.type = \"button\";\n btn.textContent = \"Retry\";\n btn.addEventListener(\"click\", retry);\n box.appendChild(p);\n box.appendChild(btn);\n}\n\nfunction clearFail(card) {\n const box = card && card.querySelector(\"[data-fail]\");\n if (box) box.remove();\n}\n\nfunction skeleton(card, on) {\n if (!card) return;\n card.classList.toggle(\"busy\", on);\n}\n\nasync function api(method, url, body) {\n const res = await fetch(url, {\n method: method,\n headers: {\n \"content-type\": \"application/json\",\n \"x-requested-with\": \"co-maintainer\",\n },\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n if (!res.ok) {\n let message = \"Request failed\";\n try {\n const data = await res.json();\n if (data.error && data.error.message) message = data.error.message;\n } catch {\n // body was not JSON; keep the generic message\n }\n throw new Error(message);\n }\n if (res.status === 204) return null;\n return await res.json();\n}\n\nasync function run(btn, card, fn) {\n if (btn) btn.disabled = true;\n skeleton(card, true);\n clearFail(card);\n try {\n await fn();\n } catch (err) {\n fail(card, err.message, function () {\n run(btn, card, fn);\n });\n } finally {\n if (btn) btn.disabled = false;\n skeleton(card, false);\n }\n}\n\nasync function postAndGo(url, body, next, btn) {\n const card = btn && btn.closest(\"[data-async]\");\n await run(btn, card, async function () {\n await api(\"POST\", url, body);\n location.href = next;\n });\n}\n\n// Called from the inline scripts on the repo list and repo pages, which deno lint\n// cannot see, so it looks unused from inside this file.\n// deno-lint-ignore no-unused-vars\nfunction bindToggle(btn, url, field) {\n btn.addEventListener(\"click\", async function () {\n if (btn.disabled) return;\n const on = !btn.classList.contains(\"on\");\n btn.classList.toggle(\"on\", on);\n btn.setAttribute(\"data-on\", on ? \"1\" : \"0\");\n btn.disabled = true;\n const card = btn.closest(\"[data-async]\");\n try {\n const payload = {};\n payload[field] = on;\n await api(\"PATCH\", url, payload);\n } catch (err) {\n btn.classList.toggle(\"on\", !on);\n btn.setAttribute(\"data-on\", on ? \"0\" : \"1\");\n fail(card, err.message, function () {\n btn.click();\n });\n } finally {\n btn.disabled = false;\n }\n });\n}\n\n// The helper is inlined into `<head>` by `layout.ts`, before any page script\n// runs, so this is the one place that can close over both `toast()` and\n// `bindToggle` without depending on point-in-time globals.\ndocument.addEventListener(\"DOMContentLoaded\", function () {\n // Client errors bubble to `window` from `document`.\n addEventListener(\"error\", function (ev) {\n toast((ev.error && ev.error.message) || \"Something broke\");\n });\n addEventListener(\"unhandledrejection\", function (ev) {\n toast((ev.reason && ev.reason.message) || \"Something broke\");\n });\n\n document.addEventListener(\"click\", function (ev) {\n const btn =\n ev.target && ev.target.closest\n ? ev.target.closest(\"[data-cancel], [data-post]\")\n : null;\n if (!btn) return;\n ev.preventDefault();\n if (btn.hasAttribute(\"data-cancel\")) {\n postAndGo(\n \"/api/jobs/\" + btn.getAttribute(\"data-cancel\") + \"/cancel\",\n {},\n \"/activity\",\n btn,\n );\n return;\n }\n const body = btn.getAttribute(\"data-body\");\n postAndGo(\n btn.getAttribute(\"data-post\"),\n body ? JSON.parse(body) : {},\n \"/activity\",\n btn,\n );\n });\n\n // Reads `activity-root` at call time, so it has to wait for the body even\n // though this file itself is already parsed.\n if (!document.getElementById(\"activity-root\")) return;\n setInterval(async function () {\n if (document.hidden) return;\n if (document.querySelector(\"[data-async].busy\")) return;\n const root = document.getElementById(\"activity-root\");\n if (!root) return;\n try {\n const res = await fetch(location.pathname + location.search, {\n headers: { accept: \"text/html\" },\n });\n if (!res.ok) return;\n const doc = new DOMParser().parseFromString(\n await res.text(),\n \"text/html\",\n );\n const next = doc.getElementById(\"activity-root\");\n if (!next || next.innerHTML === root.innerHTML) return;\n root.replaceWith(next);\n } catch {\n // a failed poll just waits for the next tick\n }\n }, 5000);\n});\n";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Generated from src/server/pages/client.js by scripts/embed_assets.mjs — do not edit. */
|
|
2
|
-
export const client = "function toast(message) {\n const host = document.getElementById(\"toasts\");\n if (!host) return;\n const el = document.createElement(\"div\");\n el.className = \"toast\";\n el.setAttribute(\"role\", \"status\");\n el.textContent = message;\n host.appendChild(el);\n setTimeout(function () {\n el.remove();\n }, 5000);\n}\n\nfunction fail(card, message, retry) {\n toast(message);\n if (!card) return;\n let box = card.querySelector(\"[data-fail]\");\n if (!box) {\n box = document.createElement(\"div\");\n box.setAttribute(\"data-fail\", \"1\");\n box.className = \"fail\";\n card.appendChild(box);\n }\n box.replaceChildren();\n const p = document.createElement(\"p\");\n p.textContent = message;\n const btn = document.createElement(\"button\");\n btn.className = \"btn sm\";\n btn.type = \"button\";\n btn.textContent = \"Retry\";\n btn.addEventListener(\"click\", retry);\n box.appendChild(p);\n box.appendChild(btn);\n}\n\nfunction clearFail(card) {\n const box = card && card.querySelector(\"[data-fail]\");\n if (box) box.remove();\n}\n\nfunction skeleton(card, on) {\n if (!card) return;\n card.classList.toggle(\"busy\", on);\n}\n\nasync function api(method, url, body) {\n const res = await fetch(url, {\n method: method,\n headers: {\n \"content-type\": \"application/json\",\n \"x-requested-with\": \"co-maintainer\",\n },\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n if (!res.ok) {\n let message = \"Request failed\";\n try {\n const data = await res.json();\n if (data.error && data.error.message) message = data.error.message;\n } catch {\n // body was not JSON; keep the generic message\n }\n throw new Error(message);\n }\n if (res.status === 204) return null;\n return await res.json();\n}\n\nasync function run(btn, card, fn) {\n if (btn) btn.disabled = true;\n skeleton(card, true);\n clearFail(card);\n try {\n await fn();\n } catch (err) {\n fail(card, err.message, function () {\n run(btn, card, fn);\n });\n } finally {\n if (btn) btn.disabled = false;\n skeleton(card, false);\n }\n}\n\nasync function postAndGo(url, body, next, btn) {\n const card = btn && btn.closest(\"[data-async]\");\n await run(btn, card, async function () {\n await api(\"POST\", url, body);\n location.href = next;\n });\n}\n\n// Called from the inline scripts on the repo list and repo pages, which deno lint\n// cannot see, so it looks unused from inside this file.\n// deno-lint-ignore no-unused-vars\nfunction bindToggle(btn, url, field) {\n btn.addEventListener(\"click\", async function () {\n if (btn.disabled) return;\n const on = !btn.classList.contains(\"on\");\n btn.classList.toggle(\"on\", on);\n btn.setAttribute(\"data-on\", on ? \"1\" : \"0\");\n btn.disabled = true;\n const card = btn.closest(\"[data-async]\");\n try {\n const payload = {};\n payload[field] = on;\n await api(\"PATCH\", url, payload);\n } catch (err) {\n btn.classList.toggle(\"on\", !on);\n btn.setAttribute(\"data-on\", on ? \"0\" : \"1\");\n fail(card, err.message, function () {\n btn.click();\n });\n } finally {\n btn.disabled = false;\n }\n });\n}\n\
|
|
2
|
+
export const client = "function toast(message) {\n const host = document.getElementById(\"toasts\");\n if (!host) return;\n const el = document.createElement(\"div\");\n el.className = \"toast\";\n el.setAttribute(\"role\", \"status\");\n el.textContent = message;\n host.appendChild(el);\n setTimeout(function () {\n el.remove();\n }, 5000);\n}\n\nfunction fail(card, message, retry) {\n toast(message);\n if (!card) return;\n let box = card.querySelector(\"[data-fail]\");\n if (!box) {\n box = document.createElement(\"div\");\n box.setAttribute(\"data-fail\", \"1\");\n box.className = \"fail\";\n card.appendChild(box);\n }\n box.replaceChildren();\n const p = document.createElement(\"p\");\n p.textContent = message;\n const btn = document.createElement(\"button\");\n btn.className = \"btn sm\";\n btn.type = \"button\";\n btn.textContent = \"Retry\";\n btn.addEventListener(\"click\", retry);\n box.appendChild(p);\n box.appendChild(btn);\n}\n\nfunction clearFail(card) {\n const box = card && card.querySelector(\"[data-fail]\");\n if (box) box.remove();\n}\n\nfunction skeleton(card, on) {\n if (!card) return;\n card.classList.toggle(\"busy\", on);\n}\n\nasync function api(method, url, body) {\n const res = await fetch(url, {\n method: method,\n headers: {\n \"content-type\": \"application/json\",\n \"x-requested-with\": \"co-maintainer\",\n },\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n if (!res.ok) {\n let message = \"Request failed\";\n try {\n const data = await res.json();\n if (data.error && data.error.message) message = data.error.message;\n } catch {\n // body was not JSON; keep the generic message\n }\n throw new Error(message);\n }\n if (res.status === 204) return null;\n return await res.json();\n}\n\nasync function run(btn, card, fn) {\n if (btn) btn.disabled = true;\n skeleton(card, true);\n clearFail(card);\n try {\n await fn();\n } catch (err) {\n fail(card, err.message, function () {\n run(btn, card, fn);\n });\n } finally {\n if (btn) btn.disabled = false;\n skeleton(card, false);\n }\n}\n\nasync function postAndGo(url, body, next, btn) {\n const card = btn && btn.closest(\"[data-async]\");\n await run(btn, card, async function () {\n await api(\"POST\", url, body);\n location.href = next;\n });\n}\n\n// Called from the inline scripts on the repo list and repo pages, which deno lint\n// cannot see, so it looks unused from inside this file.\n// deno-lint-ignore no-unused-vars\nfunction bindToggle(btn, url, field) {\n btn.addEventListener(\"click\", async function () {\n if (btn.disabled) return;\n const on = !btn.classList.contains(\"on\");\n btn.classList.toggle(\"on\", on);\n btn.setAttribute(\"data-on\", on ? \"1\" : \"0\");\n btn.disabled = true;\n const card = btn.closest(\"[data-async]\");\n try {\n const payload = {};\n payload[field] = on;\n await api(\"PATCH\", url, payload);\n } catch (err) {\n btn.classList.toggle(\"on\", !on);\n btn.setAttribute(\"data-on\", on ? \"0\" : \"1\");\n fail(card, err.message, function () {\n btn.click();\n });\n } finally {\n btn.disabled = false;\n }\n });\n}\n\n// The helper is inlined into `<head>` by `layout.ts`, before any page script\n// runs, so this is the one place that can close over both `toast()` and\n// `bindToggle` without depending on point-in-time globals.\ndocument.addEventListener(\"DOMContentLoaded\", function () {\n // Client errors bubble to `window` from `document`.\n addEventListener(\"error\", function (ev) {\n toast((ev.error && ev.error.message) || \"Something broke\");\n });\n addEventListener(\"unhandledrejection\", function (ev) {\n toast((ev.reason && ev.reason.message) || \"Something broke\");\n });\n\n document.addEventListener(\"click\", function (ev) {\n const btn =\n ev.target && ev.target.closest\n ? ev.target.closest(\"[data-cancel], [data-post]\")\n : null;\n if (!btn) return;\n ev.preventDefault();\n if (btn.hasAttribute(\"data-cancel\")) {\n postAndGo(\n \"/api/jobs/\" + btn.getAttribute(\"data-cancel\") + \"/cancel\",\n {},\n \"/activity\",\n btn,\n );\n return;\n }\n const body = btn.getAttribute(\"data-body\");\n postAndGo(\n btn.getAttribute(\"data-post\"),\n body ? JSON.parse(body) : {},\n \"/activity\",\n btn,\n );\n });\n\n // Reads `activity-root` at call time, so it has to wait for the body even\n // though this file itself is already parsed.\n if (!document.getElementById(\"activity-root\")) return;\n setInterval(async function () {\n if (document.hidden) return;\n if (document.querySelector(\"[data-async].busy\")) return;\n const root = document.getElementById(\"activity-root\");\n if (!root) return;\n try {\n const res = await fetch(location.pathname + location.search, {\n headers: { accept: \"text/html\" },\n });\n if (!res.ok) return;\n const doc = new DOMParser().parseFromString(\n await res.text(),\n \"text/html\",\n );\n const next = doc.getElementById(\"activity-root\");\n if (!next || next.innerHTML === root.innerHTML) return;\n root.replaceWith(next);\n } catch {\n // a failed poll just waits for the next tick\n }\n }, 5000);\n});\n";
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import type { SetupItem } from "../../services/setup_checklist.ts";
|
|
1
2
|
export declare function renderLogin(opts: {
|
|
2
3
|
next: string;
|
|
3
4
|
error?: string;
|
|
4
5
|
showPassword: boolean;
|
|
5
6
|
showGithub: boolean;
|
|
7
|
+
/** `CM_LOGIN_HINT`, for a deployment that hands out its own password (the
|
|
8
|
+
* Cloud image seeds one), so this page can point at it instead of the
|
|
9
|
+
* generic "printed when serve started". Falls back to the default line. */
|
|
10
|
+
hint?: string;
|
|
6
11
|
}): Response;
|
|
7
12
|
export declare function renderHome(username: string, rows: {
|
|
8
13
|
fullName: string;
|
|
@@ -13,4 +18,4 @@ export declare function renderHome(username: string, rows: {
|
|
|
13
18
|
autoOn: boolean;
|
|
14
19
|
reviews: string;
|
|
15
20
|
cost: string;
|
|
16
|
-
}[]): Response;
|
|
21
|
+
}[], checklist: SetupItem[]): Response;
|
|
@@ -18,7 +18,8 @@ export function renderLogin(opts) {
|
|
|
18
18
|
? `<p class="muted" style="text-align:center;margin:16px 0">or</p>`
|
|
19
19
|
: "";
|
|
20
20
|
const lead = opts.showPassword
|
|
21
|
-
?
|
|
21
|
+
? opts.hint?.trim() ||
|
|
22
|
+
"Use the dashboard password printed when serve started."
|
|
22
23
|
: "Sign in with your GitHub account.";
|
|
23
24
|
return html(layout({
|
|
24
25
|
title: "Sign in · co-maintainer",
|
|
@@ -33,11 +34,32 @@ export function renderLogin(opts) {
|
|
|
33
34
|
</div>`,
|
|
34
35
|
}));
|
|
35
36
|
}
|
|
36
|
-
export function renderHome(username, rows) {
|
|
37
|
+
export function renderHome(username, rows, checklist) {
|
|
37
38
|
const autoCount = rows.filter((row) => row.autoOn).length;
|
|
38
39
|
const lead = rows.length === 0
|
|
39
40
|
? "No repositories yet. Add one to start."
|
|
40
41
|
: `${rows.length} repositories · ${autoCount} reviewing pull requests automatically`;
|
|
42
|
+
const remaining = checklist.filter((item) => !item.done);
|
|
43
|
+
// The card is hidden entirely once everything is done, so a finished
|
|
44
|
+
// install is not told to "finish" anything.
|
|
45
|
+
const setupCard = remaining.length === 0
|
|
46
|
+
? ""
|
|
47
|
+
: `<div class="card" id="finish-setup">
|
|
48
|
+
<div class="hd"><h2>Finish setup</h2>
|
|
49
|
+
<span class="muted" style="margin-left:auto">${checklist.length - remaining.length}/${checklist.length} done</span></div>
|
|
50
|
+
<div class="bd">
|
|
51
|
+
<ul class="steps">
|
|
52
|
+
${checklist
|
|
53
|
+
.map((item, index) => `<li class="${item.done ? "done" : ""}">
|
|
54
|
+
<span class="n">${item.done ? "✓" : index + 1}</span>
|
|
55
|
+
<div class="body"><b>${text(item.title)}</b>
|
|
56
|
+
<span class="muted">${text(item.detail)}</span></div>
|
|
57
|
+
<a class="btn sm" href="${item.href}">${text(item.linkLabel)}</a>
|
|
58
|
+
</li>`)
|
|
59
|
+
.join("")}
|
|
60
|
+
</ul>
|
|
61
|
+
</div>
|
|
62
|
+
</div>`;
|
|
41
63
|
const body = rows.length === 0
|
|
42
64
|
? empty("No repositories", "Add a repository to generate knowledge and review pull requests.")
|
|
43
65
|
: `<div class="card" data-async>
|
|
@@ -76,6 +98,7 @@ export function renderHome(username, rows) {
|
|
|
76
98
|
<div><h1>Repositories</h1><p class="lead">${text(lead)}</p></div>
|
|
77
99
|
<div class="actions">${rows.length === 0 ? `<a class="btn" href="/setup">Get started</a>` : ""}<a class="btn primary" href="/repos/new">Add repository</a></div>
|
|
78
100
|
</div>
|
|
101
|
+
${setupCard}
|
|
79
102
|
${body}
|
|
80
103
|
</div>
|
|
81
104
|
<script>
|
|
@@ -44,7 +44,7 @@ export function renderKnowledge(username, data, viewId) {
|
|
|
44
44
|
<div class="pagehead">
|
|
45
45
|
<div><h1>Knowledge</h1>
|
|
46
46
|
<p class="lead">What co-maintainer learned about this repository, and checks every pull request against.</p></div>
|
|
47
|
-
<div class="actions"><button class="primary" id="update">
|
|
47
|
+
<div class="actions"><button class="primary" id="update">Sync</button></div>
|
|
48
48
|
</div>
|
|
49
49
|
${notice}
|
|
50
50
|
<div class="card"><div class="bd flush">${table}</div></div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { safeCopy } from "../../util/redact.js";
|
|
2
2
|
import { relativeTime } from "../../util/time.js";
|
|
3
|
+
import { client } from "./client.js";
|
|
3
4
|
export function escapeHtml(value) {
|
|
4
5
|
return String(value ?? "").replace(/[&<>"']/g, (char) => ({
|
|
5
6
|
"&": "&",
|
|
@@ -191,14 +192,13 @@ export function layout(opts) {
|
|
|
191
192
|
</div>`
|
|
192
193
|
: "";
|
|
193
194
|
return `<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
194
|
-
<title>${text(opts.title)}</title><link rel="stylesheet" href="/styles.css"><link rel="icon" href="/logo.png" type="image/png"></head><body>
|
|
195
|
+
<title>${text(opts.title)}</title><link rel="stylesheet" href="/styles.css"><link rel="icon" href="/logo.png" type="image/png"><script>${client}</script></head><body>
|
|
195
196
|
<div class="top"><div class="in">
|
|
196
197
|
<a class="logo" href="/"><img src="/logo.png" alt="">co-maintainer</a>
|
|
197
198
|
${right}
|
|
198
199
|
</div></div>
|
|
199
200
|
${opts.body}
|
|
200
201
|
<div id="toasts"></div>
|
|
201
|
-
<script src="/client.js"></script>
|
|
202
202
|
</body></html>`;
|
|
203
203
|
}
|
|
204
204
|
export function html(body, status = 200) {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { repoOverview } from "../../services/dashboard.ts";
|
|
2
|
-
export declare function renderRepo(username: string, data: ReturnType<typeof repoOverview
|
|
2
|
+
export declare function renderRepo(username: string, data: ReturnType<typeof repoOverview>, webhookUrl: string): Response;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { capped, empty, html, layout, money, repoNav, skSlot, text, when, } from "./layout.js";
|
|
2
2
|
import { COMPARE_FILE_CAP, MAX_COMMITS_COUNTED } from "../../services/drift.js";
|
|
3
|
-
|
|
3
|
+
import { webhookReachabilityProblem } from "../../util/webhook_reachability.js";
|
|
4
|
+
import { lastDeliveryForRepo } from "../../store/deliveries.js";
|
|
5
|
+
export function renderRepo(username, data, webhookUrl) {
|
|
4
6
|
const name = data.repo.full_name;
|
|
5
7
|
const stats = data.stats;
|
|
6
8
|
const drift = data.drift;
|
|
@@ -10,7 +12,7 @@ export function renderRepo(username, data) {
|
|
|
10
12
|
${skSlot()}
|
|
11
13
|
<div class="txt"><b>Update recommended.</b> Since the guide was built:
|
|
12
14
|
${drift.prs_since} new pull requests, ${drift.prs_updated} changed pull requests, ${capped(drift.commits_since, MAX_COMMITS_COUNTED)} commits, ${capped(drift.files_changed, COMPARE_FILE_CAP)} files changed.</div>
|
|
13
|
-
<button class="btn primary" id="remake">
|
|
15
|
+
<button class="btn primary" id="remake">Sync now</button>
|
|
14
16
|
</div>`
|
|
15
17
|
: "";
|
|
16
18
|
const pulls = data.recentPulls.length === 0
|
|
@@ -25,6 +27,22 @@ export function renderRepo(username, data) {
|
|
|
25
27
|
.join("")}
|
|
26
28
|
</tbody>
|
|
27
29
|
</table>`;
|
|
30
|
+
const reach = webhookReachabilityProblem(webhookUrl);
|
|
31
|
+
const last = lastDeliveryForRepo(name);
|
|
32
|
+
const delivery = last ? when(last.received_at) : "never";
|
|
33
|
+
const webhook = reach
|
|
34
|
+
? `<div class="notice bad">
|
|
35
|
+
<div class="txt"><b>GitHub cannot reach this address, so automatic reviews will not arrive.</b>
|
|
36
|
+
${text(reach)} The webhook URL is <code>${text(webhookUrl)}</code>. Change it in <a href="/settings">Settings</a>.
|
|
37
|
+
Last webhook delivery: ${delivery}.</div>
|
|
38
|
+
</div>`
|
|
39
|
+
: !last
|
|
40
|
+
? `<div class="notice info">
|
|
41
|
+
<div class="txt"><b>No webhook delivery yet.</b> GitHub has not reached this repository
|
|
42
|
+
and the last webhook delivery is never. Open a pull request, or check the webhook URL
|
|
43
|
+
<code>${text(webhookUrl)}</code> in <a href="/settings">Settings</a>.</div>
|
|
44
|
+
</div>`
|
|
45
|
+
: `<p class="muted" style="margin:0 0 18px">Last webhook delivery: ${delivery}.</p>`;
|
|
28
46
|
const auto = data.repo.auto_review === 1;
|
|
29
47
|
return html(layout({
|
|
30
48
|
title: `${name} · co-maintainer`,
|
|
@@ -47,6 +65,7 @@ export function renderRepo(username, data) {
|
|
|
47
65
|
</div>
|
|
48
66
|
</div>
|
|
49
67
|
${notice}
|
|
68
|
+
${webhook}
|
|
50
69
|
<div class="card">
|
|
51
70
|
<div class="hd"><h2>Last 30 days</h2></div>
|
|
52
71
|
<div class="bd">
|
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
import type { repoPulls } from "../../services/dashboard.ts";
|
|
2
|
-
|
|
2
|
+
import type { OpenPull } from "../../github/app.ts";
|
|
3
|
+
export declare function renderRepoPulls(username: string, fullName: string, data: ReturnType<typeof repoPulls>, open: {
|
|
4
|
+
pulls: OpenPull[] | undefined;
|
|
5
|
+
appConfigured: boolean;
|
|
6
|
+
}): Response;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { empty, html, layout, money, repoNav, skSlot, text, when, } from "./layout.js";
|
|
2
|
-
export function renderRepoPulls(username, fullName, data) {
|
|
2
|
+
export function renderRepoPulls(username, fullName, data, open) {
|
|
3
3
|
const start = (data.page - 1) * data.per + 1;
|
|
4
4
|
const end = Math.min(data.page * data.per, data.total);
|
|
5
5
|
const pager = data.total === 0
|
|
@@ -30,6 +30,32 @@ export function renderRepoPulls(username, fullName, data) {
|
|
|
30
30
|
</table>${pager}`;
|
|
31
31
|
const [owner, repo] = fullName.split("/");
|
|
32
32
|
const reviewBase = `/api/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/`;
|
|
33
|
+
const escape = (value) => value
|
|
34
|
+
.replace(/&/g, "&")
|
|
35
|
+
.replace(/</g, "<")
|
|
36
|
+
.replace(/>/g, ">")
|
|
37
|
+
.replace(/"/g, """);
|
|
38
|
+
const openList = open.pulls
|
|
39
|
+
? open.pulls.length === 0
|
|
40
|
+
? `<div class="empty"><h2>No open pull requests</h2><p>Open one on GitHub and it will appear here.</p></div>`
|
|
41
|
+
: `<table>
|
|
42
|
+
<thead><tr><th>Pull request</th><th>Author</th><th>Branch</th>
|
|
43
|
+
<th>Updated</th><th></th></tr></thead>
|
|
44
|
+
<tbody>
|
|
45
|
+
${open.pulls
|
|
46
|
+
.map((pull) => `<tr><td><a href="/repos/${text(fullName)}/pulls/${pull.number}">#${pull.number}</a>
|
|
47
|
+
${escape(pull.title)}</td>
|
|
48
|
+
<td class="muted">${escape(pull.author)}</td>
|
|
49
|
+
<td class="muted mono">${escape(pull.headRef)}</td>
|
|
50
|
+
<td class="muted">${when(pull.updatedAt)}</td>
|
|
51
|
+
<td class="num"><button class="btn sm" type="button"
|
|
52
|
+
data-review-pr="${pull.number}">Review</button></td></tr>`)
|
|
53
|
+
.join("")}
|
|
54
|
+
</tbody>
|
|
55
|
+
</table>`
|
|
56
|
+
: open.appConfigured
|
|
57
|
+
? `<div class="empty"><h2>Open pull requests are unavailable</h2><p>GitHub did not return a list. You can still start a review by number below.</p></div>`
|
|
58
|
+
: `<div class="empty"><h2>Configure the GitHub App to list open pull requests</h2><p>Without it, start a review by number below.</p></div>`;
|
|
33
59
|
return html(layout({
|
|
34
60
|
title: `Pull requests · ${fullName}`,
|
|
35
61
|
username,
|
|
@@ -42,6 +68,10 @@ export function renderRepoPulls(username, fullName, data) {
|
|
|
42
68
|
<div><h1>Pull requests</h1>
|
|
43
69
|
<p class="lead">${data.stats.pullRequests} reviewed in the last 30 days</p></div>
|
|
44
70
|
</div>
|
|
71
|
+
<div data-async>
|
|
72
|
+
${skSlot()}
|
|
73
|
+
<div class="card"><div class="bd flush">${openList}</div></div>
|
|
74
|
+
</div>
|
|
45
75
|
<div data-async>
|
|
46
76
|
${skSlot()}
|
|
47
77
|
<div class="card"><div class="bd">
|
|
@@ -70,6 +100,11 @@ document.getElementById("manual-review").addEventListener("submit", function(eve
|
|
|
70
100
|
}
|
|
71
101
|
postAndGo(${JSON.stringify(reviewBase)} + number + "/review", {}, "/activity", this.querySelector("button"));
|
|
72
102
|
});
|
|
103
|
+
document.querySelectorAll("[data-review-pr]").forEach(function(btn) {
|
|
104
|
+
btn.addEventListener("click", function() {
|
|
105
|
+
postAndGo(${JSON.stringify(reviewBase)} + btn.getAttribute("data-review-pr") + "/review", {}, "/activity", btn);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
73
108
|
</script>`,
|
|
74
109
|
}));
|
|
75
110
|
}
|
|
@@ -8,8 +8,8 @@ export function renderRepoRemote(username, fullName, data) {
|
|
|
8
8
|
<tbody>
|
|
9
9
|
${data.items
|
|
10
10
|
.map((review) => `<tr>
|
|
11
|
-
<td>${text(review.branch ?? "
|
|
12
|
-
<td>${text(review.token_name ?? "
|
|
11
|
+
<td>${text(review.branch ?? "none")}</td>
|
|
12
|
+
<td>${text(review.token_name ?? "none")}</td>
|
|
13
13
|
<td>${text(review.status)}</td>
|
|
14
14
|
<td class="num">${review.findings_count ?? 0}</td>
|
|
15
15
|
<td class="num">${money(review.cost ?? 0)}</td>
|