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.
Files changed (294) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/dist/main.d.ts +2 -0
  4. package/dist/main.js +9 -0
  5. package/dist/mod.d.ts +4 -0
  6. package/dist/mod.js +2 -0
  7. package/dist/package.json +54 -0
  8. package/dist/src/ai/batch.d.ts +16 -0
  9. package/dist/src/ai/batch.js +117 -0
  10. package/dist/src/ai/fake.d.ts +7 -0
  11. package/dist/src/ai/fake.js +40 -0
  12. package/dist/src/ai/hetzner.d.ts +16 -0
  13. package/dist/src/ai/hetzner.js +69 -0
  14. package/dist/src/ai/mermaid.d.ts +27 -0
  15. package/dist/src/ai/mermaid.js +457 -0
  16. package/dist/src/ai/mermaid_loop.d.ts +12 -0
  17. package/dist/src/ai/mermaid_loop.js +100 -0
  18. package/dist/src/ai/openrouter.d.ts +9 -0
  19. package/dist/src/ai/openrouter.js +29 -0
  20. package/dist/src/ai/provider.d.ts +4 -0
  21. package/dist/src/ai/provider.js +63 -0
  22. package/dist/src/cli/args.d.ts +5 -0
  23. package/dist/src/cli/args.js +252 -0
  24. package/dist/src/cli/commands/probe.d.ts +2 -0
  25. package/dist/src/cli/commands/probe.js +122 -0
  26. package/dist/src/cli/commands/review.d.ts +3 -0
  27. package/dist/src/cli/commands/review.js +84 -0
  28. package/dist/src/cli/commands/serve.d.ts +18 -0
  29. package/dist/src/cli/commands/serve.js +183 -0
  30. package/dist/src/cli/commands/set.d.ts +7 -0
  31. package/dist/src/cli/commands/set.js +171 -0
  32. package/dist/src/cli/main.d.ts +1 -0
  33. package/dist/src/cli/main.js +27 -0
  34. package/dist/src/cli/prompt.d.ts +4 -0
  35. package/dist/src/cli/prompt.js +26 -0
  36. package/dist/src/cli/review_args.d.ts +28 -0
  37. package/dist/src/cli/review_args.js +84 -0
  38. package/dist/src/cli/review_output.d.ts +2 -0
  39. package/dist/src/cli/review_output.js +10 -0
  40. package/dist/src/cli/review_result.d.ts +81 -0
  41. package/dist/src/cli/review_result.js +281 -0
  42. package/dist/src/config.d.ts +108 -0
  43. package/dist/src/config.js +138 -0
  44. package/dist/src/git/default_branch.d.ts +5 -0
  45. package/dist/src/git/default_branch.js +22 -0
  46. package/dist/src/github/app.d.ts +43 -0
  47. package/dist/src/github/app.js +120 -0
  48. package/dist/src/github/client.d.ts +17 -0
  49. package/dist/src/github/client.js +66 -0
  50. package/dist/src/github/collect.d.ts +7 -0
  51. package/dist/src/github/collect.js +368 -0
  52. package/dist/src/github/gh.d.ts +8 -0
  53. package/dist/src/github/gh.js +63 -0
  54. package/dist/src/github/jwt.d.ts +12 -0
  55. package/dist/src/github/jwt.js +84 -0
  56. package/dist/src/github/oauth.d.ts +9 -0
  57. package/dist/src/github/oauth.js +51 -0
  58. package/dist/src/github/pat.d.ts +10 -0
  59. package/dist/src/github/pat.js +60 -0
  60. package/dist/src/knowledge/facts.d.ts +3 -0
  61. package/dist/src/knowledge/facts.js +325 -0
  62. package/dist/src/knowledge/guide.d.ts +6 -0
  63. package/dist/src/knowledge/guide.js +82 -0
  64. package/dist/src/knowledge/probe.d.ts +12 -0
  65. package/dist/src/knowledge/probe.js +144 -0
  66. package/dist/src/knowledge/sections.d.ts +2 -0
  67. package/dist/src/knowledge/sections.js +13 -0
  68. package/dist/src/knowledge/skill.d.ts +10 -0
  69. package/dist/src/knowledge/skill.js +106 -0
  70. package/dist/src/knowledge/synthesis.d.ts +7 -0
  71. package/dist/src/knowledge/synthesis.js +315 -0
  72. package/dist/src/knowledge/types.d.ts +49 -0
  73. package/dist/src/knowledge/types.js +1 -0
  74. package/dist/src/knowledge/validate.d.ts +6 -0
  75. package/dist/src/knowledge/validate.js +134 -0
  76. package/dist/src/local/carry_over_store.d.ts +18 -0
  77. package/dist/src/local/carry_over_store.js +40 -0
  78. package/dist/src/local/codegraph_exclude.d.ts +3 -0
  79. package/dist/src/local/codegraph_exclude.js +26 -0
  80. package/dist/src/local/codegraph_prepare.d.ts +17 -0
  81. package/dist/src/local/codegraph_prepare.js +51 -0
  82. package/dist/src/local/git_ops.d.ts +12 -0
  83. package/dist/src/local/git_ops.js +98 -0
  84. package/dist/src/local/git_parse.d.ts +27 -0
  85. package/dist/src/local/git_parse.js +131 -0
  86. package/dist/src/local/git_revision.d.ts +15 -0
  87. package/dist/src/local/git_revision.js +141 -0
  88. package/dist/src/local/git_untracked.d.ts +21 -0
  89. package/dist/src/local/git_untracked.js +79 -0
  90. package/dist/src/local/review_local.d.ts +4 -0
  91. package/dist/src/local/review_local.js +273 -0
  92. package/dist/src/local/review_lock.d.ts +7 -0
  93. package/dist/src/local/review_lock.js +45 -0
  94. package/dist/src/pr/checkout.d.ts +25 -0
  95. package/dist/src/pr/checkout.js +136 -0
  96. package/dist/src/pr/codegraph_tool_args.d.ts +8 -0
  97. package/dist/src/pr/codegraph_tool_args.js +34 -0
  98. package/dist/src/pr/codegraph_tools.d.ts +15 -0
  99. package/dist/src/pr/codegraph_tools.js +408 -0
  100. package/dist/src/pr/diff_summary.d.ts +6 -0
  101. package/dist/src/pr/diff_summary.js +52 -0
  102. package/dist/src/pr/findings.d.ts +14 -0
  103. package/dist/src/pr/findings.js +94 -0
  104. package/dist/src/pr/github_revision.d.ts +4 -0
  105. package/dist/src/pr/github_revision.js +39 -0
  106. package/dist/src/pr/hunks.d.ts +18 -0
  107. package/dist/src/pr/hunks.js +73 -0
  108. package/dist/src/pr/reviewer.d.ts +40 -0
  109. package/dist/src/pr/reviewer.js +582 -0
  110. package/dist/src/pr/rounds.d.ts +17 -0
  111. package/dist/src/pr/rounds.js +43 -0
  112. package/dist/src/pr/scope.d.ts +35 -0
  113. package/dist/src/pr/scope.js +84 -0
  114. package/dist/src/pr/snapshot.d.ts +11 -0
  115. package/dist/src/pr/snapshot.js +1 -0
  116. package/dist/src/pr/suggestion.d.ts +17 -0
  117. package/dist/src/pr/suggestion.js +70 -0
  118. package/dist/src/remote/client.d.ts +4 -0
  119. package/dist/src/remote/client.js +199 -0
  120. package/dist/src/remote/client_tools.d.ts +13 -0
  121. package/dist/src/remote/client_tools.js +30 -0
  122. package/dist/src/remote/fixtures.d.ts +1 -0
  123. package/dist/src/remote/fixtures.js +30 -0
  124. package/dist/src/remote/revision_from_submit.d.ts +2 -0
  125. package/dist/src/remote/revision_from_submit.js +26 -0
  126. package/dist/src/remote/schema.d.ts +14 -0
  127. package/dist/src/remote/schema.js +30 -0
  128. package/dist/src/remote/server/body.d.ts +18 -0
  129. package/dist/src/remote/server/body.js +73 -0
  130. package/dist/src/remote/server/routes.d.ts +1 -0
  131. package/dist/src/remote/server/routes.js +150 -0
  132. package/dist/src/remote/server/sessions.d.ts +16 -0
  133. package/dist/src/remote/server/sessions.js +48 -0
  134. package/dist/src/remote/server/sync.d.ts +3 -0
  135. package/dist/src/remote/server/sync.js +90 -0
  136. package/dist/src/remote/tool_bridge.d.ts +15 -0
  137. package/dist/src/remote/tool_bridge.js +84 -0
  138. package/dist/src/remote/validate.d.ts +8 -0
  139. package/dist/src/remote/validate.js +248 -0
  140. package/dist/src/review/carry_over.d.ts +63 -0
  141. package/dist/src/review/carry_over.js +372 -0
  142. package/dist/src/review/engine.d.ts +13 -0
  143. package/dist/src/review/engine.js +4 -0
  144. package/dist/src/review/guides.d.ts +12 -0
  145. package/dist/src/review/guides.js +44 -0
  146. package/dist/src/review/revision.d.ts +26 -0
  147. package/dist/src/review/revision.js +57 -0
  148. package/dist/src/server/api/activity.d.ts +1 -0
  149. package/dist/src/server/api/activity.js +12 -0
  150. package/dist/src/server/api/analytics.d.ts +1 -0
  151. package/dist/src/server/api/analytics.js +9 -0
  152. package/dist/src/server/api/installations.d.ts +4 -0
  153. package/dist/src/server/api/installations.js +24 -0
  154. package/dist/src/server/api/jobs.d.ts +1 -0
  155. package/dist/src/server/api/jobs.js +86 -0
  156. package/dist/src/server/api/json_body.d.ts +1 -0
  157. package/dist/src/server/api/json_body.js +14 -0
  158. package/dist/src/server/api/remote_tokens.d.ts +1 -0
  159. package/dist/src/server/api/remote_tokens.js +71 -0
  160. package/dist/src/server/api/repos.d.ts +4 -0
  161. package/dist/src/server/api/repos.js +136 -0
  162. package/dist/src/server/api/settings.d.ts +1 -0
  163. package/dist/src/server/api/settings.js +210 -0
  164. package/dist/src/server/app.d.ts +27 -0
  165. package/dist/src/server/app.js +147 -0
  166. package/dist/src/server/assets.d.ts +3 -0
  167. package/dist/src/server/assets.js +21 -0
  168. package/dist/src/server/auth.d.ts +44 -0
  169. package/dist/src/server/auth.js +188 -0
  170. package/dist/src/server/errors.d.ts +8 -0
  171. package/dist/src/server/errors.js +10 -0
  172. package/dist/src/server/http.d.ts +8 -0
  173. package/dist/src/server/http.js +117 -0
  174. package/dist/src/server/logo.d.ts +1 -0
  175. package/dist/src/server/logo.js +2 -0
  176. package/dist/src/server/pages/activity.d.ts +4 -0
  177. package/dist/src/server/pages/activity.js +200 -0
  178. package/dist/src/server/pages/add_repo.d.ts +9 -0
  179. package/dist/src/server/pages/add_repo.js +56 -0
  180. package/dist/src/server/pages/analytics.d.ts +2 -0
  181. package/dist/src/server/pages/analytics.js +193 -0
  182. package/dist/src/server/pages/client.d.ts +2 -0
  183. package/dist/src/server/pages/client.js +2 -0
  184. package/dist/src/server/pages/home.d.ts +16 -0
  185. package/dist/src/server/pages/home.js +87 -0
  186. package/dist/src/server/pages/knowledge.d.ts +2 -0
  187. package/dist/src/server/pages/knowledge.js +64 -0
  188. package/dist/src/server/pages/layout.d.ts +32 -0
  189. package/dist/src/server/pages/layout.js +230 -0
  190. package/dist/src/server/pages/pr.d.ts +2 -0
  191. package/dist/src/server/pages/pr.js +109 -0
  192. package/dist/src/server/pages/repo.d.ts +2 -0
  193. package/dist/src/server/pages/repo.js +92 -0
  194. package/dist/src/server/pages/repo_prs.d.ts +2 -0
  195. package/dist/src/server/pages/repo_prs.js +75 -0
  196. package/dist/src/server/pages/repo_remote.d.ts +2 -0
  197. package/dist/src/server/pages/repo_remote.js +38 -0
  198. package/dist/src/server/pages/repo_settings.d.ts +3 -0
  199. package/dist/src/server/pages/repo_settings.js +127 -0
  200. package/dist/src/server/pages/router.d.ts +17 -0
  201. package/dist/src/server/pages/router.js +312 -0
  202. package/dist/src/server/pages/settings.d.ts +2 -0
  203. package/dist/src/server/pages/settings.js +337 -0
  204. package/dist/src/server/pages/setup.d.ts +5 -0
  205. package/dist/src/server/pages/setup.js +38 -0
  206. package/dist/src/server/pages/styles.d.ts +2 -0
  207. package/dist/src/server/pages/styles.js +2 -0
  208. package/dist/src/server/webhook/index.d.ts +1 -0
  209. package/dist/src/server/webhook/index.js +64 -0
  210. package/dist/src/server/webhook/signature.d.ts +7 -0
  211. package/dist/src/server/webhook/signature.js +41 -0
  212. package/dist/src/services/credentials.d.ts +19 -0
  213. package/dist/src/services/credentials.js +146 -0
  214. package/dist/src/services/dashboard.d.ts +165 -0
  215. package/dist/src/services/dashboard.js +200 -0
  216. package/dist/src/services/drift.d.ts +13 -0
  217. package/dist/src/services/drift.js +76 -0
  218. package/dist/src/services/jobs.d.ts +34 -0
  219. package/dist/src/services/jobs.js +222 -0
  220. package/dist/src/services/remote_review.d.ts +10 -0
  221. package/dist/src/services/remote_review.js +274 -0
  222. package/dist/src/services/remote_review_abort.d.ts +3 -0
  223. package/dist/src/services/remote_review_abort.js +11 -0
  224. package/dist/src/services/remote_token_jobs.d.ts +1 -0
  225. package/dist/src/services/remote_token_jobs.js +20 -0
  226. package/dist/src/services/remote_tokens.d.ts +13 -0
  227. package/dist/src/services/remote_tokens.js +57 -0
  228. package/dist/src/services/replies.d.ts +7 -0
  229. package/dist/src/services/replies.js +248 -0
  230. package/dist/src/services/review.d.ts +35 -0
  231. package/dist/src/services/review.js +671 -0
  232. package/dist/src/services/setup.d.ts +29 -0
  233. package/dist/src/services/setup.js +364 -0
  234. package/dist/src/services/webhook.d.ts +22 -0
  235. package/dist/src/services/webhook.js +401 -0
  236. package/dist/src/store/app_db.d.ts +10 -0
  237. package/dist/src/store/app_db.js +85 -0
  238. package/dist/src/store/cache_db.d.ts +4 -0
  239. package/dist/src/store/cache_db.js +63 -0
  240. package/dist/src/store/deliveries.d.ts +12 -0
  241. package/dist/src/store/deliveries.js +23 -0
  242. package/dist/src/store/drift.d.ts +4 -0
  243. package/dist/src/store/drift.js +22 -0
  244. package/dist/src/store/findings.d.ts +31 -0
  245. package/dist/src/store/findings.js +64 -0
  246. package/dist/src/store/installations.d.ts +9 -0
  247. package/dist/src/store/installations.js +35 -0
  248. package/dist/src/store/job_logs.d.ts +5 -0
  249. package/dist/src/store/job_logs.js +17 -0
  250. package/dist/src/store/jobs.d.ts +27 -0
  251. package/dist/src/store/jobs.js +86 -0
  252. package/dist/src/store/migrations.d.ts +5 -0
  253. package/dist/src/store/migrations.js +258 -0
  254. package/dist/src/store/remote_review_inputs.d.ts +11 -0
  255. package/dist/src/store/remote_review_inputs.js +25 -0
  256. package/dist/src/store/remote_tokens.d.ts +10 -0
  257. package/dist/src/store/remote_tokens.js +45 -0
  258. package/dist/src/store/replies.d.ts +24 -0
  259. package/dist/src/store/replies.js +79 -0
  260. package/dist/src/store/repos.d.ts +11 -0
  261. package/dist/src/store/repos.js +59 -0
  262. package/dist/src/store/reviews.d.ts +86 -0
  263. package/dist/src/store/reviews.js +220 -0
  264. package/dist/src/store/rows.d.ts +179 -0
  265. package/dist/src/store/rows.js +4 -0
  266. package/dist/src/store/sessions.d.ts +8 -0
  267. package/dist/src/store/sessions.js +34 -0
  268. package/dist/src/store/skill_state.d.ts +3 -0
  269. package/dist/src/store/skill_state.js +24 -0
  270. package/dist/src/store/sqlite.d.ts +39 -0
  271. package/dist/src/store/sqlite.js +77 -0
  272. package/dist/src/store/subjects.d.ts +15 -0
  273. package/dist/src/store/subjects.js +78 -0
  274. package/dist/src/tools/codegraph.d.ts +58 -0
  275. package/dist/src/tools/codegraph.js +165 -0
  276. package/dist/src/tools/codegraph_exec.d.ts +13 -0
  277. package/dist/src/tools/codegraph_exec.js +66 -0
  278. package/dist/src/types.d.ts +89 -0
  279. package/dist/src/types.js +1 -0
  280. package/dist/src/util/atomic.d.ts +4 -0
  281. package/dist/src/util/atomic.js +78 -0
  282. package/dist/src/util/keyed_lock.d.ts +3 -0
  283. package/dist/src/util/keyed_lock.js +33 -0
  284. package/dist/src/util/log.d.ts +11 -0
  285. package/dist/src/util/log.js +42 -0
  286. package/dist/src/util/redact.d.ts +5 -0
  287. package/dist/src/util/redact.js +30 -0
  288. package/dist/src/util/runtime.d.ts +90 -0
  289. package/dist/src/util/runtime.js +206 -0
  290. package/dist/src/util/time.d.ts +9 -0
  291. package/dist/src/util/time.js +36 -0
  292. package/dist/src/version.d.ts +1 -0
  293. package/dist/src/version.js +5 -0
  294. package/package.json +54 -0
