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,230 @@
1
+ import { safeCopy } from "../../util/redact.js";
2
+ import { relativeTime } from "../../util/time.js";
3
+ export function escapeHtml(value) {
4
+ return String(value ?? "").replace(/[&<>"']/g, (char) => ({
5
+ "&": "&amp;",
6
+ "<": "&lt;",
7
+ ">": "&gt;",
8
+ '"': "&quot;",
9
+ "'": "&#39;",
10
+ })[char] ?? char);
11
+ }
12
+ export function text(value) {
13
+ return escapeHtml(safeCopy(String(value ?? "")));
14
+ }
15
+ function markdownInline(value) {
16
+ const slots = [];
17
+ const slot = (html) => {
18
+ const index = slots.push(html) - 1;
19
+ return `\uE000${index}\uE001`;
20
+ };
21
+ let result = escapeHtml(value);
22
+ result = result.replace(/`([^`\n]+)`/g, (_, code) => slot(`<code>${code}</code>`));
23
+ result = result.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, (_, label, href) => slot(`<a href="${href}" rel="noreferrer">${label}</a>`));
24
+ result = result.replace(/\*\*([^*\n]+)\*\*/g, "<strong>$1</strong>");
25
+ result = result.replace(/__([^_\n]+)__/g, "<strong>$1</strong>");
26
+ result = result.replace(/\*([^*\n]+)\*/g, "<em>$1</em>");
27
+ result = result.replace(/_([^_\n]+)_/g, "<em>$1</em>");
28
+ result = result.replace(/\uE000(\d+)\uE001/g, (_, index) => slots[Number(index)]);
29
+ return result;
30
+ }
31
+ export function markdown(value) {
32
+ const lines = safeCopy(String(value ?? ""))
33
+ .replace(/\r\n?/g, "\n")
34
+ .split("\n");
35
+ const blocks = [];
36
+ let paragraph = [];
37
+ let list = [];
38
+ let listKind;
39
+ let code;
40
+ const flushParagraph = () => {
41
+ if (paragraph.length) {
42
+ blocks.push(`<p>${markdownInline(paragraph.join("\n"))}</p>`);
43
+ paragraph = [];
44
+ }
45
+ };
46
+ const flushList = () => {
47
+ if (!list.length || !listKind)
48
+ return;
49
+ blocks.push(`<${listKind}>${list.join("")}</${listKind}>`);
50
+ list = [];
51
+ listKind = undefined;
52
+ };
53
+ const flushText = () => {
54
+ flushParagraph();
55
+ flushList();
56
+ };
57
+ for (const line of lines) {
58
+ const fence = line.match(/^```(?:[A-Za-z0-9_-]+)?\s*$/);
59
+ if (fence) {
60
+ flushText();
61
+ if (code) {
62
+ blocks.push(`<pre><code>${escapeHtml(code.join("\n"))}</code></pre>`);
63
+ code = undefined;
64
+ }
65
+ else {
66
+ code = [];
67
+ }
68
+ continue;
69
+ }
70
+ if (code) {
71
+ code.push(line);
72
+ continue;
73
+ }
74
+ if (!line.trim()) {
75
+ flushText();
76
+ continue;
77
+ }
78
+ const heading = line.match(/^(#{1,3})\s+(.+)$/);
79
+ if (heading) {
80
+ flushText();
81
+ const level = heading[1].length;
82
+ blocks.push(`<h${level}>${markdownInline(heading[2])}</h${level}>`);
83
+ continue;
84
+ }
85
+ const bullet = line.match(/^\s*[-*+]\s+(.+)$/);
86
+ const ordered = line.match(/^\s*\d+[.)]\s+(.+)$/);
87
+ if (bullet || ordered) {
88
+ const kind = bullet ? "ul" : "ol";
89
+ if (listKind && listKind !== kind)
90
+ flushList();
91
+ flushParagraph();
92
+ listKind = kind;
93
+ list.push(`<li>${markdownInline((bullet ?? ordered)[1])}</li>`);
94
+ continue;
95
+ }
96
+ flushList();
97
+ paragraph.push(line.trim());
98
+ }
99
+ if (code) {
100
+ blocks.push(`<pre><code>${escapeHtml(code.join("\n"))}</code></pre>`);
101
+ }
102
+ flushText();
103
+ return `<div class="markdown">${blocks.join("")}</div>`;
104
+ }
105
+ export function money(value) {
106
+ const n = Number(value || 0);
107
+ if (!Number.isFinite(n) || n === 0)
108
+ return "$0.00";
109
+ if (Math.abs(n) >= 0.01)
110
+ return `$${n.toFixed(2)}`;
111
+ return `$${n.toFixed(7).replace(/0+$/, "").replace(/\.$/, "")}`;
112
+ }
113
+ const compactNumber = new Intl.NumberFormat("en", {
114
+ notation: "compact",
115
+ maximumFractionDigits: 1,
116
+ });
117
+ export function count(value) {
118
+ const n = Number(value || 0);
119
+ if (!Number.isFinite(n))
120
+ return "0";
121
+ return n < 1000 ? String(Math.round(n)) : compactNumber.format(n);
122
+ }
123
+ export function duration(ms) {
124
+ const n = Number(ms || 0);
125
+ if (!Number.isFinite(n) || n <= 0)
126
+ return "n/a";
127
+ if (n < 1000)
128
+ return `${Math.round(n)}ms`;
129
+ const sec = Math.round(n / 1000);
130
+ if (sec < 60)
131
+ return `${sec}s`;
132
+ return `${Math.floor(sec / 60)}m ${sec % 60}s`;
133
+ }
134
+ /** Rendered only when the earlier window had something to compare against,
135
+ * so an empty first month stays quiet rather than claiming a jump. */
136
+ export function delta(change, days, lowerIsBetter = false) {
137
+ if (change === undefined || change === 0)
138
+ return "";
139
+ const better = lowerIsBetter ? change < 0 : change > 0;
140
+ const sign = change > 0 ? "+" : "";
141
+ return `<div class="trend ${better ? "up" : "down"}">${sign}${change}% vs previous ${days} days</div>`;
142
+ }
143
+ /** A flex row of divs beats pulling in a chart library for one series, and
144
+ * it stays readable when a range has a single day in it. Each column is full
145
+ * height with the bar painted as a gradient stop, because a bar sized by its
146
+ * own height leaves nothing to hover over on a quiet day. */
147
+ export function bars(items) {
148
+ if (items.length === 0)
149
+ return "";
150
+ const max = Math.max(...items.map((item) => item.value));
151
+ return `<div class="bars">${items
152
+ .map((item) => `<i title="${escapeHtml(item.label)}" style="--h:${max > 0 ? Math.max(2, (item.value / max) * 100).toFixed(1) : "2"}%"></i>`)
153
+ .join("")}</div>`;
154
+ }
155
+ /** GitHub truncates long responses, so a count that reached its ceiling is
156
+ * a floor rather than a total and has to read as one. */
157
+ export function capped(value, cap) {
158
+ return value >= cap ? `${cap}+` : String(value);
159
+ }
160
+ export function when(iso) {
161
+ if (!iso)
162
+ return "never";
163
+ const abs = new Date(iso);
164
+ const title = Number.isNaN(abs.getTime())
165
+ ? iso
166
+ : abs
167
+ .toISOString()
168
+ .replace("T", " ")
169
+ .replace(/\.\d{3}Z$/, " UTC");
170
+ return `<time datetime="${escapeHtml(iso)}" title="${escapeHtml(title)}">${text(relativeTime(iso))}</time>`;
171
+ }
172
+ export function skSlot(kind = "block") {
173
+ if (kind === "table") {
174
+ return `<div class="sk-slot" hidden><div class="sk-row"></div><div class="sk-row"></div><div class="sk-row"></div></div>`;
175
+ }
176
+ return `<div class="sk-slot" hidden><div class="sk"></div><div class="sk"></div></div>`;
177
+ }
178
+ export function layout(opts) {
179
+ const right = opts.username
180
+ ? `<div class="right">
181
+ <a href="/activity"${opts.active === "activity"
182
+ ? ` style="color:var(--text);font-weight:500"`
183
+ : ""}>Activity</a>
184
+ <a href="/analytics"${opts.active === "analytics"
185
+ ? ` style="color:var(--text);font-weight:500"`
186
+ : ""}>Usage</a>
187
+ <a href="/settings"${opts.active === "settings"
188
+ ? ` style="color:var(--text);font-weight:500"`
189
+ : ""}>Settings</a>
190
+ <span>${text(opts.username)}</span>
191
+ </div>`
192
+ : "";
193
+ 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
+ <div class="top"><div class="in">
196
+ <a class="logo" href="/"><img src="/logo.png" alt="">co-maintainer</a>
197
+ ${right}
198
+ </div></div>
199
+ ${opts.body}
200
+ <div id="toasts"></div>
201
+ <script src="/client.js"></script>
202
+ </body></html>`;
203
+ }
204
+ export function html(body, status = 200) {
205
+ return new Response(body, {
206
+ status,
207
+ headers: { "content-type": "text/html; charset=utf-8" },
208
+ });
209
+ }
210
+ export function repoNav(fullName, on) {
211
+ const short = text(fullName.split("/")[1] ?? fullName);
212
+ const href = `/repos/${fullName}`;
213
+ const item = (id, label, path) => `<a href="${path}"${on === id ? ` class="on"` : ""}>${label}</a>`;
214
+ return `<aside>
215
+ <p class="lbl">${short}</p>
216
+ <nav>
217
+ ${item("overview", "Overview", href)}
218
+ ${item("pulls", "Pull requests", `${href}/pulls`)}
219
+ ${item("remote", "Remote", `${href}/remote`)}
220
+ ${item("knowledge", "Knowledge", `${href}/knowledge`)}
221
+ ${item("settings", "Settings", `${href}/settings`)}
222
+ </nav>
223
+ </aside>`;
224
+ }
225
+ export function empty(title, lead) {
226
+ return `<div class="empty"><h2>${text(title)}</h2><p>${text(lead)}</p></div>`;
227
+ }
228
+ export function statusClass(kind, label) {
229
+ return `<span class="st ${kind}">${text(label)}</span>`;
230
+ }
@@ -0,0 +1,2 @@
1
+ import type { prDetail } from "../../services/dashboard.ts";
2
+ export declare function renderPr(username: string, fullName: string, data: ReturnType<typeof prDetail>): Response;
@@ -0,0 +1,109 @@
1
+ import { empty, html, layout, markdown, money, repoNav, skSlot, text, when, } from "./layout.js";
2
+ export function renderPr(username, fullName, data) {
3
+ const n = data.prNumber;
4
+ const totalCost = data.reviews.reduce((sum, review) => sum + Number(review.cost ?? 0), 0);
5
+ const stateLabel = (state) => {
6
+ if (state === "closed")
7
+ return "Closed";
8
+ if (state === "open")
9
+ return "Still open";
10
+ return "New";
11
+ };
12
+ const findingList = (findings) => {
13
+ if (findings.length === 0) {
14
+ return empty("No findings", "This review did not report anything to fix.");
15
+ }
16
+ const order = ["new", "open", "closed"];
17
+ const groups = order
18
+ .map((state) => ({
19
+ state,
20
+ items: findings.filter((f) => (f.state ?? "new") === state),
21
+ }))
22
+ .filter((group) => group.items.length > 0);
23
+ return groups
24
+ .map((group) => `<div style="margin-bottom:20px">
25
+ <div class="muted" style="font-size:13px;font-weight:600;margin-bottom:10px;text-transform:uppercase;letter-spacing:.04em">${text(stateLabel(group.state))}</div>
26
+ ${group.items
27
+ .map((finding, index) => `<div${index < group.items.length - 1
28
+ ? ` style="padding-bottom:18px;border-bottom:1px solid var(--border2);margin-bottom:18px"`
29
+ : ""}>
30
+ <div style="margin-bottom:6px"><b>${text(finding.title)}</b>${finding.first_seen_review_id
31
+ ? ` <span class="muted" style="font-size:13px">Seen in an earlier round</span>`
32
+ : ""}</div>
33
+ <div class="mono muted" style="font-size:13px;margin-bottom:8px">${text(finding.path ? `${finding.path}:${finding.line_from ?? ""}` : "")}</div>
34
+ ${markdown(finding.body_md)}
35
+ </div>`)
36
+ .join("")}
37
+ </div>`)
38
+ .join("");
39
+ };
40
+ const findingsBlock = data.reviews.length === 0
41
+ ? empty("No findings", "Run a review to post findings on this pull request.")
42
+ : data.reviews
43
+ .map((review, index) => `<details${index === 0 ? " open" : ""} style="padding:14px 0${index < data.reviews.length - 1
44
+ ? ";border-bottom:1px solid var(--border2)"
45
+ : ""}">
46
+ <summary style="cursor:pointer">
47
+ <b>Round ${text(review.round)}</b>
48
+ <span class="muted"> · ${text((review.head_sha ?? "").slice(0, 7))} · ${review.findings.length} finding${review.findings.length === 1 ? "" : "s"}</span>
49
+ </summary>
50
+ <div style="padding:16px 4px 4px">${findingList(review.findings)}</div>
51
+ </details>`)
52
+ .join("");
53
+ const reviewsTable = data.reviews.length === 0
54
+ ? empty("Not reviewed yet", "Run a review to post findings on this pull request.")
55
+ : `<table>
56
+ <thead><tr><th>Round</th><th>When</th><th>Commit</th><th>Looked at</th>
57
+ <th class="num">Findings</th><th class="num">Cost</th></tr></thead>
58
+ <tbody>
59
+ ${data.reviews
60
+ .map((review) => `<tr><td>${text(review.round)}</td>
61
+ <td>${when(review.created_at)}</td>
62
+ <td class="mono">${text((review.head_sha ?? "").slice(0, 7))}</td>
63
+ <td class="muted">${text(review.scope === "incremental"
64
+ ? "Changes since the last review"
65
+ : "Whole pull request")}</td>
66
+ <td class="num">${review.findings_count}</td>
67
+ <td class="num">${money(Number(review.cost ?? 0))}</td></tr>`)
68
+ .join("")}
69
+ </tbody>
70
+ </table>`;
71
+ return html(layout({
72
+ title: `#${n} · co-maintainer`,
73
+ username,
74
+ body: `<div class="wrap side">
75
+ ${repoNav(fullName, "pulls")}
76
+ <div data-async>
77
+ ${skSlot()}
78
+ <div class="crumbs"><a href="/">Repositories</a> /
79
+ <a href="/repos/${text(fullName)}">${text(fullName)}</a> /
80
+ <a href="/repos/${text(fullName)}/pulls">Pull requests</a> / #${n}</div>
81
+ <div class="pagehead">
82
+ <div>
83
+ <h1>#${n}</h1>
84
+ <p class="lead">${data.reviews.length} review${data.reviews.length === 1 ? "" : "s"} on record</p>
85
+ </div>
86
+ <div class="actions">
87
+ <button id="again">Review again</button>
88
+ <a class="btn" href="https://github.com/${text(fullName)}/pull/${n}">Open on GitHub</a>
89
+ </div>
90
+ </div>
91
+ <div class="card">
92
+ <div class="hd"><h2>Findings</h2>
93
+ <span class="muted" style="margin-left:auto;font-size:13px">${data.reviews.reduce((sum, review) => sum + review.findings.length, 0)} findings across ${data.reviews.length} review${data.reviews.length === 1 ? "" : "s"}</span></div>
94
+ <div class="bd">${findingsBlock}</div>
95
+ </div>
96
+ <div class="card">
97
+ <div class="hd"><h2>Reviews</h2>
98
+ <span class="muted" style="margin-left:auto;font-size:13px">${money(totalCost)} total</span></div>
99
+ <div class="bd flush">${reviewsTable}</div>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ <script>
104
+ document.getElementById("again").addEventListener("click", function() {
105
+ postAndGo("/api/repos/" + ${JSON.stringify(fullName)} + "/pulls/${n}/review", {}, "/activity", this);
106
+ });
107
+ </script>`,
108
+ }));
109
+ }
@@ -0,0 +1,2 @@
1
+ import type { repoOverview } from "../../services/dashboard.ts";
2
+ export declare function renderRepo(username: string, data: ReturnType<typeof repoOverview>): Response;
@@ -0,0 +1,92 @@
1
+ import { capped, empty, html, layout, money, repoNav, skSlot, text, when, } from "./layout.js";
2
+ import { COMPARE_FILE_CAP, MAX_COMMITS_COUNTED } from "../../services/drift.js";
3
+ export function renderRepo(username, data) {
4
+ const name = data.repo.full_name;
5
+ const stats = data.stats;
6
+ const drift = data.drift;
7
+ const notice = drift &&
8
+ (drift.prs_since > 0 || drift.prs_updated > 0 || drift.commits_since > 0)
9
+ ? `<div class="notice" data-async>
10
+ ${skSlot()}
11
+ <div class="txt"><b>Update recommended.</b> Since the guide was built:
12
+ ${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">Update now</button>
14
+ </div>`
15
+ : "";
16
+ const pulls = data.recentPulls.length === 0
17
+ ? empty("No reviews yet", "Reviews appear here after a pull request is opened.")
18
+ : `<table>
19
+ <thead><tr><th>Pull request</th><th>Reviewed</th><th class="num">Findings</th></tr></thead>
20
+ <tbody>
21
+ ${data.recentPulls
22
+ .map((review) => `<tr><td><a href="/repos/${text(name)}/pulls/${review.pr_number}">#${review.pr_number}</a></td>
23
+ <td class="muted">${when(review.created_at)}</td>
24
+ <td class="num">${review.findings_count}</td></tr>`)
25
+ .join("")}
26
+ </tbody>
27
+ </table>`;
28
+ const auto = data.repo.auto_review === 1;
29
+ return html(layout({
30
+ title: `${name} · co-maintainer`,
31
+ username,
32
+ body: `<div class="wrap side">
33
+ ${repoNav(name, "overview")}
34
+ <div>
35
+ <div class="crumbs"><a href="/">Repositories</a> / ${text(name)}</div>
36
+ <div class="pagehead">
37
+ <div>
38
+ <h1>${text(name)}</h1>
39
+ <p class="lead">${auto
40
+ ? "Reviewing pull requests automatically"
41
+ : "Automatic review is off"} · knowledge ${data.repo.knowledge_built_at
42
+ ? when(data.repo.knowledge_built_at)
43
+ : "not built yet"}</p>
44
+ </div>
45
+ <div class="actions">
46
+ <a class="btn" href="https://github.com/${text(name)}">Open on GitHub</a>
47
+ </div>
48
+ </div>
49
+ ${notice}
50
+ <div class="card">
51
+ <div class="hd"><h2>Last 30 days</h2></div>
52
+ <div class="bd">
53
+ <div class="kfig">
54
+ <div><div class="k">Pull requests reviewed</div><div class="big">${stats.pullRequests}</div></div>
55
+ <div><div class="k">Findings</div><div class="big">${stats.findings}</div></div>
56
+ <div><div class="k">Cost</div><div class="big">${money(stats.cost)}</div></div>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ <div class="card">
61
+ <div class="hd"><h2>Recent pull requests</h2>
62
+ <a class="btn sm" style="margin-left:auto" href="/repos/${text(name)}/pulls">View all</a></div>
63
+ <div class="bd flush">${pulls}</div>
64
+ </div>
65
+ <div class="card" data-async>
66
+ ${skSlot()}
67
+ <div class="hd"><h2>Automatic review</h2></div>
68
+ <div class="bd">
69
+ <div style="display:flex;align-items:center;gap:14px">
70
+ <button class="tg${auto ? " on" : ""}" id="auto"></button>
71
+ <div>
72
+ <div><b>${auto ? "On" : "Off"}</b>. ${auto
73
+ ? "Every new push to an open pull request gets reviewed."
74
+ : "New pushes are not reviewed until you turn this on."}</div>
75
+ <div class="hint" style="margin:2px 0 0">Drafts and bot pull requests follow the skip settings.
76
+ <a href="/repos/${text(name)}/settings">Change</a></div>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ <script>
84
+ var repo = ${JSON.stringify(name)};
85
+ bindToggle(document.getElementById("auto"), "/api/repos/" + repo, "autoReview");
86
+ var remake = document.getElementById("remake");
87
+ if (remake) remake.addEventListener("click", function() {
88
+ postAndGo("/api/repos/" + repo + "/remake", {}, "/activity", remake);
89
+ });
90
+ </script>`,
91
+ }));
92
+ }
@@ -0,0 +1,2 @@
1
+ import type { repoPulls } from "../../services/dashboard.ts";
2
+ export declare function renderRepoPulls(username: string, fullName: string, data: ReturnType<typeof repoPulls>): Response;
@@ -0,0 +1,75 @@
1
+ import { empty, html, layout, money, repoNav, skSlot, text, when, } from "./layout.js";
2
+ export function renderRepoPulls(username, fullName, data) {
3
+ const start = (data.page - 1) * data.per + 1;
4
+ const end = Math.min(data.page * data.per, data.total);
5
+ const pager = data.total === 0
6
+ ? ""
7
+ : `<div class="pager">
8
+ <span>${start}-${end} of ${data.total}</span>
9
+ <span class="sp"></span>
10
+ ${data.page > 1
11
+ ? `<a class="btn sm" href="?page=${data.page - 1}">Previous</a>`
12
+ : `<button class="btn sm" disabled>Previous</button>`}
13
+ ${end < data.total
14
+ ? `<a class="btn sm" href="?page=${data.page + 1}">Next</a>`
15
+ : `<button class="btn sm" disabled>Next</button>`}
16
+ </div>`;
17
+ const table = data.items.length === 0
18
+ ? empty("No pull requests reviewed", "A review of an open pull request will show up here.")
19
+ : `<table>
20
+ <thead><tr><th>Pull request</th><th>Last reviewed</th>
21
+ <th class="num">Findings</th><th class="num">Cost</th></tr></thead>
22
+ <tbody>
23
+ ${data.items
24
+ .map((item) => `<tr><td><a href="/repos/${text(fullName)}/pulls/${item.pr_number}">#${item.pr_number}</a></td>
25
+ <td class="muted">${when(item.last_reviewed)}</td>
26
+ <td class="num">${item.findings}</td>
27
+ <td class="num">${money(item.cost)}</td></tr>`)
28
+ .join("")}
29
+ </tbody>
30
+ </table>${pager}`;
31
+ const [owner, repo] = fullName.split("/");
32
+ const reviewBase = `/api/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/`;
33
+ return html(layout({
34
+ title: `Pull requests · ${fullName}`,
35
+ username,
36
+ body: `<div class="wrap side">
37
+ ${repoNav(fullName, "pulls")}
38
+ <div>
39
+ <div class="crumbs"><a href="/">Repositories</a> /
40
+ <a href="/repos/${text(fullName)}">${text(fullName)}</a> / Pull requests</div>
41
+ <div class="pagehead">
42
+ <div><h1>Pull requests</h1>
43
+ <p class="lead">${data.stats.pullRequests} reviewed in the last 30 days</p></div>
44
+ </div>
45
+ <div data-async>
46
+ ${skSlot()}
47
+ <div class="card"><div class="bd">
48
+ <h2>Review a pull request</h2>
49
+ <p class="lead">Enter an open pull request number to start a review.</p>
50
+ <form id="manual-review" style="display:flex;align-items:end;gap:8px;margin-top:14px">
51
+ <div style="width:180px">
52
+ <label for="pr-number">PR number</label>
53
+ <input id="pr-number" name="prNumber" type="number" min="1" step="1" required>
54
+ </div>
55
+ <button class="primary" type="submit">Start review</button>
56
+ </form>
57
+ </div></div>
58
+ </div>
59
+ <div class="card"><div class="bd flush">${table}</div></div>
60
+ </div>
61
+ </div>
62
+ <script>
63
+ document.getElementById("manual-review").addEventListener("submit", function(event) {
64
+ event.preventDefault();
65
+ var input = document.getElementById("pr-number");
66
+ var number = Number(input.value);
67
+ if (!Number.isSafeInteger(number) || number < 1) {
68
+ input.focus();
69
+ return;
70
+ }
71
+ postAndGo(${JSON.stringify(reviewBase)} + number + "/review", {}, "/activity", this.querySelector("button"));
72
+ });
73
+ </script>`,
74
+ }));
75
+ }
@@ -0,0 +1,2 @@
1
+ import type { repoRemoteReviews } from "../../services/dashboard.ts";
2
+ export declare function renderRepoRemote(username: string, fullName: string, data: ReturnType<typeof repoRemoteReviews>): Response;
@@ -0,0 +1,38 @@
1
+ import { empty, html, layout, money, repoNav, text, when } from "./layout.js";
2
+ export function renderRepoRemote(username, fullName, data) {
3
+ const rows = data.items.length === 0
4
+ ? empty("No remote reviews yet", "Developers run co-maintainer review --remote with a token from Settings.")
5
+ : `<table>
6
+ <thead><tr><th>Branch</th><th>Token</th><th>Status</th><th class="num">Findings</th>
7
+ <th class="num">Cost</th><th>When</th></tr></thead>
8
+ <tbody>
9
+ ${data.items
10
+ .map((review) => `<tr>
11
+ <td>${text(review.branch ?? "—")}</td>
12
+ <td>${text(review.token_name ?? "—")}</td>
13
+ <td>${text(review.status)}</td>
14
+ <td class="num">${review.findings_count ?? 0}</td>
15
+ <td class="num">${money(review.cost ?? 0)}</td>
16
+ <td class="muted">${when(review.created_at)}</td>
17
+ </tr>`)
18
+ .join("")}
19
+ </tbody>
20
+ </table>`;
21
+ return html(layout({
22
+ title: `${fullName} · Remote · co-maintainer`,
23
+ username,
24
+ body: `<div class="wrap side">
25
+ ${repoNav(fullName, "remote")}
26
+ <div>
27
+ <div class="crumbs"><a href="/">Repositories</a> / ${text(fullName)}</div>
28
+ <div class="pagehead">
29
+ <div>
30
+ <h1>Remote reviews</h1>
31
+ <p class="lead">CLI reviews submitted from developer machines (last 30 days).</p>
32
+ </div>
33
+ </div>
34
+ <div class="card"><div class="bd flush">${rows}</div></div>
35
+ </div>
36
+ </div>`,
37
+ }));
38
+ }
@@ -0,0 +1,3 @@
1
+ import type { RepoRow } from "../../store/rows.ts";
2
+ import type { RepoConfig } from "../../config.ts";
3
+ export declare function renderRepoSettings(username: string, repo: RepoRow, config: RepoConfig): Response;