@@ -0,0 +1,337 @@
1
+ import { html, layout, skSlot, text } from "./layout.js";
2
+ import { VERSION } from "../../version.js";
3
+ const SOURCE_URL = "https://github.com/GroophyLifefor/co-maintainer";
4
+ export function renderSettings(username, config, webhookUrl) {
5
+ const aiOk = Boolean(config.ai && config.ai !== "none" && config.token);
6
+ const ghOk = Boolean(config.auth === "gh" || (config.auth === "pat" && config.githubPat));
7
+ const appOk = Boolean(config.githubAppId && config.githubAppPrivateKey);
8
+ return html(layout({
9
+ title: "Settings · co-maintainer",
10
+ username,
11
+ active: "settings",
12
+ body: `<div class="wrap side">
13
+ <aside>
14
+ <p class="lbl">Settings</p>
15
+ <nav>
16
+ <a href="/settings" class="on">Models and keys</a>
17
+ <a href="#github">GitHub</a>
18
+ <a href="#defaults">Defaults</a>
19
+ <a href="#server">Server</a>
20
+ <a href="#remote">Remote review</a>
21
+ <a href="#access">Access</a>
22
+ <a href="#about">About</a>
23
+ </nav>
24
+ </aside>
25
+ <div>
26
+ <div class="pagehead"><div><h1>Settings</h1>
27
+ <p class="lead">Used by every repository unless it overrides them.</p></div></div>
28
+ <div class="card" data-async>
29
+ ${skSlot()}
30
+ <div class="hd"><h2>Models and API key</h2>
31
+ <span class="st ${aiOk ? "ok" : "warn"}" style="margin-left:auto">${aiOk ? "Configured" : "Not set"}</span></div>
32
+ <div class="bd">
33
+ <div class="field"><label>Provider</label>
34
+ <select id="ai">
35
+ <option value="none"${!config.ai || config.ai === "none" ? " selected" : ""}>None</option>
36
+ <option value="openrouter"${config.ai === "openrouter" ? " selected" : ""}>OpenRouter</option>
37
+ <option value="hetzner"${config.ai === "hetzner" ? " selected" : ""}>Hetzner</option>
38
+ </select></div>
39
+ <div class="field"><label>API key</label>
40
+ <input id="token" type="password" placeholder="Leave blank to keep the current key">
41
+ <div class="hint">${config.token ? "A key is saved" : "No key saved"}</div></div>
42
+ <div class="two" style="max-width:none">
43
+ <div class="field"><label>Main model</label>
44
+ <input id="high" value="${text(config.highModel ?? "")}">
45
+ <div class="hint">Writes reviews</div></div>
46
+ <div class="field"><label>Cheap model</label>
47
+ <input id="low" value="${text(config.lowModel ?? "")}">
48
+ <div class="hint">Reads history when setting up a repository</div></div>
49
+ </div>
50
+ </div>
51
+ <div class="ft"><button class="primary" id="save-ai">Save</button></div>
52
+ </div>
53
+ <div class="card" id="github" data-async>
54
+ ${skSlot()}
55
+ <div class="hd"><h2>GitHub</h2>
56
+ <span class="st ${ghOk ? "ok" : "warn"}" style="margin-left:auto">${ghOk ? "Configured" : "Not set"}</span></div>
57
+ <div class="bd">
58
+ <div class="field"><label>Access</label>
59
+ <select id="auth">
60
+ <option value="gh"${config.auth !== "pat" ? " selected" : ""}>gh CLI</option>
61
+ <option value="pat"${config.auth === "pat" ? " selected" : ""}>Personal access token</option>
62
+ </select></div>
63
+ <div class="field"><label>Personal access token</label>
64
+ <input id="pat" type="password" placeholder="Leave blank to keep the current token"></div>
65
+ </div>
66
+ <div class="ft"><button class="primary" id="save-gh">Save</button></div>
67
+ </div>
68
+ <div class="card" data-async>
69
+ ${skSlot()}
70
+ <div class="hd"><h2>GitHub App</h2>
71
+ <span class="st ${appOk ? "ok" : "warn"}" style="margin-left:auto">${appOk ? "Configured" : "Not set"}</span></div>
72
+ <div class="bd">
73
+ <p class="muted" style="margin:0 0 16px">Needed to post reviews on pull requests.</p>
74
+ <div class="two" style="max-width:none">
75
+ <div class="field"><label>App ID</label>
76
+ <input id="app-id" value="${text(config.githubAppId ?? "")}"></div>
77
+ <div class="field"><label>Webhook address</label>
78
+ <input id="webhook-url" value="${text(webhookUrl)}">
79
+ <div class="hint">Public URL GitHub uses to send webhook events</div></div>
80
+ </div>
81
+ <div class="field wide"><label>Private key</label>
82
+ <textarea id="app-key" class="mono pem" rows="8" spellcheck="false"
83
+ autocomplete="off" autocapitalize="off"
84
+ placeholder="-----BEGIN RSA PRIVATE KEY-----
85
+ ...
86
+ -----END RSA PRIVATE KEY-----
87
+ Leave blank to keep the current key"></textarea></div>
88
+ <div class="field"><label>Webhook secret</label>
89
+ <input id="hook-secret" type="password" placeholder="Leave blank to keep the current secret"></div>
90
+ </div>
91
+ <div class="ft"><button class="primary" id="save-app">Save</button></div>
92
+ </div>
93
+ <div class="card" id="defaults" data-async>
94
+ ${skSlot()}
95
+ <div class="hd"><h2>Defaults for new repositories</h2></div>
96
+ <div class="bd">
97
+ <div class="two" style="max-width:none">
98
+ <div class="field"><label>Months of pull requests</label>
99
+ <input id="def-months" value="${text(config.defaults?.maxPrMonths ?? "")}"></div>
100
+ <div class="field"><label>Commits</label>
101
+ <input id="def-commits" value="${text(config.defaults?.maxCommits ?? "")}"></div>
102
+ <div class="field"><label>Max lines per pull request</label>
103
+ <input id="def-lines" value="${text(config.defaults?.maxPullRequestChangeLines ?? "")}"></div>
104
+ </div>
105
+ </div>
106
+ <div class="ft"><button class="primary" id="save-def">Save</button></div>
107
+ </div>
108
+ <div class="card" id="server" data-async>
109
+ ${skSlot()}
110
+ <div class="hd"><h2>Job queue</h2></div>
111
+ <div class="bd">
112
+ <div class="field"><label>Max concurrent jobs</label>
113
+ <input id="max-jobs" value="${text(config.maxConcurrentJobs ?? "")}" placeholder="No limit">
114
+ <div class="hint">Cap how many background jobs run at once across init, remake, and review. Leave blank for no limit.</div></div>
115
+ <div class="two" style="max-width:none;margin-top:16px">
116
+ <div class="field"><label>Remote sync timeout (seconds)</label>
117
+ <input id="remote-timeout" value="${text(config.remoteSyncTimeoutSeconds ?? "")}" placeholder="10"></div>
118
+ <div class="field"><label>Max remote reviews per token</label>
119
+ <input id="remote-per-token" value="${text(config.maxConcurrentRemoteReviewsPerToken ?? "")}" placeholder="No limit"></div>
120
+ <div class="field"><label>Max tool output chars</label>
121
+ <input id="remote-tool-chars" value="${text(config.remoteToolOutputMaxChars ?? "")}" placeholder="500000"></div>
122
+ </div>
123
+ </div>
124
+ <div class="ft"><button class="primary" id="save-server">Save</button></div>
125
+ </div>
126
+ <div class="card" id="remote" data-async>
127
+ ${skSlot()}
128
+ <div class="hd"><h2>Remote review tokens</h2></div>
129
+ <div class="bd">
130
+ <p class="muted" style="margin:0 0 16px">Bearer tokens for <code>co-maintainer review --remote</code>. The secret is shown once when created.</p>
131
+ <div id="remote-token-list" class="muted">Loading…</div>
132
+ <div class="two" style="max-width:none;margin-top:16px">
133
+ <div class="field"><label>New token name</label>
134
+ <input id="remote-token-name" placeholder="e.g. laptop"></div>
135
+ </div>
136
+ </div>
137
+ <div class="ft">
138
+ <button class="primary" id="create-remote-token">Create token</button>
139
+ </div>
140
+ </div>
141
+ <div class="card" id="access" data-async>
142
+ ${skSlot()}
143
+ <div class="hd"><h2>Sign-in</h2></div>
144
+ <div class="bd">
145
+ <p class="muted" style="margin:0 0 16px">A <code>--disable-auth</code>/<code>--enable-auth</code> flag on <code>serve</code> overrides these for that run.</p>
146
+ <div class="field wide"><label><input type="checkbox" id="password-auth"${config.passwordAuthDisabled ? "" : " checked"}> Password sign-in</label>
147
+ <div class="hint">Set with <code>--password</code> when you start serve, or printed to the console if you omit the flag.</div></div>
148
+ <div class="field wide"><label><input type="checkbox" id="github-auth"${config.githubAuthEnabled ? " checked" : ""}> GitHub sign-in</label></div>
149
+ <div class="two" style="max-width:none">
150
+ <div class="field"><label>OAuth client ID</label>
151
+ <input id="oauth-client-id" value="${text(config.githubOAuthClientId ?? "")}"></div>
152
+ <div class="field"><label>OAuth client secret</label>
153
+ <input id="oauth-client-secret" type="password" placeholder="${config.githubOAuthClientSecret
154
+ ? "Leave blank to keep the current secret"
155
+ : ""}"></div>
156
+ </div>
157
+ <div class="field wide"><label>Allowed GitHub username</label>
158
+ <input id="oauth-allowed-user" value="${text(config.githubOAuthAllowedUser ?? "")}">
159
+ <div class="hint">Only this GitHub account can sign in through OAuth.</div></div>
160
+ </div>
161
+ <div class="ft"><button class="primary" id="save-access">Save</button></div>
162
+ </div>
163
+ <div class="card" id="about">
164
+ <div class="hd"><h2>About</h2></div>
165
+ <div class="bd">
166
+ <table style="margin:-18px -18px 0;width:calc(100% + 36px)">
167
+ <tbody>
168
+ <tr><td style="width:200px" class="muted">Version</td>
169
+ <td>${text(VERSION)}</td></tr>
170
+ <tr><td class="muted">Repository</td>
171
+ <td><a href="${SOURCE_URL}">${text(SOURCE_URL.replace("https://", ""))}</a></td></tr>
172
+ <tr><td class="muted">Developed by</td>
173
+ <td>Murat Kirazkaya</td></tr>
174
+ </tbody>
175
+ </table>
176
+ </div>
177
+ </div>
178
+ </div>
179
+ </div>
180
+ <script>
181
+ function save(btn, body) {
182
+ run(btn, btn.closest("[data-async]"), async function() {
183
+ await api("PUT", "/api/settings", body);
184
+ location.reload();
185
+ });
186
+ }
187
+ document.getElementById("save-ai").addEventListener("click", function() {
188
+ save(this, {
189
+ ai: document.getElementById("ai").value,
190
+ token: document.getElementById("token").value,
191
+ highModel: document.getElementById("high").value,
192
+ lowModel: document.getElementById("low").value
193
+ });
194
+ });
195
+ document.getElementById("save-gh").addEventListener("click", function() {
196
+ save(this, {
197
+ auth: document.getElementById("auth").value,
198
+ githubPat: document.getElementById("pat").value
199
+ });
200
+ });
201
+ document.getElementById("save-app").addEventListener("click", function() {
202
+ save(this, {
203
+ githubAppId: document.getElementById("app-id").value,
204
+ githubAppPrivateKey: document.getElementById("app-key").value,
205
+ githubWebhookSecret: document.getElementById("hook-secret").value,
206
+ webhookUrl: document.getElementById("webhook-url").value
207
+ });
208
+ });
209
+ document.getElementById("save-access").addEventListener("click", function() {
210
+ save(this, {
211
+ passwordAuthDisabled: !document.getElementById("password-auth").checked,
212
+ githubAuthEnabled: document.getElementById("github-auth").checked,
213
+ githubOAuthClientId: document.getElementById("oauth-client-id").value,
214
+ githubOAuthClientSecret: document.getElementById("oauth-client-secret").value,
215
+ githubOAuthAllowedUser: document.getElementById("oauth-allowed-user").value
216
+ });
217
+ });
218
+ function positiveOrNull(raw, label) {
219
+ if (raw === "") return { ok: true, value: null };
220
+ var n = Number(raw);
221
+ if (!Number.isInteger(n) || n <= 0) {
222
+ return { ok: false, message: label + " must be a positive whole number, or blank." };
223
+ }
224
+ return { ok: true, value: n };
225
+ }
226
+ document.getElementById("save-server").addEventListener("click", function() {
227
+ var card = this.closest("[data-async]");
228
+ var jobs = positiveOrNull(document.getElementById("max-jobs").value.trim(), "Max concurrent jobs");
229
+ if (!jobs.ok) { fail(card, jobs.message, function () {}); return; }
230
+ var timeout = positiveOrNull(document.getElementById("remote-timeout").value.trim(), "Remote sync timeout");
231
+ if (!timeout.ok) { fail(card, timeout.message, function () {}); return; }
232
+ var perToken = positiveOrNull(document.getElementById("remote-per-token").value.trim(), "Max remote reviews per token");
233
+ if (!perToken.ok) { fail(card, perToken.message, function () {}); return; }
234
+ var toolChars = positiveOrNull(document.getElementById("remote-tool-chars").value.trim(), "Max tool output chars");
235
+ if (!toolChars.ok) { fail(card, toolChars.message, function () {}); return; }
236
+ save(this, {
237
+ maxConcurrentJobs: jobs.value,
238
+ remoteSyncTimeoutSeconds: timeout.value,
239
+ maxConcurrentRemoteReviewsPerToken: perToken.value,
240
+ remoteToolOutputMaxChars: toolChars.value
241
+ });
242
+ });
243
+ function escHtml(s) {
244
+ return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
245
+ }
246
+ async function loadRemoteTokens() {
247
+ var el = document.getElementById("remote-token-list");
248
+ try {
249
+ var rows = await api("GET", "/api/remote-tokens");
250
+ if (!rows.length) {
251
+ el.innerHTML = "<p>No tokens yet.</p>";
252
+ return;
253
+ }
254
+ el.innerHTML = "<table><thead><tr><th>Name</th><th>Status</th><th class=\\"num\\">Reviews (30d)</th><th class=\\"num\\">Cost (30d)</th><th></th></tr></thead><tbody>" +
255
+ rows.map(function(r) {
256
+ return "<tr><td>" + escHtml(r.name) + "</td><td>" + (r.active ? "Active" : "Inactive") + "</td>" +
257
+ "<td class=\\"num\\">" + r.reviews30d + "</td><td class=\\"num\\">" + r.cost30d + "</td>" +
258
+ "<td><button type=\\"button\\" class=\\"btn sm\\" data-token-id=\\"" + escHtml(r.id) + "\\" data-active=\\"" + r.active + "\\">" +
259
+ (r.active ? "Deactivate" : "Activate") + "</button> " +
260
+ "<button type=\\"button\\" class=\\"btn sm\\" data-delete-token=\\"" + escHtml(r.id) + "\\">Delete</button></td></tr>";
261
+ }).join("") + "</tbody></table>";
262
+ el.querySelectorAll("[data-token-id]").forEach(function(btn) {
263
+ btn.addEventListener("click", function() {
264
+ var id = btn.getAttribute("data-token-id");
265
+ var active = btn.getAttribute("data-active") === "true";
266
+ run(btn, cardFor(btn), async function() {
267
+ await api("PATCH", "/api/remote-tokens/" + id, { active: !active });
268
+ await loadRemoteTokens();
269
+ });
270
+ });
271
+ });
272
+ el.querySelectorAll("[data-delete-token]").forEach(function(btn) {
273
+ btn.addEventListener("click", function() {
274
+ if (!confirm("Delete this token? In-flight remote reviews will be canceled.")) return;
275
+ var id = btn.getAttribute("data-delete-token");
276
+ run(btn, cardFor(btn), async function() {
277
+ await api("DELETE", "/api/remote-tokens/" + id);
278
+ await loadRemoteTokens();
279
+ });
280
+ });
281
+ });
282
+ } catch (e) {
283
+ el.textContent = "Could not load tokens.";
284
+ }
285
+ }
286
+ function cardFor(btn) { return btn.closest("[data-async]"); }
287
+ loadRemoteTokens();
288
+ document.getElementById("create-remote-token").addEventListener("click", function() {
289
+ var name = document.getElementById("remote-token-name").value.trim();
290
+ if (!name) {
291
+ fail(this.closest("[data-async]"), "Enter a name for the token.", function () {});
292
+ return;
293
+ }
294
+ run(this, this.closest("[data-async]"), async function() {
295
+ var created = await api("POST", "/api/remote-tokens", { name: name });
296
+ prompt("Copy this token now — it will not be shown again:", created.token);
297
+ document.getElementById("remote-token-name").value = "";
298
+ await loadRemoteTokens();
299
+ });
300
+ });
301
+ document.getElementById("save-def").addEventListener("click", function() {
302
+ var card = this.closest("[data-async]");
303
+ var positiveInt = function(id, label) {
304
+ var v = document.getElementById(id).value.trim();
305
+ if (v === "") return { ok: true, value: null };
306
+ var n = Number(v);
307
+ if (!Number.isInteger(n) || n <= 0) {
308
+ return { ok: false, message: label + " must be a positive whole number, or blank." };
309
+ }
310
+ return { ok: true, value: n };
311
+ };
312
+ var months = positiveInt("def-months", "Max PR age (months)");
313
+ if (!months.ok) {
314
+ fail(card, months.message, function () {});
315
+ return;
316
+ }
317
+ var commits = positiveInt("def-commits", "Max commits");
318
+ if (!commits.ok) {
319
+ fail(card, commits.message, function () {});
320
+ return;
321
+ }
322
+ var lines = positiveInt("def-lines", "Max changed lines");
323
+ if (!lines.ok) {
324
+ fail(card, lines.message, function () {});
325
+ return;
326
+ }
327
+ save(this, {
328
+ defaults: {
329
+ maxPrMonths: months.value,
330
+ maxCommits: commits.value,
331
+ maxPullRequestChangeLines: lines.value
332
+ }
333
+ });
334
+ });
335
+ </script>`,
336
+ }));
337
+ }
@@ -0,0 +1,5 @@
1
+ export declare function renderSetup(username: string, setup: {
2
+ ai: boolean;
3
+ github: boolean;
4
+ app: boolean;
5
+ }): Response;
@@ -0,0 +1,38 @@
1
+ import { html, layout, text } from "./layout.js";
2
+ export function renderSetup(username, setup) {
3
+ const row = (done, now, n, title, detail) => `<li class="${done ? "done" : now ? "now" : ""}">
4
+ <span class="n">${done ? "✓" : n}</span>
5
+ <div class="body">
6
+ <b>${title}</b>
7
+ <span class="muted">${text(detail)}</span>
8
+ </div>
9
+ <a class="btn sm" href="/settings">Change</a>
10
+ </li>`;
11
+ const aiDetail = setup.ai
12
+ ? "An AI provider and key are saved"
13
+ : "Add a provider and API key in Settings";
14
+ const ghDetail = setup.github
15
+ ? "GitHub access is configured"
16
+ : "Use gh or a personal access token";
17
+ const appDetail = setup.app
18
+ ? "The GitHub App is configured"
19
+ : "Needed to post reviews on pull requests";
20
+ return html(layout({
21
+ title: "Get started · co-maintainer",
22
+ username,
23
+ body: `<div class="wrap" style="max-width:660px">
24
+ <div class="pagehead">
25
+ <div><h1>Get started</h1>
26
+ <p class="lead">Two things now, then add your first repository.</p></div>
27
+ </div>
28
+ <div class="card"><div class="bd">
29
+ <ul class="steps">
30
+ ${row(setup.ai, !setup.ai, "1", "Models and API key", aiDetail)}
31
+ ${row(setup.github, setup.ai && !setup.github, "2", "GitHub access", ghDetail)}
32
+ ${row(setup.app, setup.ai && setup.github && !setup.app, "3", "GitHub App", appDetail)}
33
+ </ul>
34
+ </div></div>
35
+ <p class="muted" style="font-size:13px">Ready? <a href="/repos/new">Add a repository</a>.</p>
36
+ </div>`,
37
+ }));
38
+ }
@@ -0,0 +1,2 @@
1
+ /** Generated from src/server/pages/styles.css by scripts/embed_assets.mjs — do not edit. */
2
+ export declare const styles = ":root {\n --bg: #f6f6f7;\n --card: #fff;\n --border: #e2e2e4;\n --border2: #ededef;\n --text: #1f2124;\n --muted: #6a6f75;\n --dim: #8b9096;\n --accent: #80c580;\n --accent-d: #6cae6c;\n --link: #0b5cd5;\n --green: #1a9d5a;\n --amber: #c98a00;\n --red: #cf3c3c;\n --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n}\n* {\n box-sizing: border-box;\n}\nbody {\n margin: 0;\n background: var(--bg);\n color: var(--text);\n font:\n 14px/1.5 -apple-system,\n BlinkMacSystemFont,\n \"Segoe UI\",\n Roboto,\n Helvetica,\n Arial,\n sans-serif;\n}\na {\n color: var(--link);\n text-decoration: none;\n}\na:hover {\n text-decoration: underline;\n}\ncode,\n.mono {\n font-family: var(--mono);\n font-size: 0.9em;\n}\n\n/* top bar */\n.top {\n background: #fff;\n border-bottom: 1px solid var(--border);\n}\n.top .in {\n max-width: 1100px;\n margin: 0 auto;\n padding: 0 24px;\n height: 56px;\n display: flex;\n align-items: center;\n gap: 12px;\n}\n.logo {\n display: flex;\n align-items: center;\n gap: 9px;\n font-weight: 600;\n font-size: 15px;\n color: var(--text);\n}\n.logo:hover {\n text-decoration: none;\n}\n.logo img {\n height: 32px;\n width: 32px;\n display: block;\n border-radius: 5px;\n image-rendering: pixelated;\n object-fit: cover;\n}\n.brand {\n width: 180px;\n height: auto;\n display: block;\n margin: 0 0 14px;\n border-radius: 8px;\n image-rendering: pixelated;\n}\n.top .right {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 18px;\n font-size: 13px;\n color: var(--muted);\n}\n\n/* layout */\n.wrap {\n max-width: 1100px;\n margin: 0 auto;\n padding: 28px 24px 72px;\n}\n.wrap.side {\n display: grid;\n grid-template-columns: 196px 1fr;\n gap: 32px;\n}\naside nav {\n display: flex;\n flex-direction: column;\n gap: 1px;\n position: sticky;\n top: 24px;\n}\naside nav a {\n padding: 7px 12px;\n border-radius: 4px;\n color: var(--text);\n font-size: 14px;\n}\naside nav a:hover {\n background: #ededef;\n text-decoration: none;\n}\naside nav a.on {\n background: #fff;\n box-shadow: inset 2px 0 0 var(--accent);\n border: 1px solid var(--border);\n font-weight: 500;\n}\naside .lbl {\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--dim);\n padding: 0 12px;\n margin: 0 0 8px;\n}\n\nh1 {\n font-size: 22px;\n font-weight: 600;\n margin: 0;\n}\nh2 {\n font-size: 15px;\n font-weight: 600;\n margin: 0;\n}\np.lead {\n color: var(--muted);\n margin: 6px 0 0;\n font-size: 14px;\n}\n.pagehead {\n display: flex;\n align-items: center;\n gap: 16px;\n margin-bottom: 22px;\n}\n.pagehead .actions {\n margin-left: auto;\n display: flex;\n gap: 8px;\n}\n\n.card {\n background: var(--card);\n border: 1px solid var(--border);\n border-radius: 6px;\n margin-bottom: 18px;\n}\n.card .hd {\n padding: 14px 18px;\n border-bottom: 1px solid var(--border);\n display: flex;\n align-items: center;\n gap: 10px;\n}\n.card .bd {\n padding: 18px;\n}\n.card .bd.flush {\n padding: 0;\n}\n.card .ft {\n padding: 12px 18px;\n border-top: 1px solid var(--border);\n background: #fbfbfc;\n display: flex;\n align-items: center;\n gap: 10px;\n font-size: 13px;\n color: var(--muted);\n border-radius: 0 0 6px 6px;\n}\n\n/* table */\ntable {\n width: 100%;\n border-collapse: collapse;\n}\nth {\n text-align: left;\n font-size: 12px;\n font-weight: 600;\n color: var(--muted);\n padding: 10px 18px;\n border-bottom: 1px solid var(--border);\n background: #fbfbfc;\n}\ntd {\n padding: 14px 18px;\n border-bottom: 1px solid var(--border2);\n font-size: 14px;\n}\ntr:last-child td {\n border-bottom: none;\n}\ntbody tr:hover {\n background: #fafafb;\n}\n.num {\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n/* status text, not badges */\n.st {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 13px;\n}\n.st::before {\n content: \"\";\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: var(--dim);\n}\n.st.ok::before {\n background: var(--green);\n}\n.st.warn::before {\n background: var(--amber);\n}\n.st.err::before {\n background: var(--red);\n}\n.st.run::before {\n background: var(--link);\n}\n\n/* buttons */\nbutton,\n.btn {\n font: inherit;\n font-size: 14px;\n padding: 6px 14px;\n border-radius: 4px;\n border: 1px solid var(--border);\n background: #fff;\n color: var(--text);\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n gap: 7px;\n}\nbutton:hover,\n.btn:hover {\n background: #f4f4f5;\n text-decoration: none;\n}\n.btn.primary,\nbutton.primary {\n background: var(--accent);\n border-color: var(--accent);\n color: #fff;\n font-weight: 500;\n}\n.btn.primary:hover,\nbutton.primary:hover {\n background: var(--accent-d);\n border-color: var(--accent-d);\n}\n.btn.sm {\n padding: 4px 10px;\n font-size: 13px;\n}\n.btn.link {\n border: none;\n background: none;\n color: var(--link);\n padding: 0;\n}\n.btn.link:hover {\n background: none;\n text-decoration: underline;\n}\nbutton[disabled] {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n/* toggle */\n.tg {\n position: relative;\n width: 38px;\n height: 22px;\n border-radius: 22px;\n background: #cfd2d6;\n border: none;\n padding: 0;\n cursor: pointer;\n transition: background 0.12s ease;\n}\n.tg::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 18px;\n height: 18px;\n border-radius: 50%;\n background: #fff;\n transition: left 0.12s ease;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);\n}\n.tg.on {\n background: var(--accent);\n}\n.tg.on::after {\n left: 18px;\n}\n.tg:hover {\n filter: brightness(0.97);\n}\n\n/* forms */\nlabel {\n display: block;\n font-size: 13px;\n font-weight: 500;\n margin-bottom: 6px;\n}\ninput,\nselect,\ntextarea {\n width: 100%;\n background: #fff;\n border: 1px solid #cfd2d6;\n border-radius: 4px;\n color: var(--text);\n padding: 8px 10px;\n font: inherit;\n font-size: 14px;\n}\ninput:focus,\nselect:focus,\ntextarea:focus {\n outline: none;\n border-color: var(--accent);\n box-shadow: 0 0 0 2px rgba(128, 197, 128, 0.35);\n}\ntextarea {\n resize: vertical;\n line-height: 1.35;\n}\n.pem {\n min-height: 150px;\n}\n.field {\n margin-bottom: 18px;\n max-width: 420px;\n}\n.field.wide {\n max-width: none;\n}\n.hint {\n font-size: 13px;\n color: var(--muted);\n margin-top: 6px;\n font-weight: 400;\n}\n.two {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 0 24px;\n}\n\n/* bits */\n.muted {\n color: var(--muted);\n}\n.dim {\n color: var(--dim);\n}\n.big {\n font-size: 26px;\n font-weight: 600;\n letter-spacing: -0.01em;\n}\n.kfig {\n display: flex;\n gap: 44px;\n}\n.kfig .k {\n font-size: 13px;\n color: var(--muted);\n margin-bottom: 2px;\n}\n.notice {\n background: #f3faf3;\n border: 1px solid #cfe6cf;\n border-radius: 6px;\n padding: 14px 18px;\n display: flex;\n align-items: center;\n gap: 14px;\n margin-bottom: 18px;\n}\n.notice.info {\n background: #f2f7ff;\n border-color: #cfe0fb;\n}\n.notice.bad {\n background: #fdf3f3;\n border-color: #f3d0d0;\n}\n.notice .txt {\n flex: 1;\n}\n.notice b {\n font-weight: 600;\n}\n.notice .dismiss-worth {\n margin-left: auto;\n border: 0;\n background: transparent;\n color: var(--muted);\n padding: 0 4px;\n font-size: 20px;\n line-height: 1;\n cursor: pointer;\n}\n.notice .dismiss-worth:hover {\n background: transparent;\n color: var(--text);\n}\n.markdown > :first-child {\n margin-top: 0;\n}\n.markdown > :last-child {\n margin-bottom: 0;\n}\n.markdown p,\n.markdown h1,\n.markdown h2,\n.markdown h3,\n.markdown ul,\n.markdown ol,\n.markdown pre {\n margin: 0 0 12px;\n}\n.markdown h1,\n.markdown h2,\n.markdown h3 {\n font-size: 15px;\n}\n.markdown ul,\n.markdown ol {\n padding-left: 22px;\n}\n.markdown pre {\n overflow-x: auto;\n padding: 10px 12px;\n border-radius: 4px;\n background: #f1f2f3;\n font-family: var(--mono);\n font-size: 13px;\n white-space: pre;\n}\n.markdown code {\n padding: 1px 4px;\n border-radius: 3px;\n background: #f1f2f3;\n}\n.markdown pre code {\n padding: 0;\n background: transparent;\n}\n.log {\n background: #1f2124;\n color: #e6e6e6;\n border-radius: 6px;\n padding: 14px 16px;\n font-family: var(--mono);\n font-size: 12.5px;\n line-height: 1.7;\n max-height: 320px;\n overflow: auto;\n white-space: pre-wrap;\n}\n.log .t {\n color: #8b9096;\n}\n.crumbs {\n font-size: 13px;\n color: var(--muted);\n margin-bottom: 14px;\n}\n.pager {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 12px 18px;\n border-top: 1px solid var(--border);\n font-size: 13px;\n color: var(--muted);\n}\n.pager .sp {\n margin-left: auto;\n}\n.empty {\n padding: 56px 24px;\n text-align: center;\n color: var(--muted);\n}\n.empty h2 {\n margin-bottom: 6px;\n}\n.steps {\n list-style: none;\n padding: 0;\n margin: 0;\n}\n.steps li {\n display: flex;\n gap: 14px;\n padding: 16px 0;\n border-bottom: 1px solid var(--border2);\n}\n.steps li:last-child {\n border-bottom: none;\n}\n.steps .n {\n width: 24px;\n height: 24px;\n border-radius: 50%;\n background: #ededef;\n color: var(--muted);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 12px;\n font-weight: 600;\n flex: none;\n}\n.steps li.done .n {\n background: var(--green);\n color: #fff;\n}\n.steps li.now .n {\n background: var(--accent);\n color: #fff;\n}\n.steps .body {\n flex: 1;\n}\n.steps .body b {\n display: block;\n font-weight: 600;\n margin-bottom: 2px;\n}\n\nbutton,\n.btn,\n.tg,\n.tg::after,\ntbody tr {\n transition:\n background 0.12s ease,\n border-color 0.12s ease,\n filter 0.12s ease,\n left 0.12s ease,\n color 0.12s ease;\n}\n:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 2px;\n}\n\n#toasts {\n position: fixed;\n right: 20px;\n bottom: 20px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n z-index: 20;\n max-width: 360px;\n}\n.toast {\n background: #1f2124;\n color: #fff;\n border-radius: 6px;\n padding: 10px 14px;\n font-size: 13px;\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);\n}\n.fail {\n margin: 12px 18px;\n padding: 12px 14px;\n background: #fdf3f3;\n border: 1px solid #f3d0d0;\n border-radius: 6px;\n display: flex;\n align-items: center;\n gap: 12px;\n font-size: 13px;\n}\n.fail p {\n margin: 0;\n flex: 1;\n}\n\n.sk-slot {\n display: none;\n padding: 18px;\n}\n.sk,\n.sk-row {\n height: 12px;\n background: #ededef;\n border-radius: 4px;\n margin-bottom: 10px;\n}\n.sk-row {\n height: 36px;\n}\n[data-async].busy > :not(.sk-slot):not([data-fail]) {\n visibility: hidden;\n}\n[data-async].busy > .sk-slot {\n display: block !important;\n visibility: visible;\n}\n\n@media (prefers-reduced-motion: reduce) {\n *,\n *::before,\n *::after {\n animation: none !important;\n transition: none !important;\n }\n}\n.bars {\n display: flex;\n align-items: flex-end;\n gap: 2px;\n height: 56px;\n}\n.bars i {\n flex: 1 1 0;\n min-width: 2px;\n align-self: stretch;\n background-image: linear-gradient(\n to top,\n var(--bar, var(--accent)) var(--h),\n transparent var(--h)\n );\n}\n.bars i:hover {\n --bar: var(--accent-d);\n background-color: var(--border2);\n}\n.trend {\n font-size: 12px;\n margin-top: 3px;\n}\n.trend.up {\n color: var(--green);\n}\n.trend.down {\n color: var(--red);\n}\n.kfig.wrap4 {\n flex-wrap: wrap;\n gap: 28px 44px;\n}\n";
@@ -0,0 +1,2 @@
1
+ /** Generated from src/server/pages/styles.css by scripts/embed_assets.mjs — do not edit. */
2
+ export const styles = ":root {\n --bg: #f6f6f7;\n --card: #fff;\n --border: #e2e2e4;\n --border2: #ededef;\n --text: #1f2124;\n --muted: #6a6f75;\n --dim: #8b9096;\n --accent: #80c580;\n --accent-d: #6cae6c;\n --link: #0b5cd5;\n --green: #1a9d5a;\n --amber: #c98a00;\n --red: #cf3c3c;\n --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n}\n* {\n box-sizing: border-box;\n}\nbody {\n margin: 0;\n background: var(--bg);\n color: var(--text);\n font:\n 14px/1.5 -apple-system,\n BlinkMacSystemFont,\n \"Segoe UI\",\n Roboto,\n Helvetica,\n Arial,\n sans-serif;\n}\na {\n color: var(--link);\n text-decoration: none;\n}\na:hover {\n text-decoration: underline;\n}\ncode,\n.mono {\n font-family: var(--mono);\n font-size: 0.9em;\n}\n\n/* top bar */\n.top {\n background: #fff;\n border-bottom: 1px solid var(--border);\n}\n.top .in {\n max-width: 1100px;\n margin: 0 auto;\n padding: 0 24px;\n height: 56px;\n display: flex;\n align-items: center;\n gap: 12px;\n}\n.logo {\n display: flex;\n align-items: center;\n gap: 9px;\n font-weight: 600;\n font-size: 15px;\n color: var(--text);\n}\n.logo:hover {\n text-decoration: none;\n}\n.logo img {\n height: 32px;\n width: 32px;\n display: block;\n border-radius: 5px;\n image-rendering: pixelated;\n object-fit: cover;\n}\n.brand {\n width: 180px;\n height: auto;\n display: block;\n margin: 0 0 14px;\n border-radius: 8px;\n image-rendering: pixelated;\n}\n.top .right {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 18px;\n font-size: 13px;\n color: var(--muted);\n}\n\n/* layout */\n.wrap {\n max-width: 1100px;\n margin: 0 auto;\n padding: 28px 24px 72px;\n}\n.wrap.side {\n display: grid;\n grid-template-columns: 196px 1fr;\n gap: 32px;\n}\naside nav {\n display: flex;\n flex-direction: column;\n gap: 1px;\n position: sticky;\n top: 24px;\n}\naside nav a {\n padding: 7px 12px;\n border-radius: 4px;\n color: var(--text);\n font-size: 14px;\n}\naside nav a:hover {\n background: #ededef;\n text-decoration: none;\n}\naside nav a.on {\n background: #fff;\n box-shadow: inset 2px 0 0 var(--accent);\n border: 1px solid var(--border);\n font-weight: 500;\n}\naside .lbl {\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--dim);\n padding: 0 12px;\n margin: 0 0 8px;\n}\n\nh1 {\n font-size: 22px;\n font-weight: 600;\n margin: 0;\n}\nh2 {\n font-size: 15px;\n font-weight: 600;\n margin: 0;\n}\np.lead {\n color: var(--muted);\n margin: 6px 0 0;\n font-size: 14px;\n}\n.pagehead {\n display: flex;\n align-items: center;\n gap: 16px;\n margin-bottom: 22px;\n}\n.pagehead .actions {\n margin-left: auto;\n display: flex;\n gap: 8px;\n}\n\n.card {\n background: var(--card);\n border: 1px solid var(--border);\n border-radius: 6px;\n margin-bottom: 18px;\n}\n.card .hd {\n padding: 14px 18px;\n border-bottom: 1px solid var(--border);\n display: flex;\n align-items: center;\n gap: 10px;\n}\n.card .bd {\n padding: 18px;\n}\n.card .bd.flush {\n padding: 0;\n}\n.card .ft {\n padding: 12px 18px;\n border-top: 1px solid var(--border);\n background: #fbfbfc;\n display: flex;\n align-items: center;\n gap: 10px;\n font-size: 13px;\n color: var(--muted);\n border-radius: 0 0 6px 6px;\n}\n\n/* table */\ntable {\n width: 100%;\n border-collapse: collapse;\n}\nth {\n text-align: left;\n font-size: 12px;\n font-weight: 600;\n color: var(--muted);\n padding: 10px 18px;\n border-bottom: 1px solid var(--border);\n background: #fbfbfc;\n}\ntd {\n padding: 14px 18px;\n border-bottom: 1px solid var(--border2);\n font-size: 14px;\n}\ntr:last-child td {\n border-bottom: none;\n}\ntbody tr:hover {\n background: #fafafb;\n}\n.num {\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n/* status text, not badges */\n.st {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 13px;\n}\n.st::before {\n content: \"\";\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: var(--dim);\n}\n.st.ok::before {\n background: var(--green);\n}\n.st.warn::before {\n background: var(--amber);\n}\n.st.err::before {\n background: var(--red);\n}\n.st.run::before {\n background: var(--link);\n}\n\n/* buttons */\nbutton,\n.btn {\n font: inherit;\n font-size: 14px;\n padding: 6px 14px;\n border-radius: 4px;\n border: 1px solid var(--border);\n background: #fff;\n color: var(--text);\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n gap: 7px;\n}\nbutton:hover,\n.btn:hover {\n background: #f4f4f5;\n text-decoration: none;\n}\n.btn.primary,\nbutton.primary {\n background: var(--accent);\n border-color: var(--accent);\n color: #fff;\n font-weight: 500;\n}\n.btn.primary:hover,\nbutton.primary:hover {\n background: var(--accent-d);\n border-color: var(--accent-d);\n}\n.btn.sm {\n padding: 4px 10px;\n font-size: 13px;\n}\n.btn.link {\n border: none;\n background: none;\n color: var(--link);\n padding: 0;\n}\n.btn.link:hover {\n background: none;\n text-decoration: underline;\n}\nbutton[disabled] {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n/* toggle */\n.tg {\n position: relative;\n width: 38px;\n height: 22px;\n border-radius: 22px;\n background: #cfd2d6;\n border: none;\n padding: 0;\n cursor: pointer;\n transition: background 0.12s ease;\n}\n.tg::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 18px;\n height: 18px;\n border-radius: 50%;\n background: #fff;\n transition: left 0.12s ease;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);\n}\n.tg.on {\n background: var(--accent);\n}\n.tg.on::after {\n left: 18px;\n}\n.tg:hover {\n filter: brightness(0.97);\n}\n\n/* forms */\nlabel {\n display: block;\n font-size: 13px;\n font-weight: 500;\n margin-bottom: 6px;\n}\ninput,\nselect,\ntextarea {\n width: 100%;\n background: #fff;\n border: 1px solid #cfd2d6;\n border-radius: 4px;\n color: var(--text);\n padding: 8px 10px;\n font: inherit;\n font-size: 14px;\n}\ninput:focus,\nselect:focus,\ntextarea:focus {\n outline: none;\n border-color: var(--accent);\n box-shadow: 0 0 0 2px rgba(128, 197, 128, 0.35);\n}\ntextarea {\n resize: vertical;\n line-height: 1.35;\n}\n.pem {\n min-height: 150px;\n}\n.field {\n margin-bottom: 18px;\n max-width: 420px;\n}\n.field.wide {\n max-width: none;\n}\n.hint {\n font-size: 13px;\n color: var(--muted);\n margin-top: 6px;\n font-weight: 400;\n}\n.two {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 0 24px;\n}\n\n/* bits */\n.muted {\n color: var(--muted);\n}\n.dim {\n color: var(--dim);\n}\n.big {\n font-size: 26px;\n font-weight: 600;\n letter-spacing: -0.01em;\n}\n.kfig {\n display: flex;\n gap: 44px;\n}\n.kfig .k {\n font-size: 13px;\n color: var(--muted);\n margin-bottom: 2px;\n}\n.notice {\n background: #f3faf3;\n border: 1px solid #cfe6cf;\n border-radius: 6px;\n padding: 14px 18px;\n display: flex;\n align-items: center;\n gap: 14px;\n margin-bottom: 18px;\n}\n.notice.info {\n background: #f2f7ff;\n border-color: #cfe0fb;\n}\n.notice.bad {\n background: #fdf3f3;\n border-color: #f3d0d0;\n}\n.notice .txt {\n flex: 1;\n}\n.notice b {\n font-weight: 600;\n}\n.notice .dismiss-worth {\n margin-left: auto;\n border: 0;\n background: transparent;\n color: var(--muted);\n padding: 0 4px;\n font-size: 20px;\n line-height: 1;\n cursor: pointer;\n}\n.notice .dismiss-worth:hover {\n background: transparent;\n color: var(--text);\n}\n.markdown > :first-child {\n margin-top: 0;\n}\n.markdown > :last-child {\n margin-bottom: 0;\n}\n.markdown p,\n.markdown h1,\n.markdown h2,\n.markdown h3,\n.markdown ul,\n.markdown ol,\n.markdown pre {\n margin: 0 0 12px;\n}\n.markdown h1,\n.markdown h2,\n.markdown h3 {\n font-size: 15px;\n}\n.markdown ul,\n.markdown ol {\n padding-left: 22px;\n}\n.markdown pre {\n overflow-x: auto;\n padding: 10px 12px;\n border-radius: 4px;\n background: #f1f2f3;\n font-family: var(--mono);\n font-size: 13px;\n white-space: pre;\n}\n.markdown code {\n padding: 1px 4px;\n border-radius: 3px;\n background: #f1f2f3;\n}\n.markdown pre code {\n padding: 0;\n background: transparent;\n}\n.log {\n background: #1f2124;\n color: #e6e6e6;\n border-radius: 6px;\n padding: 14px 16px;\n font-family: var(--mono);\n font-size: 12.5px;\n line-height: 1.7;\n max-height: 320px;\n overflow: auto;\n white-space: pre-wrap;\n}\n.log .t {\n color: #8b9096;\n}\n.crumbs {\n font-size: 13px;\n color: var(--muted);\n margin-bottom: 14px;\n}\n.pager {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 12px 18px;\n border-top: 1px solid var(--border);\n font-size: 13px;\n color: var(--muted);\n}\n.pager .sp {\n margin-left: auto;\n}\n.empty {\n padding: 56px 24px;\n text-align: center;\n color: var(--muted);\n}\n.empty h2 {\n margin-bottom: 6px;\n}\n.steps {\n list-style: none;\n padding: 0;\n margin: 0;\n}\n.steps li {\n display: flex;\n gap: 14px;\n padding: 16px 0;\n border-bottom: 1px solid var(--border2);\n}\n.steps li:last-child {\n border-bottom: none;\n}\n.steps .n {\n width: 24px;\n height: 24px;\n border-radius: 50%;\n background: #ededef;\n color: var(--muted);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 12px;\n font-weight: 600;\n flex: none;\n}\n.steps li.done .n {\n background: var(--green);\n color: #fff;\n}\n.steps li.now .n {\n background: var(--accent);\n color: #fff;\n}\n.steps .body {\n flex: 1;\n}\n.steps .body b {\n display: block;\n font-weight: 600;\n margin-bottom: 2px;\n}\n\nbutton,\n.btn,\n.tg,\n.tg::after,\ntbody tr {\n transition:\n background 0.12s ease,\n border-color 0.12s ease,\n filter 0.12s ease,\n left 0.12s ease,\n color 0.12s ease;\n}\n:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 2px;\n}\n\n#toasts {\n position: fixed;\n right: 20px;\n bottom: 20px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n z-index: 20;\n max-width: 360px;\n}\n.toast {\n background: #1f2124;\n color: #fff;\n border-radius: 6px;\n padding: 10px 14px;\n font-size: 13px;\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);\n}\n.fail {\n margin: 12px 18px;\n padding: 12px 14px;\n background: #fdf3f3;\n border: 1px solid #f3d0d0;\n border-radius: 6px;\n display: flex;\n align-items: center;\n gap: 12px;\n font-size: 13px;\n}\n.fail p {\n margin: 0;\n flex: 1;\n}\n\n.sk-slot {\n display: none;\n padding: 18px;\n}\n.sk,\n.sk-row {\n height: 12px;\n background: #ededef;\n border-radius: 4px;\n margin-bottom: 10px;\n}\n.sk-row {\n height: 36px;\n}\n[data-async].busy > :not(.sk-slot):not([data-fail]) {\n visibility: hidden;\n}\n[data-async].busy > .sk-slot {\n display: block !important;\n visibility: visible;\n}\n\n@media (prefers-reduced-motion: reduce) {\n *,\n *::before,\n *::after {\n animation: none !important;\n transition: none !important;\n }\n}\n.bars {\n display: flex;\n align-items: flex-end;\n gap: 2px;\n height: 56px;\n}\n.bars i {\n flex: 1 1 0;\n min-width: 2px;\n align-self: stretch;\n background-image: linear-gradient(\n to top,\n var(--bar, var(--accent)) var(--h),\n transparent var(--h)\n );\n}\n.bars i:hover {\n --bar: var(--accent-d);\n background-color: var(--border2);\n}\n.trend {\n font-size: 12px;\n margin-top: 3px;\n}\n.trend.up {\n color: var(--green);\n}\n.trend.down {\n color: var(--red);\n}\n.kfig.wrap4 {\n flex-wrap: wrap;\n gap: 28px 44px;\n}\n";
@@ -0,0 +1 @@
1
+ export declare function handleWebhookRequest(request: Request, secret?: string): Promise<Response>;
@@ -0,0 +1,64 @@
1
+ import { readConfig } from "../../config.js";
2
+ import { errorResponse } from "../errors.js";
3
+ import { hasDelivery, recordDelivery } from "../../store/deliveries.js";
4
+ import { dispatchGithubEvent } from "../../services/webhook.js";
5
+ import { verifySignature } from "./signature.js";
6
+ function json(status, body) {
7
+ return Response.json(body, { status });
8
+ }
9
+ export async function handleWebhookRequest(request, secret) {
10
+ const raw = new Uint8Array(await request.arrayBuffer());
11
+ if (secret) {
12
+ const header = request.headers.get("x-hub-signature-256");
13
+ if (!(await verifySignature(secret, raw, header))) {
14
+ return errorResponse(401, "unauthorized", "bad webhook signature");
15
+ }
16
+ }
17
+ const deliveryId = request.headers.get("x-github-delivery");
18
+ const event = request.headers.get("x-github-event");
19
+ if (!deliveryId || !event) {
20
+ return errorResponse(400, "bad_request", "x-github-delivery and x-github-event are required");
21
+ }
22
+ if (hasDelivery(deliveryId)) {
23
+ return json(200, { outcome: "duplicate" });
24
+ }
25
+ let payload;
26
+ try {
27
+ payload = JSON.parse(new TextDecoder().decode(raw));
28
+ }
29
+ catch {
30
+ return errorResponse(400, "bad_request", "body is not JSON");
31
+ }
32
+ if (payload === null ||
33
+ typeof payload !== "object" ||
34
+ Array.isArray(payload)) {
35
+ return errorResponse(400, "bad_request", "body is not a JSON object");
36
+ }
37
+ const config = readConfig();
38
+ const repoFullName = payload.repository?.full_name;
39
+ const repoName = typeof repoFullName === "string" ? repoFullName : undefined;
40
+ const repoConfig = repoName ? config.repos?.[repoName] : undefined;
41
+ const maxDiffLines = repoConfig?.maxPullRequestChangeLines ??
42
+ config.defaults?.maxPullRequestChangeLines;
43
+ let result;
44
+ try {
45
+ result = dispatchGithubEvent(event, payload, deliveryId, maxDiffLines);
46
+ }
47
+ catch (error) {
48
+ result = { outcome: "error", reason: String(error) };
49
+ }
50
+ recordDelivery({
51
+ deliveryId,
52
+ event,
53
+ action: typeof payload.action === "string" ? payload.action : undefined,
54
+ repo: result.repo,
55
+ prNumber: result.prNumber,
56
+ outcome: result.outcome,
57
+ reason: result.reason,
58
+ });
59
+ return json(200, {
60
+ outcome: result.outcome,
61
+ ...(result.reason ? { reason: result.reason } : {}),
62
+ ...(result.jobId ? { jobId: result.jobId } : {}),
63
+ });
64
+ }
@@ -0,0 +1,7 @@
1
+ /** GitHub's `x-hub-signature-256` is `sha256=` plus hex HMAC-SHA256 of the
2
+ * raw body. Compared in constant time so a mismatch does not leak which
3
+ * byte differed. */
4
+ export declare function hmacSha256Hex(secret: string, body: Uint8Array): Promise<string>;
5
+ /** `false` when the header is missing, malformed, or the digest does not
6
+ * match. Callers skip this entirely when no webhook secret is configured. */
7
+ export declare function verifySignature(secret: string, body: Uint8Array, header: string | null): Promise<boolean>;
@@ -0,0 +1,41 @@
1
+ /** GitHub's `x-hub-signature-256` is `sha256=` plus hex HMAC-SHA256 of the
2
+ * raw body. Compared in constant time so a mismatch does not leak which
3
+ * byte differed. */
4
+ function hexToBytes(hex) {
5
+ if (hex.length % 2 !== 0)
6
+ return undefined;
7
+ const out = new Uint8Array(hex.length / 2);
8
+ for (let i = 0; i < out.length; i++) {
9
+ const n = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
10
+ if (Number.isNaN(n))
11
+ return undefined;
12
+ out[i] = n;
13
+ }
14
+ return out;
15
+ }
16
+ function timingSafeEqual(a, b) {
17
+ if (a.byteLength !== b.byteLength)
18
+ return false;
19
+ let diff = 0;
20
+ for (let i = 0; i < a.byteLength; i++)
21
+ diff |= a[i] ^ b[i];
22
+ return diff === 0;
23
+ }
24
+ export async function hmacSha256Hex(secret, body) {
25
+ const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
26
+ const sig = new Uint8Array(await crypto.subtle.sign("HMAC", key, body));
27
+ return Array.from(sig, (byte) => byte.toString(16).padStart(2, "0")).join("");
28
+ }
29
+ /** `false` when the header is missing, malformed, or the digest does not
30
+ * match. Callers skip this entirely when no webhook secret is configured. */
31
+ export async function verifySignature(secret, body, header) {
32
+ if (!header?.startsWith("sha256="))
33
+ return false;
34
+ const expected = hexToBytes(header.slice("sha256=".length));
35
+ if (!expected)
36
+ return false;
37
+ const actual = hexToBytes(await hmacSha256Hex(secret, body));
38
+ if (!actual)
39
+ return false;
40
+ return timingSafeEqual(actual, expected);
41
+ }
@@ -0,0 +1,19 @@
1
+ export type AccessOk = {
2
+ ok: true;
3
+ login?: string;
4
+ installations?: number;
5
+ };
6
+ export type AccessFail = {
7
+ ok: false;
8
+ message: string;
9
+ };
10
+ export type AccessResult = AccessOk | AccessFail;
11
+ export declare function missingAppPermissions(permissions: Record<string, string> | undefined): string[];
12
+ export declare function testGithubAccess(opts: {
13
+ auth: "gh" | "pat";
14
+ githubPat?: string;
15
+ }): Promise<AccessResult>;
16
+ export declare function testAppAccess(opts: {
17
+ appId: string;
18
+ privateKeyPem: string;
19
+ }): Promise<AccessResult>;