@moritzwilksch/diffle 0.1.6 → 0.1.7

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 (95) hide show
  1. package/README.md +7 -5
  2. package/THIRD_PARTY_NOTICES.md +95 -0
  3. package/dist/client/assets/{angular-html-VfoYZbqR.js → angular-html-gsHTuMGB.js} +1 -1
  4. package/dist/client/assets/{angular-ts-E_ds17k3.js → angular-ts-xhBJShFG.js} +1 -1
  5. package/dist/client/assets/{apl-DfLg31Yo.js → apl-nsC6ra4J.js} +1 -1
  6. package/dist/client/assets/{astro-DDZGZEa0.js → astro-X5_I_ruP.js} +1 -1
  7. package/dist/client/assets/{blade-C7j7NUSc.js → blade-BqlxrrkS.js} +1 -1
  8. package/dist/client/assets/{c-B9ldfZRm.js → c-DVgCXnBp.js} +1 -1
  9. package/dist/client/assets/{chapel-D1rbgfGL.js → chapel-LgKo5FoM.js} +1 -1
  10. package/dist/client/assets/{cobol-CvvLWZ5H.js → cobol-BVkDOaHP.js} +1 -1
  11. package/dist/client/assets/{coffee-Cq5EY8Kf.js → coffee-BVuWHkH6.js} +1 -1
  12. package/dist/client/assets/{cpp-KDnKhkos.js → cpp-DnxElGuW.js} +1 -1
  13. package/dist/client/assets/{crystal-qnDGw82Q.js → crystal-CC8Vxuob.js} +1 -1
  14. package/dist/client/assets/{css-DbYR8NW9.js → css-BBuKGmvC.js} +1 -1
  15. package/dist/client/assets/{edge-BytNLpnp.js → edge-CNOZBEfw.js} +1 -1
  16. package/dist/client/assets/{elixir-BOIZTT6O.js → elixir-BG8NE-xg.js} +1 -1
  17. package/dist/client/assets/{elm-BLyo2uoF.js → elm-Cyp6TnT_.js} +1 -1
  18. package/dist/client/assets/{erb-C8RG1ggg.js → erb-C58VxpfX.js} +1 -1
  19. package/dist/client/assets/{git-rebase-BsJjDHkQ.js → git-rebase-A6HkVJC_.js} +1 -1
  20. package/dist/client/assets/{glimmer-js-F0nfRvQ4.js → glimmer-js-CqR_jN8G.js} +1 -1
  21. package/dist/client/assets/{glimmer-ts-BaUh_0sm.js → glimmer-ts-M3ExOO4n.js} +1 -1
  22. package/dist/client/assets/{glsl-r7jKENsf.js → glsl--WIxeXSU.js} +1 -1
  23. package/dist/client/assets/{graphql-DecxGIfy.js → graphql-BXRMLGZY.js} +1 -1
  24. package/dist/client/assets/{hack-D1QdSbsI.js → hack-C8N7KWCY.js} +1 -1
  25. package/dist/client/assets/{haml-DZ3Y8IEU.js → haml-CxHPK-iA.js} +1 -1
  26. package/dist/client/assets/{handlebars-9ij-Y0dS.js → handlebars-BNjxS1qh.js} +1 -1
  27. package/dist/client/assets/{html-_813EOsa.js → html-CO9JbMOk.js} +1 -1
  28. package/dist/client/assets/{html-derivative-DMoYXYJX.js → html-derivative-CD1w-J8T.js} +1 -1
  29. package/dist/client/assets/{http-BHZjBd1F.js → http-DvdIZe4B.js} +1 -1
  30. package/dist/client/assets/{hurl-B9C7GLqi.js → hurl-BPchDEWD.js} +1 -1
  31. package/dist/client/assets/index-B47iHZXu.css +2 -0
  32. package/dist/client/assets/{index-DDePiSWW.js → index-BcTYvSA1.js} +90 -90
  33. package/dist/client/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  34. package/dist/client/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  35. package/dist/client/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  36. package/dist/client/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  37. package/dist/client/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  38. package/dist/client/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  39. package/dist/client/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  40. package/dist/client/assets/{java-BXcKXYSC.js → java-Cw12dAwx.js} +1 -1
  41. package/dist/client/assets/{javascript-DzK8bvMk.js → javascript-CVrIgFqG.js} +1 -1
  42. package/dist/client/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  43. package/dist/client/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  44. package/dist/client/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  45. package/dist/client/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  46. package/dist/client/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  47. package/dist/client/assets/{jinja-DkPJymJ8.js → jinja-Dcw1Cb-z.js} +1 -1
  48. package/dist/client/assets/{jison-BRfwGndr.js → jison-BCHOo1FF.js} +1 -1
  49. package/dist/client/assets/{json-CVsgTDXF.js → json-BkfEJ7uD.js} +1 -1
  50. package/dist/client/assets/{jsx-CCEGQMKy.js → jsx-CBE97uyR.js} +1 -1
  51. package/dist/client/assets/{julia-DB5wqhUl.js → julia-Dt37WVZB.js} +1 -1
  52. package/dist/client/assets/{just-saVQlczz.js → just-DFHYF8cW.js} +1 -1
  53. package/dist/client/assets/{latex-C7pJ0jXy.js → latex-BtlIyAq9.js} +1 -1
  54. package/dist/client/assets/{liquid-BzVhxXiW.js → liquid-PdbqGeMi.js} +1 -1
  55. package/dist/client/assets/{lua-B68msXNV.js → lua-lhtz56hU.js} +1 -1
  56. package/dist/client/assets/{marko-D0vTAOKD.js → marko-DPsaza10.js} +1 -1
  57. package/dist/client/assets/{mdc-C5xXBRll.js → mdc-DIH2Ec5y.js} +1 -1
  58. package/dist/client/assets/{nginx-BgUn926n.js → nginx-bHk_XM7O.js} +1 -1
  59. package/dist/client/assets/{nim-BWiAEcU7.js → nim-BgUabkBA.js} +1 -1
  60. package/dist/client/assets/{org-PxncFL6r.js → org-PMkj4aaP.js} +1 -1
  61. package/dist/client/assets/{perl-DNCL3XMU.js → perl-B1ZCbepN.js} +1 -1
  62. package/dist/client/assets/{php-B4HLiXa-.js → php-Vj6Jmzuy.js} +1 -1
  63. package/dist/client/assets/{pug-YBRTFp5G.js → pug-B0ziVZ95.js} +1 -1
  64. package/dist/client/assets/{qml-Kbw3ykka.js → qml--NUJOMX2.js} +1 -1
  65. package/dist/client/assets/{r-e7ntBSQI.js → r-CCy6kg5Q.js} +1 -1
  66. package/dist/client/assets/{razor-C-7kbnr6.js → razor-DBTJol_D.js} +1 -1
  67. package/dist/client/assets/{regexp-BfKQ26Bd.js → regexp-B-0M8p3f.js} +1 -1
  68. package/dist/client/assets/{rst-Dk61acH4.js → rst-DVPZR2Zw.js} +1 -1
  69. package/dist/client/assets/{ruby-B-Gsrm_0.js → ruby-CY-1xRWL.js} +1 -1
  70. package/dist/client/assets/{sas-DrwY32Sv.js → sas-BfWvhRC7.js} +1 -1
  71. package/dist/client/assets/{scss-BQ0tGjl9.js → scss-CDSNB-cI.js} +1 -1
  72. package/dist/client/assets/{shellscript-70AyB6AW.js → shellscript-x3a-v0TL.js} +1 -1
  73. package/dist/client/assets/{shellsession-C0TRVWC2.js → shellsession-CBXmn1pk.js} +1 -1
  74. package/dist/client/assets/{soy-ljPZKxfc.js → soy-Dx9wgv4j.js} +1 -1
  75. package/dist/client/assets/{sql-CpAh8MTM.js → sql-GJE8dpQY.js} +1 -1
  76. package/dist/client/assets/{stata-U-vhcCzr.js → stata-Cu3eQvZM.js} +1 -1
  77. package/dist/client/assets/{surrealql-Cq-W9D43.js → surrealql-BSIyNQF3.js} +1 -1
  78. package/dist/client/assets/{svelte-CsPr3yNN.js → svelte-BC05E7SM.js} +1 -1
  79. package/dist/client/assets/{templ-DieGJMZV.js → templ-BZb4iLGg.js} +1 -1
  80. package/dist/client/assets/{tex-D0ERfPEs.js → tex-BgSQdktB.js} +1 -1
  81. package/dist/client/assets/{ts-tags-Bw9LC0mV.js → ts-tags-ByZvgo6s.js} +1 -1
  82. package/dist/client/assets/{tsx-DIXVqg6z.js → tsx-BkywoqnG.js} +1 -1
  83. package/dist/client/assets/{twig-DbTBKLwc.js → twig-DT3o-XyD.js} +1 -1
  84. package/dist/client/assets/{typescript-Bd6Plo3n.js → typescript-Dc_VYLnG.js} +1 -1
  85. package/dist/client/assets/{typst-DqDsanSi.js → typst-Dufd3udg.js} +1 -1
  86. package/dist/client/assets/{vue-Da_CXJw9.js → vue-CWow0PK2.js} +1 -1
  87. package/dist/client/assets/{vue-html-n6FmJEJ_.js → vue-html-Dukrnida.js} +1 -1
  88. package/dist/client/assets/{vue-vine-BOc7r2gI.js → vue-vine-DeFkcU4G.js} +1 -1
  89. package/dist/client/assets/{xml-DzU5oF9g.js → xml-w1QbHsNc.js} +1 -1
  90. package/dist/client/assets/{xsl-BTxmEaay.js → xsl-DQ1uA8pZ.js} +1 -1
  91. package/dist/client/assets/{yaml-DYMpEUsY.js → yaml-Bt6SDR__.js} +1 -1
  92. package/dist/client/index.html +2 -2
  93. package/dist/server/main.js +1758 -430
  94. package/package.json +6 -2
  95. package/dist/client/assets/index-CDwn6vHT.css +0 -2
@@ -3951,7 +3951,7 @@ function generateCompletion(program, { shell, executable = program.name() }) {
3951
3951
  }
3952
3952
  //#endregion
3953
3953
  //#region package.json
3954
- var version$1 = "0.1.6";
3954
+ var version$1 = "0.1.7";
3955
3955
  //#endregion
3956
3956
  //#region src/server/comments/anchor.ts
3957
3957
  function splitLines(contents) {
@@ -5112,460 +5112,1340 @@ function diffRange(oldRev, newRev) {
5112
5112
  return oldRev === "worktree" ? ["-R", newRev] : newRev === "worktree" ? [oldRev] : [oldRev, newRev];
5113
5113
  }
5114
5114
  //#endregion
5115
- //#region src/server/github.ts
5116
- /** A precondition the user can fix (no gh, no PR, wrong mode, unpushed head): the route answers 4xx. */
5117
- var GithubError = class extends Error {
5118
- status;
5119
- constructor(message, status = 409) {
5120
- super(message);
5121
- this.status = status;
5122
- }
5115
+ //#region node_modules/universal-user-agent/index.js
5116
+ function getUserAgent() {
5117
+ if (typeof navigator === "object" && "userAgent" in navigator) return navigator.userAgent;
5118
+ if (typeof process === "object" && process.version !== void 0) return `Node.js/${process.version.substr(1)} (${process.platform}; ${process.arch})`;
5119
+ return "<environment undetectable>";
5120
+ }
5121
+ //#endregion
5122
+ //#region node_modules/before-after-hook/lib/register.js
5123
+ function register(state, name, method, options) {
5124
+ if (typeof method !== "function") throw new Error("method for before hook must be a function");
5125
+ if (!options) options = {};
5126
+ if (Array.isArray(name)) return name.reverse().reduce((callback, name) => {
5127
+ return register.bind(null, state, name, callback, options);
5128
+ }, method)();
5129
+ return Promise.resolve().then(() => {
5130
+ if (!state.registry[name]) return method(options);
5131
+ return state.registry[name].reduce((method, registered) => {
5132
+ return registered.hook.bind(null, method, options);
5133
+ }, method)();
5134
+ });
5135
+ }
5136
+ //#endregion
5137
+ //#region node_modules/before-after-hook/lib/add.js
5138
+ function addHook(state, kind, name, hook) {
5139
+ const orig = hook;
5140
+ if (!state.registry[name]) state.registry[name] = [];
5141
+ if (kind === "before") hook = (method, options) => {
5142
+ return Promise.resolve().then(orig.bind(null, options)).then(method.bind(null, options));
5143
+ };
5144
+ if (kind === "after") hook = (method, options) => {
5145
+ let result;
5146
+ return Promise.resolve().then(method.bind(null, options)).then((result_) => {
5147
+ result = result_;
5148
+ return orig(result, options);
5149
+ }).then(() => {
5150
+ return result;
5151
+ });
5152
+ };
5153
+ if (kind === "error") hook = (method, options) => {
5154
+ return Promise.resolve().then(method.bind(null, options)).catch((error) => {
5155
+ return orig(error, options);
5156
+ });
5157
+ };
5158
+ state.registry[name].push({
5159
+ hook,
5160
+ orig
5161
+ });
5162
+ }
5163
+ //#endregion
5164
+ //#region node_modules/before-after-hook/lib/remove.js
5165
+ function removeHook(state, name, method) {
5166
+ if (!state.registry[name]) return;
5167
+ const index = state.registry[name].map((registered) => {
5168
+ return registered.orig;
5169
+ }).indexOf(method);
5170
+ if (index === -1) return;
5171
+ state.registry[name].splice(index, 1);
5172
+ }
5173
+ //#endregion
5174
+ //#region node_modules/before-after-hook/index.js
5175
+ const bind = Function.bind;
5176
+ const bindable = bind.bind(bind);
5177
+ function bindApi(hook, state, name) {
5178
+ const removeHookRef = bindable(removeHook, null).apply(null, name ? [state, name] : [state]);
5179
+ hook.api = { remove: removeHookRef };
5180
+ hook.remove = removeHookRef;
5181
+ [
5182
+ "before",
5183
+ "error",
5184
+ "after",
5185
+ "wrap"
5186
+ ].forEach((kind) => {
5187
+ const args = name ? [
5188
+ state,
5189
+ kind,
5190
+ name
5191
+ ] : [state, kind];
5192
+ hook[kind] = hook.api[kind] = bindable(addHook, null).apply(null, args);
5193
+ });
5194
+ }
5195
+ function Singular() {
5196
+ const singularHookName = Symbol("Singular");
5197
+ const singularHookState = { registry: {} };
5198
+ const singularHook = register.bind(null, singularHookState, singularHookName);
5199
+ bindApi(singularHook, singularHookState, singularHookName);
5200
+ return singularHook;
5201
+ }
5202
+ function Collection() {
5203
+ const state = { registry: {} };
5204
+ const hook = register.bind(null, state);
5205
+ bindApi(hook, state);
5206
+ return hook;
5207
+ }
5208
+ var before_after_hook_default = {
5209
+ Singular,
5210
+ Collection
5123
5211
  };
5124
- function isFileComment(c) {
5125
- return "subject_type" in c;
5212
+ //#endregion
5213
+ //#region node_modules/@octokit/endpoint/dist-bundle/index.js
5214
+ var DEFAULTS = {
5215
+ method: "GET",
5216
+ baseUrl: "https://api.github.com",
5217
+ headers: {
5218
+ accept: "application/vnd.github.v3+json",
5219
+ "user-agent": `octokit-endpoint.js/0.0.0-development ${getUserAgent()}`
5220
+ },
5221
+ mediaType: { format: "" }
5222
+ };
5223
+ function lowercaseKeys(object) {
5224
+ if (!object) return {};
5225
+ return Object.keys(object).reduce((newObj, key) => {
5226
+ newObj[key.toLowerCase()] = object[key];
5227
+ return newObj;
5228
+ }, {});
5229
+ }
5230
+ function isPlainObject$2(value) {
5231
+ if (typeof value !== "object" || value === null) return false;
5232
+ if (Object.prototype.toString.call(value) !== "[object Object]") return false;
5233
+ const proto = Object.getPrototypeOf(value);
5234
+ if (proto === null) return true;
5235
+ const Ctor = Object.prototype.hasOwnProperty.call(proto, "constructor") && proto.constructor;
5236
+ return typeof Ctor === "function" && Ctor instanceof Ctor && Function.prototype.call(Ctor) === Function.prototype.call(value);
5237
+ }
5238
+ function mergeDeep(defaults, options) {
5239
+ const result = Object.assign({}, defaults);
5240
+ Object.keys(options).forEach((key) => {
5241
+ if (isPlainObject$2(options[key])) {
5242
+ if (!(key in defaults)) Object.assign(result, { [key]: options[key] });
5243
+ else result[key] = mergeDeep(defaults[key], options[key]);
5244
+ } else Object.assign(result, { [key]: options[key] });
5245
+ });
5246
+ return result;
5126
5247
  }
5127
- /**
5128
- * Threads → the comments of one pending GitHub review, one per thread. Stale threads are skipped:
5129
- * their lines no longer sit in the diff, and GitHub would refuse them anyway. With
5130
- * `threadIds`, only those (resolved included, the user asked for them by hand);
5131
- * without, every unresolved thread. `review.comments` holds the line comments REST can
5132
- * create; the file comments are appended afterwards, but `ids` counts both, in review order.
5133
- */
5134
- function buildReview(threads, commitId, threadIds) {
5135
- const skipped = [];
5136
- let chosen;
5137
- if (threadIds) {
5138
- const byId = new Map(threads.map((t) => [t.id, t]));
5139
- chosen = [];
5140
- for (const id of threadIds) {
5141
- const t = byId.get(id);
5142
- if (t) chosen.push(t);
5143
- else skipped.push({
5144
- id,
5145
- reason: "unknown thread"
5248
+ function removeUndefinedProperties(obj) {
5249
+ for (const key in obj) if (obj[key] === void 0) delete obj[key];
5250
+ return obj;
5251
+ }
5252
+ function merge$1(defaults, route, options) {
5253
+ if (typeof route === "string") {
5254
+ let [method, url] = route.split(" ");
5255
+ options = Object.assign(url ? {
5256
+ method,
5257
+ url
5258
+ } : { url: method }, options);
5259
+ } else options = Object.assign({}, route);
5260
+ options.headers = lowercaseKeys(options.headers);
5261
+ removeUndefinedProperties(options);
5262
+ removeUndefinedProperties(options.headers);
5263
+ const mergedOptions = mergeDeep(defaults || {}, options);
5264
+ if (options.url === "/graphql") {
5265
+ if (defaults && defaults.mediaType.previews?.length) mergedOptions.mediaType.previews = defaults.mediaType.previews.filter((preview) => !mergedOptions.mediaType.previews.includes(preview)).concat(mergedOptions.mediaType.previews);
5266
+ mergedOptions.mediaType.previews = (mergedOptions.mediaType.previews || []).map((preview) => preview.replace(/-preview/, ""));
5267
+ }
5268
+ return mergedOptions;
5269
+ }
5270
+ function addQueryParameters(url, parameters) {
5271
+ const separator = /\?/.test(url) ? "&" : "?";
5272
+ const names = Object.keys(parameters);
5273
+ if (names.length === 0) return url;
5274
+ return url + separator + names.map((name) => {
5275
+ if (name === "q") return "q=" + parameters.q.split("+").map(encodeURIComponent).join("+");
5276
+ return `${name}=${encodeURIComponent(parameters[name])}`;
5277
+ }).join("&");
5278
+ }
5279
+ var urlVariableRegex = /\{[^{}}]+\}/g;
5280
+ function removeNonChars(variableName) {
5281
+ return variableName.replace(/(?:^\W+)|(?:(?<!\W)\W+$)/g, "").split(/,/);
5282
+ }
5283
+ function extractUrlVariableNames(url) {
5284
+ const matches = url.match(urlVariableRegex);
5285
+ if (!matches) return [];
5286
+ return matches.map(removeNonChars).reduce((a, b) => a.concat(b), []);
5287
+ }
5288
+ function omit$1(object, keysToOmit) {
5289
+ const result = { __proto__: null };
5290
+ for (const key of Object.keys(object)) if (keysToOmit.indexOf(key) === -1) result[key] = object[key];
5291
+ return result;
5292
+ }
5293
+ function encodeReserved(str) {
5294
+ return str.split(/(%[0-9A-Fa-f]{2})/g).map(function(part) {
5295
+ if (!/%[0-9A-Fa-f]/.test(part)) part = encodeURI(part).replace(/%5B/g, "[").replace(/%5D/g, "]");
5296
+ return part;
5297
+ }).join("");
5298
+ }
5299
+ function encodeUnreserved(str) {
5300
+ return encodeURIComponent(str).replace(/[!'()*]/g, function(c) {
5301
+ return "%" + c.charCodeAt(0).toString(16).toUpperCase();
5302
+ });
5303
+ }
5304
+ function encodeValue(operator, value, key) {
5305
+ value = operator === "+" || operator === "#" ? encodeReserved(value) : encodeUnreserved(value);
5306
+ if (key) return encodeUnreserved(key) + "=" + value;
5307
+ else return value;
5308
+ }
5309
+ function isDefined(value) {
5310
+ return value !== void 0 && value !== null;
5311
+ }
5312
+ function isKeyOperator(operator) {
5313
+ return operator === ";" || operator === "&" || operator === "?";
5314
+ }
5315
+ function getValues(context, operator, key, modifier) {
5316
+ var value = context[key], result = [];
5317
+ if (isDefined(value) && value !== "") {
5318
+ if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") {
5319
+ value = value.toString();
5320
+ if (modifier && modifier !== "*") value = value.substring(0, parseInt(modifier, 10));
5321
+ result.push(encodeValue(operator, value, isKeyOperator(operator) ? key : ""));
5322
+ } else if (modifier === "*") {
5323
+ if (Array.isArray(value)) value.filter(isDefined).forEach(function(value2) {
5324
+ result.push(encodeValue(operator, value2, isKeyOperator(operator) ? key : ""));
5146
5325
  });
5147
- }
5148
- } else chosen = threads.filter((t) => !t.resolved);
5149
- const comments = [];
5150
- const ids = [];
5151
- for (const t of [...chosen].sort(compareThreads)) {
5152
- if (t.stale) {
5153
- skipped.push({
5154
- id: t.id,
5155
- reason: "stale"
5326
+ else Object.keys(value).forEach(function(k) {
5327
+ if (isDefined(value[k])) result.push(encodeValue(operator, value[k], k));
5156
5328
  });
5157
- continue;
5329
+ } else {
5330
+ const tmp = [];
5331
+ if (Array.isArray(value)) value.filter(isDefined).forEach(function(value2) {
5332
+ tmp.push(encodeValue(operator, value2));
5333
+ });
5334
+ else Object.keys(value).forEach(function(k) {
5335
+ if (isDefined(value[k])) {
5336
+ tmp.push(encodeUnreserved(k));
5337
+ tmp.push(encodeValue(operator, value[k].toString()));
5338
+ }
5339
+ });
5340
+ if (isKeyOperator(operator)) result.push(encodeUnreserved(key) + "=" + tmp.join(","));
5341
+ else if (tmp.length !== 0) result.push(tmp.join(","));
5158
5342
  }
5159
- comments.push(toReviewComment(t));
5160
- ids.push(t.id);
5161
- }
5162
- return {
5163
- review: {
5164
- commit_id: commitId,
5165
- comments: comments.filter((c) => !isFileComment(c))
5166
- },
5167
- comments,
5168
- ids,
5169
- skipped
5170
- };
5343
+ } else if (operator === ";") {
5344
+ if (isDefined(value)) result.push(encodeUnreserved(key));
5345
+ } else if (value === "" && (operator === "&" || operator === "?")) result.push(encodeUnreserved(key) + "=");
5346
+ else if (value === "") result.push("");
5347
+ return result;
5171
5348
  }
5172
- function toReviewComment(t) {
5173
- const { anchor } = t;
5174
- const body = formatBody(t.messages);
5175
- if (anchor.kind === "file") return {
5176
- path: anchor.path,
5177
- subject_type: "file",
5178
- body
5179
- };
5180
- const side = anchor.side === "old" ? "LEFT" : "RIGHT";
5181
- const c = {
5182
- path: anchor.path,
5183
- line: anchor.endLine,
5184
- side,
5185
- body
5186
- };
5187
- if (anchor.startLine !== anchor.endLine) {
5188
- c.start_line = anchor.startLine;
5189
- c.start_side = side;
5190
- }
5191
- return c;
5349
+ function parseUrl(template) {
5350
+ return { expand: expand.bind(null, template) };
5351
+ }
5352
+ function expand(template, context) {
5353
+ var operators = [
5354
+ "+",
5355
+ "#",
5356
+ ".",
5357
+ "/",
5358
+ ";",
5359
+ "?",
5360
+ "&"
5361
+ ];
5362
+ template = template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, function(_, expression, literal) {
5363
+ if (expression) {
5364
+ let operator = "";
5365
+ const values = [];
5366
+ if (operators.indexOf(expression.charAt(0)) !== -1) {
5367
+ operator = expression.charAt(0);
5368
+ expression = expression.substr(1);
5369
+ }
5370
+ expression.split(/,/g).forEach(function(variable) {
5371
+ var tmp = /([^:\*]*)(?::(\d+)|(\*))?/.exec(variable);
5372
+ values.push(getValues(context, operator, tmp[1], tmp[2] || tmp[3]));
5373
+ });
5374
+ if (operator && operator !== "+") {
5375
+ var separator = ",";
5376
+ if (operator === "?") separator = "&";
5377
+ else if (operator !== "#") separator = operator;
5378
+ return (values.length !== 0 ? operator : "") + values.join(separator);
5379
+ } else return values.join(",");
5380
+ } else return encodeReserved(literal);
5381
+ });
5382
+ if (template === "/") return template;
5383
+ else return template.replace(/\/$/, "");
5384
+ }
5385
+ function parse$2(options) {
5386
+ let method = options.method.toUpperCase();
5387
+ let url = (options.url || "/").replace(/:([a-z]\w+)/g, "{$1}");
5388
+ let headers = Object.assign({}, options.headers);
5389
+ let body;
5390
+ let parameters = omit$1(options, [
5391
+ "method",
5392
+ "baseUrl",
5393
+ "url",
5394
+ "headers",
5395
+ "request",
5396
+ "mediaType"
5397
+ ]);
5398
+ const urlVariableNames = extractUrlVariableNames(url);
5399
+ url = parseUrl(url).expand(parameters);
5400
+ if (!/^http/.test(url)) url = options.baseUrl + url;
5401
+ const remainingParameters = omit$1(parameters, Object.keys(options).filter((option) => urlVariableNames.includes(option)).concat("baseUrl"));
5402
+ if (!/application\/octet-stream/i.test(headers.accept)) {
5403
+ if (options.mediaType.format) headers.accept = headers.accept.split(/,/).map((format) => format.replace(/application\/vnd(\.\w+)(\.v3)?(\.\w+)?(\+json)?$/, `application/vnd$1$2.${options.mediaType.format}`)).join(",");
5404
+ if (url.endsWith("/graphql")) {
5405
+ if (options.mediaType.previews?.length) headers.accept = (headers.accept.match(/(?<![\w-])[\w-]+(?=-preview)/g) || []).concat(options.mediaType.previews).map((preview) => {
5406
+ return `application/vnd.github.${preview}-preview${options.mediaType.format ? `.${options.mediaType.format}` : "+json"}`;
5407
+ }).join(",");
5408
+ }
5409
+ }
5410
+ if (["GET", "HEAD"].includes(method)) url = addQueryParameters(url, remainingParameters);
5411
+ else if ("data" in remainingParameters) body = remainingParameters.data;
5412
+ else if (Object.keys(remainingParameters).length) body = remainingParameters;
5413
+ if (!headers["content-type"] && typeof body !== "undefined") headers["content-type"] = "application/json; charset=utf-8";
5414
+ if (["PATCH", "PUT"].includes(method) && typeof body === "undefined") body = "";
5415
+ return Object.assign({
5416
+ method,
5417
+ url,
5418
+ headers
5419
+ }, typeof body !== "undefined" ? { body } : null, options.request ? { request: options.request } : null);
5420
+ }
5421
+ function endpointWithDefaults(defaults, route, options) {
5422
+ return parse$2(merge$1(defaults, route, options));
5423
+ }
5424
+ function withDefaults$2(oldDefaults, newDefaults) {
5425
+ const DEFAULTS2 = merge$1(oldDefaults, newDefaults);
5426
+ const endpoint2 = endpointWithDefaults.bind(null, DEFAULTS2);
5427
+ return Object.assign(endpoint2, {
5428
+ DEFAULTS: DEFAULTS2,
5429
+ defaults: withDefaults$2.bind(null, DEFAULTS2),
5430
+ merge: merge$1.bind(null, DEFAULTS2),
5431
+ parse: parse$2
5432
+ });
5192
5433
  }
5434
+ var endpoint = withDefaults$2(null, DEFAULTS);
5435
+ //#endregion
5436
+ //#region node_modules/content-type/dist/index.js
5437
+ /*!
5438
+ * content-type
5439
+ * Copyright(c) 2015 Douglas Christopher Wilson
5440
+ * MIT Licensed
5441
+ */
5442
+ const SP = 32;
5443
+ const HTAB = 9;
5444
+ const SEMI = 59;
5445
+ const EQ = 61;
5446
+ const DQUOTE = 34;
5447
+ const BSLASH = 92;
5448
+ const COMMA = 44;
5449
+ const LOWER_CASE = 1;
5450
+ const OWS = 2;
5451
+ const SEMI_FLAG = 4;
5452
+ const COMMA_FLAG = 8;
5453
+ const TOKEN_FLAG = 16;
5454
+ const NON_ASCII = 65280;
5455
+ const CASE_FLAGS = 65281;
5193
5456
  /**
5194
- * Messages joined as markdown. GitHub renders ```suggestion fences itself, so bodies
5195
- * stay verbatim.
5457
+ * Character flags used to normalize HTTP field values while scanning.
5458
+ * Out-of-range reads intentionally coerce to zero in bitwise expressions.
5196
5459
  */
5197
- function formatBody(messages) {
5198
- return messages.map((m) => m.body.trim()).join("\n\n---\n\n");
5460
+ const CHAR_MAP = /* @__PURE__ */ new Uint8Array(256);
5461
+ CHAR_MAP[HTAB] |= OWS;
5462
+ CHAR_MAP[SP] |= OWS;
5463
+ CHAR_MAP[SEMI] |= SEMI_FLAG;
5464
+ CHAR_MAP[COMMA] |= COMMA_FLAG;
5465
+ for (let code = 128; code <= 255; code++) CHAR_MAP[code] |= LOWER_CASE;
5466
+ for (const char of "!#$%&'*+-.^_`|~") CHAR_MAP[char.charCodeAt(0)] |= TOKEN_FLAG;
5467
+ for (let code = 48; code <= 57; code++) CHAR_MAP[code] |= TOKEN_FLAG;
5468
+ for (let code = 65; code <= 90; code++) CHAR_MAP[code] |= 17;
5469
+ for (let code = 97; code <= 122; code++) CHAR_MAP[code] |= TOKEN_FLAG;
5470
+ /**
5471
+ * Null object perf optimization. Faster than `Object.create(null)` and `{ __proto__: null }`.
5472
+ */
5473
+ const NullObject = /* @__PURE__ */ (() => {
5474
+ const C = function() {};
5475
+ C.prototype = Object.create(null);
5476
+ return C;
5477
+ })();
5478
+ /**
5479
+ * Parse a `Content-Type` header.
5480
+ */
5481
+ function parse$1(header, options) {
5482
+ const stopFlags = SEMI_FLAG | (options?.comma === true ? COMMA_FLAG : 0);
5483
+ const len = header.length;
5484
+ let valueStart = options?.start ?? 0;
5485
+ while ((CHAR_MAP[header.charCodeAt(valueStart)] & OWS) !== 0) valueStart++;
5486
+ let index = valueStart;
5487
+ let typeFlags = 0;
5488
+ let whitespace = -1;
5489
+ let stop = options?.parameters === false ? COMMA_FLAG : 0;
5490
+ while (index < len) {
5491
+ const code = header.charCodeAt(index);
5492
+ const flags = CHAR_MAP[code];
5493
+ if ((flags & stopFlags) !== 0) {
5494
+ stop |= flags & COMMA_FLAG;
5495
+ break;
5496
+ }
5497
+ if ((flags & OWS) !== 0) {
5498
+ if (whitespace === -1) whitespace = index;
5499
+ } else whitespace = -1;
5500
+ typeFlags |= code & NON_ASCII | flags;
5501
+ index++;
5502
+ }
5503
+ const valueEnd = whitespace === -1 ? index : whitespace;
5504
+ const value = header.slice(valueStart, valueEnd);
5505
+ const type = (typeFlags & CASE_FLAGS) === 0 ? value : value.toLowerCase();
5506
+ if (index === len || stop !== 0) return {
5507
+ type,
5508
+ index,
5509
+ parameters: new NullObject()
5510
+ };
5511
+ return parseParameters(header, type, index, len, stopFlags);
5199
5512
  }
5200
- /** The base repository the pull request lives in, taken from its url (a fork's own remote is not it). */
5201
- function repoOfPrUrl(url) {
5202
- const m = /^\/([^/]+)\/([^/]+)\/pull\/\d+/.exec(new URL(url).pathname);
5203
- if (!m) throw new GithubError(`cannot read owner/repo from the pull request url: ${url}`, 502);
5513
+ /**
5514
+ * Parses the parameters of a `Content-Type` header starting at the given index.
5515
+ */
5516
+ function parseParameters(header, type, index, len, stopFlags) {
5517
+ const parameters = new NullObject();
5518
+ parameter: while (index < len) {
5519
+ index++;
5520
+ while ((CHAR_MAP[header.charCodeAt(index)] & OWS) !== 0) index++;
5521
+ const keyStart = index;
5522
+ let keyFlags = 0;
5523
+ let keyWhitespace = -1;
5524
+ while (index < len) {
5525
+ const code = header.charCodeAt(index);
5526
+ const flags = CHAR_MAP[code];
5527
+ if ((flags & stopFlags) !== 0) {
5528
+ if ((flags & COMMA_FLAG) !== 0) break parameter;
5529
+ continue parameter;
5530
+ }
5531
+ if (code === EQ) {
5532
+ const keyEnd = keyWhitespace === -1 ? index : keyWhitespace;
5533
+ const value = header.slice(keyStart, keyEnd);
5534
+ const key = (keyFlags & CASE_FLAGS) === 0 ? value : value.toLowerCase();
5535
+ index++;
5536
+ while ((CHAR_MAP[header.charCodeAt(index)] & OWS) !== 0) index++;
5537
+ if (index < len && header.charCodeAt(index) === DQUOTE) {
5538
+ const quotedStart = ++index;
5539
+ let escaped = false;
5540
+ while (index < len) {
5541
+ const code = header.charCodeAt(index);
5542
+ if (code === DQUOTE) {
5543
+ if (parameters[key] === void 0) parameters[key] = escaped ? unescapeQuotedPairs(header, quotedStart, index) : header.slice(quotedStart, index);
5544
+ index++;
5545
+ let stop = 0;
5546
+ while (index < len) {
5547
+ const code = header.charCodeAt(index);
5548
+ const flags = CHAR_MAP[code];
5549
+ if ((flags & stopFlags) !== 0) {
5550
+ stop = flags & COMMA_FLAG;
5551
+ break;
5552
+ }
5553
+ index++;
5554
+ }
5555
+ if (stop !== 0) break parameter;
5556
+ continue parameter;
5557
+ }
5558
+ if (code === BSLASH && index + 1 < len) {
5559
+ escaped = true;
5560
+ index += 2;
5561
+ continue;
5562
+ }
5563
+ index++;
5564
+ }
5565
+ continue parameter;
5566
+ }
5567
+ const valueStart = index;
5568
+ let stop = 0;
5569
+ let valueWhitespace = -1;
5570
+ while (index < len) {
5571
+ const code = header.charCodeAt(index);
5572
+ const flags = CHAR_MAP[code];
5573
+ if ((flags & stopFlags) !== 0) {
5574
+ stop = flags & COMMA_FLAG;
5575
+ break;
5576
+ }
5577
+ if ((flags & OWS) !== 0) {
5578
+ if (valueWhitespace === -1) valueWhitespace = index;
5579
+ } else valueWhitespace = -1;
5580
+ index++;
5581
+ }
5582
+ if (parameters[key] === void 0) {
5583
+ const valueEnd = valueWhitespace === -1 ? index : valueWhitespace;
5584
+ parameters[key] = header.slice(valueStart, valueEnd);
5585
+ }
5586
+ if (stop !== 0) break parameter;
5587
+ continue parameter;
5588
+ }
5589
+ if ((flags & OWS) !== 0) {
5590
+ if (keyWhitespace === -1) keyWhitespace = index;
5591
+ } else keyWhitespace = -1;
5592
+ keyFlags |= code & NON_ASCII | flags;
5593
+ index++;
5594
+ }
5595
+ }
5204
5596
  return {
5205
- owner: m[1],
5206
- repo: m[2]
5597
+ type,
5598
+ index,
5599
+ parameters
5207
5600
  };
5208
5601
  }
5209
- /** Serializes validation and writes so overlapping exports cannot add the same thread twice. */
5210
- var GithubExporter = class {
5211
- queue = Promise.resolve();
5212
- export(prepare) {
5213
- const result = this.queue.then(async () => exportToGithub(await prepare()));
5214
- this.queue = result.catch(() => {});
5215
- return result;
5602
+ /**
5603
+ * Remove backslashes from quoted pairs in a known-terminated quoted string body.
5604
+ */
5605
+ function unescapeQuotedPairs(str, start, end) {
5606
+ let result = "";
5607
+ for (let index = start; index < end; index++) if (str.charCodeAt(index) === BSLASH) {
5608
+ result += str.slice(start, index);
5609
+ start = ++index;
5216
5610
  }
5217
- };
5611
+ return result + str.slice(start, end);
5612
+ }
5613
+ //#endregion
5614
+ //#region node_modules/json-with-bigint/json-with-bigint.js
5615
+ const intRegex = /^-?\d+$/;
5616
+ const noiseValue = /^-?\d+n+$/;
5617
+ const originalStringify = JSON.stringify;
5618
+ const originalParse = JSON.parse;
5619
+ const customFormat = /^-?\d+n$/;
5620
+ const bigIntsStringify = /([\[:])?"(-?\d+)n"($|\s*[,\}\]])/g;
5621
+ const noiseStringify = /([\[:])?("-?\d+n+)n("$|"\s*[,\}\]])/g;
5218
5622
  /**
5219
- * Adds the threads to a *pending* review on the active PR, creating the pending review
5220
- * when there is none. The review is never submitted: the human opens the PR and submits
5221
- * it themselves. The caller validates the comparison inside the export queue before posting.
5623
+ * @typedef {(this: any, key: string | number | undefined, value: any) => any} Replacer
5624
+ * @typedef {(key: string | number | undefined, value: any, context?: { source: string }) => any} Reviver
5625
+ */
5626
+ /**
5627
+ * Checks if a value is unstringifiable according to native JSON.stringify rules.
5222
5628
  *
5223
- * Re-exporting matches a hidden thread ID and its anchor in the pending review.
5224
- * A matching comment is left alone when its body matches and rewritten when it does not, so
5225
- * editing a thread and exporting again does not stack a second comment on the line. Comments
5226
- * of an already-submitted review are out of reach — those would have to be replied to.
5629
+ * @param {any} val The value to check.
5630
+ * @returns {boolean} True if the value is undefined, a function, or a symbol.
5227
5631
  */
5228
- async function exportToGithub({ snap, pullRequest: pr, threads, threadIds, run = runGh }) {
5229
- if (snap.newSha === "worktree") throw new GithubError("GitHub cannot anchor comments to the worktree");
5230
- const { review, comments, ids, skipped } = buildReview(threads, snap.newSha, threadIds);
5231
- if (comments.length === 0) throw new GithubError(skipped.length ? `nothing to post: ${describe(skipped)}` : "nothing to post", 400);
5232
- for (const [i, comment] of comments.entries()) comment.body += `\n\n${threadMarker(ids[i])}`;
5233
- const pending = await findPendingReview(run, snap.root, pr);
5234
- if (!pending) {
5235
- const created = await createPendingReview(run, snap.root, pr.repository, pr.number, review);
5236
- let appended = 0;
5237
- try {
5238
- for (const c of comments.filter(isFileComment)) {
5239
- await addToPendingReview(run, snap.root, created.id, c);
5240
- appended++;
5632
+ const isUnstringifiable = (val) => val === void 0 || typeof val === "function" || typeof val === "symbol";
5633
+ /**
5634
+ * Checks if a value is a native JSON.rawJSON object (Node.js 22+).
5635
+ *
5636
+ * @param {any} val The value to check.
5637
+ * @returns {boolean} True if the value is a RawJSON instance.
5638
+ */
5639
+ const isRawJSON = (val) => val !== null && typeof val === "object" && val.constructor && val.constructor.name === "RawJSON";
5640
+ /**
5641
+ * Iteratively converts a JS value to a JSON string.
5642
+ * Used as a fallback when the native JSON.stringify hits the Maximum Call Stack size.
5643
+ * Fully compliant with JSON formatting (space), replacers, and toJSON behaviors.
5644
+ *
5645
+ * @param {any} rootValue The value to stringify.
5646
+ * @param {Replacer | Array<string | number> | null} [replacer] User's custom replacer function.
5647
+ * @param {string | number} [spaceParam] Indentation for pretty-printing.
5648
+ * @returns {string | undefined} The generated JSON string.
5649
+ */
5650
+ const stringifyIteratively = (rootValue, replacer, spaceParam) => {
5651
+ let space = "";
5652
+ if (typeof spaceParam === "number") space = " ".repeat(Math.min(10, Math.max(0, Math.floor(spaceParam))));
5653
+ else if (typeof spaceParam === "string") space = spaceParam.slice(0, 10);
5654
+ const isFunctionReplacer = typeof replacer === "function";
5655
+ const propertyList = Array.isArray(replacer) ? new Set(replacer.map(String)) : null;
5656
+ /**
5657
+ * Prepares a value for stringification by resolving toJSON, handling BigInts,
5658
+ * applying custom replacers, and unwrapping primitive objects.
5659
+ *
5660
+ * @param {object|Array} parent The parent object or array holding the value.
5661
+ * @param {string} key The key associated with the value.
5662
+ * @param {any} val The raw value to process.
5663
+ * @returns {any} The processed value ready for stringification.
5664
+ */
5665
+ const prepareVal = (parent, key, val) => {
5666
+ if (val !== null && typeof val === "object" && typeof val.toJSON === "function") val = val.toJSON(key);
5667
+ if (typeof val === "string" && noiseValue.test(val)) return val + "n";
5668
+ if (typeof val === "bigint") {
5669
+ if ("rawJSON" in JSON) return JSON.rawJSON(val.toString());
5670
+ return val.toString() + "n";
5671
+ }
5672
+ if (isFunctionReplacer) val = replacer.call(parent, key, val);
5673
+ if (val !== null && typeof val === "object") {
5674
+ if (val instanceof Number || val instanceof String || val instanceof Boolean) val = val.valueOf();
5675
+ }
5676
+ return val;
5677
+ };
5678
+ const rootProcessed = prepareVal({ "": rootValue }, "", rootValue);
5679
+ if (isUnstringifiable(rootProcessed)) return;
5680
+ const isRootPrimitive = rootProcessed === null || typeof rootProcessed !== "object";
5681
+ const isRootNativeRawJSON = isRawJSON(rootProcessed);
5682
+ if (isRootPrimitive || isRootNativeRawJSON) return originalStringify(rootProcessed);
5683
+ const chunks = [];
5684
+ let level = 0;
5685
+ const stack = [{
5686
+ parent: { "": rootProcessed },
5687
+ key: "",
5688
+ val: rootProcessed,
5689
+ isArray: Array.isArray(rootProcessed),
5690
+ keys: Array.isArray(rootProcessed) ? null : Object.keys(rootProcessed),
5691
+ index: 0,
5692
+ first: true
5693
+ }];
5694
+ const visited = new WeakSet([rootProcessed]);
5695
+ while (stack.length > 0) {
5696
+ const node = stack[stack.length - 1];
5697
+ if (node.index === 0) {
5698
+ chunks.push(node.isArray ? "[" : "{");
5699
+ level++;
5700
+ }
5701
+ let isDone = false;
5702
+ if (node.isArray) {
5703
+ if (node.index < node.val.length) {
5704
+ if (!node.first) chunks.push(",");
5705
+ if (space) chunks.push("\n" + space.repeat(level));
5706
+ const childRaw = node.val[node.index];
5707
+ const childVal = prepareVal(node.val, String(node.index), childRaw);
5708
+ if (isUnstringifiable(childVal)) {
5709
+ chunks.push("null");
5710
+ node.first = false;
5711
+ node.index++;
5712
+ } else {
5713
+ const isComplexObject = childVal !== null && typeof childVal === "object";
5714
+ const isNativeRaw = isRawJSON(childVal);
5715
+ if (isComplexObject && !isNativeRaw) {
5716
+ if (visited.has(childVal)) throw new TypeError("Converting circular structure to JSON");
5717
+ visited.add(childVal);
5718
+ stack.push({
5719
+ parent: node.val,
5720
+ key: String(node.index),
5721
+ val: childVal,
5722
+ isArray: Array.isArray(childVal),
5723
+ keys: Array.isArray(childVal) ? null : Object.keys(childVal),
5724
+ index: 0,
5725
+ first: true
5726
+ });
5727
+ node.first = false;
5728
+ node.index++;
5729
+ } else {
5730
+ chunks.push(originalStringify(childVal));
5731
+ node.first = false;
5732
+ node.index++;
5733
+ }
5734
+ }
5735
+ } else isDone = true;
5736
+ } else {
5737
+ while (node.index < node.keys.length) {
5738
+ const k = node.keys[node.index++];
5739
+ if (propertyList && !propertyList.has(k)) continue;
5740
+ const childRaw = node.val[k];
5741
+ const childVal = prepareVal(node.val, k, childRaw);
5742
+ if (isUnstringifiable(childVal)) continue;
5743
+ if (!node.first) chunks.push(",");
5744
+ if (space) chunks.push("\n" + space.repeat(level) + originalStringify(k) + ": ");
5745
+ else chunks.push(originalStringify(k) + ":");
5746
+ const isComplexObject = childVal !== null && typeof childVal === "object";
5747
+ const isNativeRaw = isRawJSON(childVal);
5748
+ if (isComplexObject && !isNativeRaw) {
5749
+ if (visited.has(childVal)) throw new TypeError("Converting circular structure to JSON");
5750
+ visited.add(childVal);
5751
+ stack.push({
5752
+ parent: node.val,
5753
+ key: k,
5754
+ val: childVal,
5755
+ isArray: Array.isArray(childVal),
5756
+ keys: Array.isArray(childVal) ? null : Object.keys(childVal),
5757
+ index: 0,
5758
+ first: true
5759
+ });
5760
+ node.first = false;
5761
+ break;
5762
+ } else {
5763
+ chunks.push(originalStringify(childVal));
5764
+ node.first = false;
5765
+ }
5241
5766
  }
5242
- } catch (e) {
5243
- const detail = e instanceof Error ? e.message : String(e);
5244
- throw new GithubError(`created the pending review with ${review.comments.length + appended} comments, then ${detail}`, 502);
5767
+ if (node.index >= node.keys.length && stack[stack.length - 1] === node) isDone = true;
5768
+ }
5769
+ if (isDone) {
5770
+ level--;
5771
+ if (!node.first && space) chunks.push("\n" + space.repeat(level));
5772
+ chunks.push(node.isArray ? "]" : "}");
5773
+ visited.delete(node.val);
5774
+ stack.pop();
5245
5775
  }
5246
- return {
5247
- url: pr.url,
5248
- posted: comments.length,
5249
- updated: 0,
5250
- review: "created",
5251
- skipped
5252
- };
5253
5776
  }
5254
- const existing = await pendingComments(run, snap.root, pr, pending.id);
5255
- const add = [];
5256
- let updated = 0;
5257
- let posted = 0;
5777
+ return chunks.join("");
5778
+ };
5779
+ /**
5780
+ * Converts a JavaScript value to a JSON string.
5781
+ *
5782
+ * Supports serialization of BigInt values using two strategies:
5783
+ * 1. Custom format "123n" → "123" (universal fallback)
5784
+ * 2. Native JSON.rawJSON() (Node.js 22+, fastest) when available
5785
+ *
5786
+ * All other values are serialized exactly like native JSON.stringify().
5787
+ *
5788
+ * @param {*} value The value to convert to a JSON string.
5789
+ * @param {Replacer | Array<string | number> | null} [replacer]
5790
+ * A function that alters the behavior of the stringification process,
5791
+ * or an array of strings/numbers to indicate properties to exclude.
5792
+ * @param {string | number} [space]
5793
+ * A string or number to specify indentation or pretty-printing.
5794
+ * @returns {string} The JSON string representation.
5795
+ */
5796
+ const JSONStringify = (value, replacer, space) => {
5258
5797
  try {
5259
- for (const [i, c] of comments.entries()) {
5260
- const id = ids[i];
5261
- const at = existing.get(anchorKey(c));
5262
- const index = at?.findIndex((comment) => comment.body.trimEnd().endsWith(threadMarker(id))) ?? -1;
5263
- const hit = index < 0 ? void 0 : at.splice(index, 1)[0];
5264
- if (!hit) {
5265
- add.push(c);
5266
- continue;
5267
- }
5268
- if (hit.body.trim() === c.body.trim()) {
5269
- skipped.push({
5270
- id,
5271
- reason: "already in the review"
5272
- });
5273
- continue;
5274
- }
5275
- await graphql(run, snap.root, UPDATE_COMMENT, { input: {
5276
- pullRequestReviewCommentId: hit.nodeId,
5277
- body: c.body
5278
- } });
5279
- updated++;
5280
- }
5281
- for (const c of add) {
5282
- await addToPendingReview(run, snap.root, pending.id, c);
5283
- posted++;
5798
+ if ("rawJSON" in JSON) return originalStringify(value, (key, val) => {
5799
+ if (typeof val === "bigint") return JSON.rawJSON(val.toString());
5800
+ if (typeof replacer === "function") return replacer(key, val);
5801
+ if (Array.isArray(replacer) && replacer.includes(key)) return val;
5802
+ return val;
5803
+ }, space);
5804
+ if (!value) return originalStringify(value, replacer, space);
5805
+ return originalStringify(value, (key, val) => {
5806
+ if (typeof val === "string" && noiseValue.test(val)) return val.toString() + "n";
5807
+ if (typeof val === "bigint") return val.toString() + "n";
5808
+ if (typeof replacer === "function") return replacer(key, val);
5809
+ if (Array.isArray(replacer) && replacer.includes(key)) return val;
5810
+ return val;
5811
+ }, space).replace(bigIntsStringify, "$1$2$3").replace(noiseStringify, "$1$2$3");
5812
+ } catch (error) {
5813
+ if (error instanceof RangeError) {
5814
+ const convertedJSON = stringifyIteratively(value, replacer, space);
5815
+ if (convertedJSON === void 0) return void 0;
5816
+ if ("rawJSON" in JSON) return convertedJSON;
5817
+ return convertedJSON.replace(bigIntsStringify, "$1$2$3").replace(noiseStringify, "$1$2$3");
5284
5818
  }
5285
- } catch (e) {
5286
- if (updated === 0 && posted === 0) throw e;
5287
- const detail = e instanceof Error ? e.message : String(e);
5288
- throw new GithubError(`updated ${updated} and added ${posted} comments in the pending review, then ${detail}`, 502);
5819
+ throw error;
5289
5820
  }
5290
- return {
5291
- url: pr.url,
5292
- posted,
5293
- updated,
5294
- review: "existing",
5295
- skipped
5296
- };
5297
- }
5298
- function threadMarker(id) {
5299
- return `<!-- diffle-thread:${encodeURIComponent(id)} -->`;
5300
- }
5301
- /** The anchor must still match before we rewrite an exported thread. */
5302
- function anchorKey(c) {
5303
- if (isFileComment(c)) return [c.path, "FILE"].join("\0");
5304
- return [
5305
- c.path,
5306
- c.side,
5307
- c.start_line ?? c.line,
5308
- c.line
5309
- ].join("\0");
5310
- }
5311
- const PENDING_QUERY = `query($owner:String!,$repo:String!,$number:Int!){
5312
- viewer{login}
5313
- repository(owner:$owner,name:$repo){pullRequest(number:$number){reviews(last:20,states:[PENDING]){nodes{id author{login}}}}}
5314
- }`;
5821
+ };
5822
+ const featureCache = /* @__PURE__ */ new Map();
5823
+ /**
5824
+ * Detects if the current JSON.parse implementation supports the context.source feature.
5825
+ *
5826
+ * Uses toString() fingerprinting to cache results and automatically detect runtime
5827
+ * replacements of JSON.parse (polyfills, mocks, etc.).
5828
+ *
5829
+ * @returns {boolean} true if context.source is supported, false otherwise.
5830
+ */
5831
+ const isContextSourceSupported = () => {
5832
+ const parseFingerprint = JSON.parse.toString();
5833
+ if (featureCache.has(parseFingerprint)) return featureCache.get(parseFingerprint);
5834
+ try {
5835
+ const result = JSON.parse("1", (_, __, context) => !!context?.source && context.source === "1");
5836
+ featureCache.set(parseFingerprint, result);
5837
+ return result;
5838
+ } catch {
5839
+ featureCache.set(parseFingerprint, false);
5840
+ return false;
5841
+ }
5842
+ };
5315
5843
  /**
5316
- * The viewer's own pending review on the PR, or null. GitHub hides other people's pending
5317
- * reviews, and the login check makes sure of it: appending to someone else's draft would
5318
- * put our comments in their review.
5844
+ * Reviver function that converts custom-format BigInt strings back to BigInt values.
5845
+ * Also handles "noise" strings that accidentally match the BigInt format.
5846
+ *
5847
+ * @param {string | number | undefined} key The object key.
5848
+ * @param {*} value The value being parsed.
5849
+ * @param {object} [context] Parse context (if supported by JSON.parse).
5850
+ * @param {Reviver} [userReviver] User's custom reviver function.
5851
+ * @returns {any} The transformed value.
5319
5852
  */
5320
- async function findPendingReview(run, cwd, pr) {
5321
- const { owner, repo } = repoOfPrUrl(pr.url);
5322
- const data = await graphql(run, cwd, PENDING_QUERY, {
5323
- owner,
5324
- repo,
5325
- number: pr.number
5853
+ const convertMarkedBigIntsReviver = (key, value, context, userReviver) => {
5854
+ if (typeof value === "string" && customFormat.test(value)) return BigInt(value.slice(0, -1));
5855
+ if (typeof value === "string" && noiseValue.test(value)) return value.slice(0, -1);
5856
+ if (!(typeof userReviver === "function")) return value;
5857
+ return userReviver(key, value, context);
5858
+ };
5859
+ /**
5860
+ * Fast JSON.parse implementation (~2x faster than classic fallback).
5861
+ * Uses JSON.parse's context.source feature to detect integers and convert
5862
+ * large numbers directly to BigInt without string manipulation.
5863
+ *
5864
+ * Does not support legacy custom format from v1 of this library.
5865
+ *
5866
+ * @param {string} text JSON string to parse.
5867
+ * @param {Reviver} [reviver] Transform function to apply to each value.
5868
+ * @returns {any} Parsed JavaScript value.
5869
+ */
5870
+ const JSONParseV2 = (text, reviver) => {
5871
+ return JSON.parse(text, (key, value, context) => {
5872
+ const isNumber = typeof value === "number";
5873
+ const isOutOfBounds = value > Number.MAX_SAFE_INTEGER || value < Number.MIN_SAFE_INTEGER;
5874
+ const isBigNumber = isNumber && isOutOfBounds;
5875
+ const isInt = context && intRegex.test(context.source);
5876
+ if (isBigNumber && isInt) return BigInt(context.source);
5877
+ if (!(typeof reviver === "function")) return value;
5878
+ return reviver(key, value, context);
5326
5879
  });
5327
- const login = data.viewer?.login;
5328
- for (const node of data.repository?.pullRequest?.reviews?.nodes ?? []) if (node?.id && node.author?.login === login) return { id: node.id };
5329
- return null;
5330
- }
5331
- const THREADS_QUERY = `query($owner:String!,$repo:String!,$number:Int!,$after:String){
5332
- repository(owner:$owner,name:$repo){pullRequest(number:$number){reviewThreads(first:100,after:$after){
5333
- pageInfo{hasNextPage endCursor}
5334
- nodes{path line startLine diffSide subjectType comments(first:1){nodes{id body pullRequestReview{id}}}}
5335
- }}}
5336
- }`;
5880
+ };
5881
+ const MAX_INT = Number.MAX_SAFE_INTEGER.toString();
5882
+ const MAX_DIGITS = MAX_INT.length;
5883
+ const stringsOrLargeNumbers = /"(?:[^"\\]|\\.)*"|-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?/g;
5884
+ const noiseValueWithQuotes = /^"-?\d+n+"$/;
5337
5885
  /**
5338
- * The pending review's own comments, by anchor. It has to be `reviewThreads`: it is the only view
5339
- * that reports a *pending* comment's real position — REST's review-comment list leaves `line`,
5340
- * `side` and `start_line` null for one, and the GraphQL comment type has no side field at all.
5341
- * A failed or incomplete lookup aborts the export: absence is safe to infer only from a complete read.
5886
+ * Iteratively traverses the parsed object bottom-up (post-order),
5887
+ * emulating the native JSON.parse reviver behavior.
5888
+ * This avoids Call Stack overflows (RangeError) on deeply nested structures.
5889
+ *
5890
+ * @param {any} parsed The natively parsed JSON object.
5891
+ * @param {Reviver} [userReviver] User's custom reviver function.
5892
+ * @returns {any} The fully processed object.
5342
5893
  */
5343
- async function pendingComments(run, cwd, pr, reviewId) {
5344
- const by = /* @__PURE__ */ new Map();
5345
- const { owner, repo } = repoOfPrUrl(pr.url);
5346
- let after = null;
5347
- for (let page = 0; page < 10; page++) {
5348
- const threads = (await graphql(run, cwd, THREADS_QUERY, {
5349
- owner,
5350
- repo,
5351
- number: pr.number,
5352
- after
5353
- })).repository?.pullRequest?.reviewThreads;
5354
- if (!threads?.nodes || typeof threads.pageInfo?.hasNextPage !== "boolean") throw new GithubError("cannot read the complete pending review", 502);
5355
- for (const t of threads.nodes) {
5356
- const c = t?.comments?.nodes?.[0];
5357
- if (!t?.path || !c?.id || c.pullRequestReview?.id !== reviewId) continue;
5358
- const key = pendingKey(t);
5359
- if (key == null) continue;
5360
- const at = by.get(key);
5361
- if (at) at.push({
5362
- nodeId: c.id,
5363
- body: c.body ?? ""
5364
- });
5365
- else by.set(key, [{
5366
- nodeId: c.id,
5367
- body: c.body ?? ""
5368
- }]);
5894
+ const applyReviverIteratively = (parsed, userReviver) => {
5895
+ const rootHolder = { "": parsed };
5896
+ const stack = [{
5897
+ parent: rootHolder,
5898
+ key: "",
5899
+ visited: false
5900
+ }];
5901
+ while (stack.length > 0) {
5902
+ const node = stack[stack.length - 1];
5903
+ if (!node.visited) {
5904
+ node.visited = true;
5905
+ const value = node.parent[node.key];
5906
+ if (value !== null && typeof value === "object") {
5907
+ const keys = Object.keys(value);
5908
+ for (let i = keys.length - 1; i >= 0; i--) stack.push({
5909
+ parent: value,
5910
+ key: keys[i],
5911
+ visited: false
5912
+ });
5913
+ }
5914
+ } else {
5915
+ const { parent, key } = node;
5916
+ let value = parent[key];
5917
+ if (typeof value === "string") {
5918
+ if (customFormat.test(value)) value = BigInt(value.slice(0, -1));
5919
+ else if (noiseValue.test(value)) value = value.slice(0, -1);
5920
+ }
5921
+ if (typeof userReviver === "function") value = userReviver.call(parent, key, value);
5922
+ if (value === void 0) delete parent[key];
5923
+ else parent[key] = value;
5924
+ stack.pop();
5369
5925
  }
5370
- if (!threads.pageInfo.hasNextPage) return by;
5371
- if (!threads.pageInfo.endCursor || threads.pageInfo.endCursor === after) throw new GithubError("cannot read the next page of the pending review", 502);
5372
- after = threads.pageInfo.endCursor;
5373
5926
  }
5374
- throw new GithubError("pending review exceeds the 1000-thread lookup limit; export stopped without changes", 502);
5375
- }
5376
- /** The anchor key of a thread already in the review, or null for one GitHub reports without a position. */
5377
- function pendingKey(t) {
5378
- const path = t.path;
5379
- if (t.subjectType === "FILE") return anchorKey({
5380
- path,
5381
- subject_type: "file",
5382
- body: ""
5383
- });
5384
- if (t.line == null) return null;
5385
- return anchorKey({
5386
- path,
5387
- side: t.diffSide === "LEFT" ? "LEFT" : "RIGHT",
5388
- line: t.line,
5389
- start_line: t.startLine ?? void 0,
5390
- body: ""
5391
- });
5392
- }
5393
- const UPDATE_COMMENT = `mutation($input:UpdatePullRequestReviewCommentInput!){updatePullRequestReviewComment(input:$input){pullRequestReviewComment{id}}}`;
5927
+ return rootHolder[""];
5928
+ };
5394
5929
  /**
5395
- * No `event` in the body, so GitHub keeps the new review pending with all of its comments.
5396
- * Resolves the review's node id, for the comments REST cannot carry.
5930
+ * Pre-processes the JSON string to mark large numbers with an 'n' suffix.
5931
+ *
5932
+ * @param {string} text The raw JSON string.
5933
+ * @returns {string} The serialized string with marked BigInts.
5397
5934
  */
5398
- async function createPendingReview(run, cwd, repository, number, review) {
5399
- const out = await run([
5400
- "api",
5401
- "--method",
5402
- "POST",
5403
- `repos/${repository}/pulls/${number}/reviews`,
5404
- "--input",
5405
- "-"
5406
- ], {
5407
- cwd,
5408
- input: JSON.stringify(review)
5935
+ const serializeBigInts = (text) => {
5936
+ return text.replace(stringsOrLargeNumbers, (match, digits, fractional, exponential) => {
5937
+ const isString = match[0] === "\"";
5938
+ if (isString && noiseValueWithQuotes.test(match)) return match.substring(0, match.length - 1) + "n\"";
5939
+ const hasFractionalOrExponential = fractional || exponential;
5940
+ const isLessThanMaxSafeInt = digits && (digits.length < MAX_DIGITS || digits.length === MAX_DIGITS && digits <= MAX_INT);
5941
+ if (isString || hasFractionalOrExponential || isLessThanMaxSafeInt) return match;
5942
+ return "\"" + match + "n\"";
5409
5943
  });
5410
- let id;
5944
+ };
5945
+ /**
5946
+ * Converts a JSON string into a JavaScript value.
5947
+ *
5948
+ * Supports parsing of large integers using two strategies:
5949
+ * 1. Classic fallback: Marks large numbers with "123n" format, then converts to BigInt
5950
+ * 2. Fast path (JSONParseV2): Uses context.source feature (~2x faster) when available
5951
+ *
5952
+ * All other JSON values are parsed exactly like native JSON.parse().
5953
+ *
5954
+ * @param {string} text A valid JSON string.
5955
+ * @param {Reviver} [reviver]
5956
+ * A function that transforms the results. This function is called for each member
5957
+ * of the object. If a member contains nested objects, the nested objects are
5958
+ * transformed before the parent object is.
5959
+ * @returns {any} The parsed JavaScript value.
5960
+ * @throws {SyntaxError} If text is not valid JSON.
5961
+ */
5962
+ const JSONParse = (text, reviver) => {
5963
+ if (!text) return originalParse(text, reviver);
5411
5964
  try {
5412
- id = JSON.parse(out).node_id;
5413
- } catch {
5414
- throw new GithubError(`unexpected output from gh api: ${out.slice(0, 200)}`, 502);
5965
+ if (isContextSourceSupported()) return JSONParseV2(text, reviver);
5966
+ const serializedData = serializeBigInts(text);
5967
+ return originalParse(serializedData, (key, value, context) => convertMarkedBigIntsReviver(key, value, context, reviver));
5968
+ } catch (error) {
5969
+ if (error instanceof RangeError) {
5970
+ const serializedData = serializeBigInts(text);
5971
+ const parsed = originalParse(serializedData);
5972
+ return applyReviverIteratively(parsed, reviver);
5973
+ }
5974
+ throw error;
5415
5975
  }
5416
- if (typeof id !== "string") throw new GithubError(`unexpected output from gh api: ${out.slice(0, 200)}`, 502);
5417
- return { id };
5976
+ };
5977
+ //#endregion
5978
+ //#region node_modules/@octokit/request-error/dist-src/index.js
5979
+ var RequestError$1 = class extends Error {
5980
+ name;
5981
+ /**
5982
+ * http status code
5983
+ */
5984
+ status;
5985
+ /**
5986
+ * Request options that lead to the error.
5987
+ */
5988
+ request;
5989
+ /**
5990
+ * Response object if a response was received
5991
+ */
5992
+ response;
5993
+ constructor(message, statusCode, options) {
5994
+ super(message, { cause: options.cause });
5995
+ this.name = "HttpError";
5996
+ this.status = Number.parseInt(statusCode);
5997
+ if (Number.isNaN(this.status)) this.status = 0;
5998
+ /* v8 ignore else -- @preserve -- Bug with vitest coverage where it sees an else branch that doesn't exist */
5999
+ if ("response" in options) this.response = options.response;
6000
+ const requestCopy = Object.assign({}, options.request);
6001
+ if (options.request.headers.authorization) requestCopy.headers = Object.assign({}, options.request.headers, { authorization: options.request.headers.authorization.replace(/(?<! ) .*$/, " [REDACTED]") });
6002
+ requestCopy.url = requestCopy.url.replace(/\bclient_secret=\w+/g, "client_secret=[REDACTED]").replace(/\baccess_token=\w+/g, "access_token=[REDACTED]");
6003
+ this.request = requestCopy;
6004
+ }
6005
+ };
6006
+ //#endregion
6007
+ //#region node_modules/@octokit/request/dist-bundle/index.js
6008
+ var defaults_default = { headers: { "user-agent": `octokit-request.js/10.0.16 ${getUserAgent()}` } };
6009
+ function isPlainObject$1(value) {
6010
+ if (typeof value !== "object" || value === null) return false;
6011
+ if (Object.prototype.toString.call(value) !== "[object Object]") return false;
6012
+ const proto = Object.getPrototypeOf(value);
6013
+ if (proto === null) return true;
6014
+ const Ctor = Object.prototype.hasOwnProperty.call(proto, "constructor") && proto.constructor;
6015
+ return typeof Ctor === "function" && Ctor instanceof Ctor && Function.prototype.call(Ctor) === Function.prototype.call(value);
6016
+ }
6017
+ var noop$1 = () => "";
6018
+ async function fetchWrapper(requestOptions) {
6019
+ const fetch = requestOptions.request?.fetch || globalThis.fetch;
6020
+ if (!fetch) throw new Error("fetch is not set. Please pass a fetch implementation as new Octokit({ request: { fetch }}). Learn more at https://github.com/octokit/octokit.js/#fetch-missing");
6021
+ const log = requestOptions.request?.log || console;
6022
+ const parseSuccessResponseBody = requestOptions.request?.parseSuccessResponseBody !== false;
6023
+ const body = isPlainObject$1(requestOptions.body) || Array.isArray(requestOptions.body) ? JSONStringify(requestOptions.body) : requestOptions.body;
6024
+ const requestHeaders = Object.fromEntries(Object.entries(requestOptions.headers).map(([name, value]) => [name, String(value)]));
6025
+ let fetchResponse;
6026
+ try {
6027
+ fetchResponse = await fetch(requestOptions.url, {
6028
+ method: requestOptions.method,
6029
+ body,
6030
+ redirect: requestOptions.request?.redirect,
6031
+ headers: requestHeaders,
6032
+ signal: requestOptions.request?.signal,
6033
+ ...requestOptions.body && { duplex: "half" }
6034
+ });
6035
+ } catch (error) {
6036
+ let message = "Unknown Error";
6037
+ if (error instanceof Error) {
6038
+ if (error.name === "AbortError") {
6039
+ error.status = 500;
6040
+ throw error;
6041
+ }
6042
+ message = error.message;
6043
+ if (error.name === "TypeError" && "cause" in error) {
6044
+ if (error.cause instanceof Error) message = error.cause.message;
6045
+ else if (typeof error.cause === "string") message = error.cause;
6046
+ }
6047
+ }
6048
+ const requestError = new RequestError$1(message, 500, { request: requestOptions });
6049
+ requestError.cause = error;
6050
+ throw requestError;
6051
+ }
6052
+ const status = fetchResponse.status;
6053
+ const url = fetchResponse.url;
6054
+ const responseHeaders = {};
6055
+ for (const [key, value] of fetchResponse.headers) responseHeaders[key] = value;
6056
+ const octokitResponse = {
6057
+ url,
6058
+ status,
6059
+ headers: responseHeaders,
6060
+ data: ""
6061
+ };
6062
+ if ("deprecation" in responseHeaders) {
6063
+ const matches = responseHeaders.link && responseHeaders.link.match(/<([^<>]+)>; rel="deprecation"/);
6064
+ const deprecationLink = matches && matches.pop();
6065
+ log.warn(`[@octokit/request] "${requestOptions.method} ${requestOptions.url}" is deprecated. It is scheduled to be removed on ${responseHeaders.sunset}${deprecationLink ? `. See ${deprecationLink}` : ""}`);
6066
+ }
6067
+ if (status === 204 || status === 205) return octokitResponse;
6068
+ if (requestOptions.method === "HEAD") {
6069
+ if (status < 400) return octokitResponse;
6070
+ throw new RequestError$1(fetchResponse.statusText, status, {
6071
+ response: octokitResponse,
6072
+ request: requestOptions
6073
+ });
6074
+ }
6075
+ if (status === 304) {
6076
+ octokitResponse.data = await getResponseData(fetchResponse);
6077
+ throw new RequestError$1("Not modified", status, {
6078
+ response: octokitResponse,
6079
+ request: requestOptions
6080
+ });
6081
+ }
6082
+ if (status >= 400) {
6083
+ octokitResponse.data = await getResponseData(fetchResponse);
6084
+ throw new RequestError$1(toErrorMessage(octokitResponse.data), status, {
6085
+ response: octokitResponse,
6086
+ request: requestOptions
6087
+ });
6088
+ }
6089
+ octokitResponse.data = parseSuccessResponseBody ? await getResponseData(fetchResponse) : fetchResponse.body;
6090
+ return octokitResponse;
5418
6091
  }
5419
- const ADD_THREAD = `mutation($input:AddPullRequestReviewThreadInput!){addPullRequestReviewThread(input:$input){thread{id}}}`;
5420
- /** Appends one thread through GraphQL; REST cannot extend a pending review, nor place a file comment. */
5421
- async function addToPendingReview(run, cwd, reviewId, c) {
5422
- const input = {
5423
- pullRequestReviewId: reviewId,
5424
- path: c.path,
5425
- body: c.body
6092
+ async function getResponseData(response) {
6093
+ const contentType = response.headers.get("content-type");
6094
+ if (!contentType) return response.text().catch(noop$1);
6095
+ const mimetype = parse$1(contentType);
6096
+ if (isJSONResponse(mimetype)) {
6097
+ let text = "";
6098
+ try {
6099
+ text = await response.text();
6100
+ return JSONParse(text);
6101
+ } catch (err) {
6102
+ return text;
6103
+ }
6104
+ } else if (mimetype.type.startsWith("text/") || mimetype.parameters.charset?.toLowerCase() === "utf-8" && mimetype.type !== "application/octet-stream") return response.text().catch(noop$1);
6105
+ else return response.arrayBuffer().catch(
6106
+ /* v8 ignore next -- @preserve */
6107
+ () => /* @__PURE__ */ new ArrayBuffer(0)
6108
+ );
6109
+ }
6110
+ function isJSONResponse(mimetype) {
6111
+ return mimetype.type === "application/json" || mimetype.type === "application/scim+json";
6112
+ }
6113
+ function toErrorMessage(data) {
6114
+ if (typeof data === "string") return data;
6115
+ if (data instanceof ArrayBuffer) return "Unknown error";
6116
+ if (typeof data === "object" && data !== null && "message" in data) {
6117
+ const objectData = data;
6118
+ const suffix = "documentation_url" in objectData ? ` - ${objectData.documentation_url}` : "";
6119
+ return Array.isArray(objectData.errors) ? `${objectData.message}: ${objectData.errors.map((v) => JSON.stringify(v)).join(", ")}${suffix}` : `${objectData.message}${suffix}`;
6120
+ }
6121
+ return `Unknown error: ${JSON.stringify(data)}`;
6122
+ }
6123
+ function withDefaults$1(oldEndpoint, newDefaults) {
6124
+ const endpoint2 = oldEndpoint.defaults(newDefaults);
6125
+ const newApi = function(route, parameters) {
6126
+ const endpointOptions = endpoint2.merge(route, parameters);
6127
+ if (!endpointOptions.request || !endpointOptions.request.hook) return fetchWrapper(endpoint2.parse(endpointOptions));
6128
+ const request2 = (route2, parameters2) => {
6129
+ return fetchWrapper(endpoint2.parse(endpoint2.merge(route2, parameters2)));
6130
+ };
6131
+ Object.assign(request2, {
6132
+ endpoint: endpoint2,
6133
+ defaults: withDefaults$1.bind(null, endpoint2)
6134
+ });
6135
+ return endpointOptions.request.hook(request2, endpointOptions);
5426
6136
  };
5427
- if (isFileComment(c)) input.subjectType = "FILE";
5428
- else {
5429
- input.line = c.line;
5430
- input.side = c.side;
5431
- if (c.start_line != null) {
5432
- input.startLine = c.start_line;
5433
- input.startSide = c.start_side;
5434
- }
5435
- }
5436
- await graphql(run, cwd, ADD_THREAD, { input });
5437
- }
5438
- /** Runs a GraphQL document through `gh api graphql` and returns its `data`. */
5439
- async function graphql(run, cwd, query, variables) {
5440
- const out = await run([
5441
- "api",
5442
- "graphql",
5443
- "--input",
5444
- "-"
5445
- ], {
5446
- cwd,
5447
- input: JSON.stringify({
5448
- query,
5449
- variables
5450
- })
6137
+ return Object.assign(newApi, {
6138
+ endpoint: endpoint2,
6139
+ defaults: withDefaults$1.bind(null, endpoint2)
5451
6140
  });
5452
- let body;
5453
- try {
5454
- body = JSON.parse(out);
5455
- } catch {
5456
- throw new GithubError(`unexpected output from gh api graphql: ${out.slice(0, 200)}`, 502);
5457
- }
5458
- if (body.data == null) throw new GithubError(`unexpected output from gh api graphql: ${out.slice(0, 200)}`, 502);
5459
- return body.data;
5460
6141
  }
5461
- function describe(skipped) {
5462
- const counts = /* @__PURE__ */ new Map();
5463
- for (const s of skipped) counts.set(s.reason, (counts.get(s.reason) ?? 0) + 1);
5464
- return [...counts].map(([reason, n]) => `${n} ${reason}`).join(", ");
6142
+ var request = withDefaults$1(endpoint, defaults_default);
6143
+ /* v8 ignore next -- @preserve */
6144
+ /* v8 ignore else -- @preserve */
6145
+ //#endregion
6146
+ //#region node_modules/@octokit/graphql/dist-bundle/index.js
6147
+ var VERSION$1 = "0.0.0-development";
6148
+ function _buildMessageForResponseErrors(data) {
6149
+ return `Request failed due to following response errors:
6150
+ ` + data.errors.map((e) => ` - ${e.message}`).join("\n");
6151
+ }
6152
+ var GraphqlResponseError = class extends Error {
6153
+ constructor(request2, headers, response) {
6154
+ super(_buildMessageForResponseErrors(response));
6155
+ this.request = request2;
6156
+ this.headers = headers;
6157
+ this.response = response;
6158
+ this.errors = response.errors;
6159
+ this.data = response.data;
6160
+ if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor);
6161
+ }
6162
+ request;
6163
+ headers;
6164
+ response;
6165
+ name = "GraphqlResponseError";
6166
+ errors;
6167
+ data;
6168
+ };
6169
+ var NON_VARIABLE_OPTIONS = [
6170
+ "method",
6171
+ "baseUrl",
6172
+ "url",
6173
+ "headers",
6174
+ "request",
6175
+ "query",
6176
+ "mediaType",
6177
+ "operationName"
6178
+ ];
6179
+ var FORBIDDEN_VARIABLE_OPTIONS = [
6180
+ "query",
6181
+ "method",
6182
+ "url"
6183
+ ];
6184
+ var GHES_V3_SUFFIX_REGEX = /\/api\/v3\/?$/;
6185
+ function graphql(request2, query, options) {
6186
+ if (options) {
6187
+ if (typeof query === "string" && "query" in options) return Promise.reject(/* @__PURE__ */ new Error(`[@octokit/graphql] "query" cannot be used as variable name`));
6188
+ for (const key in options) {
6189
+ if (!FORBIDDEN_VARIABLE_OPTIONS.includes(key)) continue;
6190
+ return Promise.reject(/* @__PURE__ */ new Error(`[@octokit/graphql] "${key}" cannot be used as variable name`));
6191
+ }
6192
+ }
6193
+ const parsedOptions = typeof query === "string" ? Object.assign({ query }, options) : query;
6194
+ const requestOptions = Object.keys(parsedOptions).reduce((result, key) => {
6195
+ if (NON_VARIABLE_OPTIONS.includes(key)) {
6196
+ result[key] = parsedOptions[key];
6197
+ return result;
6198
+ }
6199
+ if (!result.variables) result.variables = {};
6200
+ result.variables[key] = parsedOptions[key];
6201
+ return result;
6202
+ }, {});
6203
+ const baseUrl = parsedOptions.baseUrl || request2.endpoint.DEFAULTS.baseUrl;
6204
+ if (GHES_V3_SUFFIX_REGEX.test(baseUrl)) requestOptions.url = baseUrl.replace(GHES_V3_SUFFIX_REGEX, "/api/graphql");
6205
+ return request2(requestOptions).then((response) => {
6206
+ if (response.data.errors) {
6207
+ const headers = {};
6208
+ for (const key of Object.keys(response.headers)) headers[key] = response.headers[key];
6209
+ throw new GraphqlResponseError(requestOptions, headers, response.data);
6210
+ }
6211
+ return response.data.data;
6212
+ });
5465
6213
  }
5466
- /** Spawns the local `gh`. A missing binary or a failing command becomes a GithubError carrying gh's own message. */
5467
- const runGh = (args, { cwd, input, timeoutMs }) => new Promise((resolve, reject) => {
5468
- const child = execFile("gh", args, {
5469
- cwd,
5470
- maxBuffer: 16777216,
5471
- timeout: timeoutMs
5472
- }, (err, stdout, stderr) => {
5473
- if (!err) return resolve(stdout);
5474
- if (err.code === "ENOENT") return reject(new GithubError("gh is not installed; see https://cli.github.com"));
5475
- const detail = (stderr || err.message).trim().split("\n")[0] ?? "";
5476
- reject(new GithubError(`gh ${args[0]} ${args[1] ?? ""} failed: ${detail}`.trim(), args[0] === "api" ? 502 : 409));
6214
+ function withDefaults(request2, newDefaults) {
6215
+ const newRequest = request2.defaults(newDefaults);
6216
+ const newApi = (query, options) => {
6217
+ return graphql(newRequest, query, options);
6218
+ };
6219
+ return Object.assign(newApi, {
6220
+ defaults: withDefaults.bind(null, newRequest),
6221
+ endpoint: newRequest.endpoint
5477
6222
  });
5478
- if (input != null) child.stdin?.end(input);
5479
- else child.stdin?.end();
6223
+ }
6224
+ withDefaults(request, {
6225
+ headers: { "user-agent": `octokit-graphql.js/${VERSION$1} ${getUserAgent()}` },
6226
+ method: "POST",
6227
+ url: "/graphql"
5480
6228
  });
5481
- const PR_FIELDS = "number,url,title,state,isDraft,baseRefOid,baseRefName,headRefName,headRefOid,headRepository,headRepositoryOwner";
5482
- /**
5483
- * `gh pr view` for a PR named by number, `#number`, URL or branch. Without a
5484
- * selector, the PR of the checked-out branch. Option-shaped selectors are
5485
- * refused so a stray flag never reaches gh.
5486
- */
5487
- async function viewPr(selector, cwd, run = runGh, timeoutMs) {
5488
- return parsePullRequest(parseJson(await run([
5489
- "pr",
5490
- "view",
5491
- ...selector == null ? [] : [normalizeSelector(selector)],
5492
- "--json",
5493
- PR_FIELDS
5494
- ], {
5495
- cwd,
5496
- timeoutMs
5497
- }), "view"));
5498
- }
5499
- /** Open pull requests whose head is the named branch in the named remote repository. */
5500
- async function listPrsForHead(baseRepo, base, head, headRepo, cwd, run = runGh, timeoutMs) {
5501
- const out = await run([
5502
- "pr",
5503
- "list",
5504
- "--repo",
5505
- baseRepo,
5506
- "--base",
5507
- base,
5508
- "--head",
5509
- head,
5510
- "--state",
5511
- "open",
5512
- "--limit",
5513
- "100",
5514
- "--json",
5515
- PR_FIELDS
5516
- ], {
5517
- cwd,
5518
- timeoutMs
5519
- });
5520
- const values = parseJson(out, "list");
5521
- if (!Array.isArray(values)) throw new GithubError(`unexpected output from gh pr list: ${out.slice(0, 200)}`, 502);
5522
- return values.flatMap((value) => {
5523
- const candidate = value;
5524
- const owner = candidate?.headRepositoryOwner?.login;
5525
- const name = candidate?.headRepository?.name;
5526
- if (typeof owner !== "string" || typeof name !== "string") throw new GithubError(`unexpected output from gh pr list: ${out.slice(0, 200)}`, 502);
5527
- if (`${owner}/${name}`.toLowerCase() !== headRepo.toLowerCase()) return [];
5528
- return [parsePullRequest(value)];
6229
+ function withCustomRequest(customRequest) {
6230
+ return withDefaults(customRequest, {
6231
+ method: "POST",
6232
+ url: "/graphql"
5529
6233
  });
5530
6234
  }
5531
- function normalizeSelector(selector) {
5532
- const s = selector.trim();
5533
- if (s.startsWith("-")) throw new GithubError(`not a pull request: ${selector}`, 400);
5534
- const num = /^#?(\d+)$/.exec(s);
5535
- return num ? num[1] : s;
6235
+ /* v8 ignore if -- @preserve */
6236
+ //#endregion
6237
+ //#region node_modules/@octokit/auth-token/dist-bundle/index.js
6238
+ var b64url = "(?:[a-zA-Z0-9_-]+)";
6239
+ var sep$1 = "\\.";
6240
+ var jwtRE = new RegExp(`^${b64url}${sep$1}${b64url}${sep$1}${b64url}$`);
6241
+ var isJWT = jwtRE.test.bind(jwtRE);
6242
+ async function auth(token) {
6243
+ const isApp = isJWT(token);
6244
+ const isInstallation = token.startsWith("v1.") || token.startsWith("ghs_");
6245
+ const isUserToServer = token.startsWith("ghu_");
6246
+ return {
6247
+ type: "token",
6248
+ token,
6249
+ tokenType: isApp ? "app" : isInstallation ? "installation" : isUserToServer ? "user-to-server" : "oauth"
6250
+ };
5536
6251
  }
5537
- function parseJson(out, command) {
5538
- try {
5539
- return JSON.parse(out);
5540
- } catch {
5541
- throw new GithubError(`unexpected output from gh pr ${command}: ${out.slice(0, 200)}`, 502);
6252
+ function withAuthorizationPrefix(token) {
6253
+ if (token.split(/\./).length === 3) return `bearer ${token}`;
6254
+ return `token ${token}`;
6255
+ }
6256
+ async function hook(token, request, route, parameters) {
6257
+ const endpoint = request.endpoint.merge(route, parameters);
6258
+ endpoint.headers.authorization = withAuthorizationPrefix(token);
6259
+ return request(endpoint);
6260
+ }
6261
+ var createTokenAuth = function createTokenAuth2(token) {
6262
+ if (!token) throw new Error("[@octokit/auth-token] No token passed to createTokenAuth");
6263
+ if (typeof token !== "string") throw new Error("[@octokit/auth-token] Token passed to createTokenAuth is not a string");
6264
+ token = token.replace(/^(token|bearer) +/i, "");
6265
+ return Object.assign(auth.bind(null, token), { hook: hook.bind(null, token) });
6266
+ };
6267
+ //#endregion
6268
+ //#region node_modules/@octokit/core/dist-src/version.js
6269
+ const VERSION = "7.0.8";
6270
+ //#endregion
6271
+ //#region node_modules/@octokit/core/dist-src/index.js
6272
+ const noop = () => {};
6273
+ const consoleWarn = console.warn.bind(console);
6274
+ const consoleError = console.error.bind(console);
6275
+ function createLogger(logger = {}) {
6276
+ if (typeof logger.debug !== "function") logger.debug = noop;
6277
+ if (typeof logger.info !== "function") logger.info = noop;
6278
+ if (typeof logger.warn !== "function") logger.warn = consoleWarn;
6279
+ if (typeof logger.error !== "function") logger.error = consoleError;
6280
+ return logger;
6281
+ }
6282
+ const userAgentTrail = `octokit-core.js/${VERSION} ${getUserAgent()}`;
6283
+ var Octokit = class {
6284
+ static VERSION = VERSION;
6285
+ static defaults(defaults) {
6286
+ const OctokitWithDefaults = class extends this {
6287
+ constructor(...args) {
6288
+ const options = args[0] || {};
6289
+ if (typeof defaults === "function") {
6290
+ super(defaults(options));
6291
+ return;
6292
+ }
6293
+ super(Object.assign({}, defaults, options, options.userAgent && defaults.userAgent ? { userAgent: `${options.userAgent} ${defaults.userAgent}` } : null));
6294
+ }
6295
+ };
6296
+ return OctokitWithDefaults;
6297
+ }
6298
+ static plugins = [];
6299
+ /**
6300
+ * Attach a plugin (or many) to your Octokit instance.
6301
+ *
6302
+ * @example
6303
+ * const API = Octokit.plugin(plugin1, plugin2, plugin3, ...)
6304
+ */
6305
+ static plugin(...newPlugins) {
6306
+ const currentPlugins = this.plugins;
6307
+ const NewOctokit = class extends this {
6308
+ static plugins = currentPlugins.concat(newPlugins.filter((plugin) => !currentPlugins.includes(plugin)));
6309
+ };
6310
+ return NewOctokit;
6311
+ }
6312
+ constructor(options = {}) {
6313
+ const hook = new before_after_hook_default.Collection();
6314
+ const requestDefaults = {
6315
+ baseUrl: request.endpoint.DEFAULTS.baseUrl,
6316
+ headers: {},
6317
+ request: Object.assign({}, options.request, { hook: hook.bind(null, "request") }),
6318
+ mediaType: {
6319
+ previews: [],
6320
+ format: ""
6321
+ }
6322
+ };
6323
+ requestDefaults.headers["user-agent"] = options.userAgent ? `${options.userAgent} ${userAgentTrail}` : userAgentTrail;
6324
+ if (options.baseUrl) requestDefaults.baseUrl = options.baseUrl;
6325
+ if (options.previews) requestDefaults.mediaType.previews = options.previews;
6326
+ if (options.timeZone) requestDefaults.headers["time-zone"] = options.timeZone;
6327
+ this.request = request.defaults(requestDefaults);
6328
+ this.graphql = withCustomRequest(this.request).defaults(requestDefaults);
6329
+ this.log = createLogger(options.log);
6330
+ this.hook = hook;
6331
+ if (!options.authStrategy) {
6332
+ if (!options.auth) this.auth = async () => ({ type: "unauthenticated" });
6333
+ else {
6334
+ const auth = createTokenAuth(options.auth);
6335
+ hook.wrap("request", auth.hook);
6336
+ this.auth = auth;
6337
+ }
6338
+ } else {
6339
+ const { authStrategy, ...otherOptions } = options;
6340
+ const auth = authStrategy(Object.assign({
6341
+ request: this.request,
6342
+ log: this.log,
6343
+ octokit: this,
6344
+ octokitOptions: otherOptions
6345
+ }, options.auth));
6346
+ hook.wrap("request", auth.hook);
6347
+ this.auth = auth;
6348
+ }
6349
+ const classConstructor = this.constructor;
6350
+ for (let i = 0; i < classConstructor.plugins.length; ++i) Object.assign(this, classConstructor.plugins[i](this, options));
6351
+ }
6352
+ request;
6353
+ graphql;
6354
+ log;
6355
+ hook;
6356
+ auth;
6357
+ };
6358
+ //#endregion
6359
+ //#region src/server/github/client.ts
6360
+ /** A precondition the user can fix (no token, no PR, wrong mode, unpushed head): the route answers 4xx. */
6361
+ var GithubError = class extends Error {
6362
+ status;
6363
+ constructor(message, status = 409) {
6364
+ super(message);
6365
+ this.status = status;
5542
6366
  }
6367
+ };
6368
+ /** Said wherever a GitHub feature is asked for without a token. */
6369
+ const NO_TOKEN = "No GitHub token: set GITHUB_TOKEN or sign in with gh auth login";
6370
+ /**
6371
+ * `GITHUB_TOKEN`, then `GH_TOKEN`, then the token `gh` holds. Null when none of them has
6372
+ * one: diffle then runs without GitHub, the origin repository still shows and everything
6373
+ * else says why it is off.
6374
+ */
6375
+ async function resolveToken(env = process.env, ghToken = ghAuthToken) {
6376
+ for (const source of ["GITHUB_TOKEN", "GH_TOKEN"]) {
6377
+ const token = env[source]?.trim();
6378
+ if (token) return {
6379
+ token,
6380
+ source
6381
+ };
6382
+ }
6383
+ const token = await ghToken();
6384
+ return token ? {
6385
+ token,
6386
+ source: "gh auth token"
6387
+ } : null;
6388
+ }
6389
+ /** The only `gh` left: its stored token, or null when gh is missing, signed out, or slow. */
6390
+ function ghAuthToken() {
6391
+ return new Promise((resolve) => {
6392
+ execFile("gh", ["auth", "token"], {
6393
+ timeout: 5e3,
6394
+ windowsHide: true
6395
+ }, (err, stdout) => {
6396
+ const token = stdout?.trim();
6397
+ resolve(!err && token ? token : null);
6398
+ });
6399
+ });
5543
6400
  }
5544
- function parsePullRequest(value) {
5545
- const pr = value;
5546
- if (!(pr != null && typeof pr.title === "string" && [
5547
- "OPEN",
5548
- "CLOSED",
5549
- "MERGED"
5550
- ].includes(pr.state ?? "") && typeof pr.isDraft === "boolean" && typeof pr.baseRefOid === "string" && typeof pr.number === "number" && typeof pr.url === "string" && typeof pr.baseRefName === "string" && typeof pr.headRefName === "string" && typeof pr.headRefOid === "string")) throw new GithubError("unexpected pull request data from gh", 502);
5551
- const slug = /^https?:\/\/[^/]+\/([^/]+\/[^/]+)\/pull\/\d+/.exec(pr.url);
5552
- if (!slug) throw new GithubError(`cannot read the repository from the pull request url: ${pr.url}`, 502);
5553
- const source = value;
5554
- const owner = source.headRepositoryOwner?.login;
5555
- const name = source.headRepository?.name;
5556
- return {
5557
- headRepository: typeof owner === "string" && typeof name === "string" ? `${owner}/${name}` : null,
5558
- number: pr.number,
5559
- url: pr.url,
5560
- title: pr.title,
5561
- state: pr.state,
5562
- isDraft: pr.isDraft,
5563
- repository: slug[1],
5564
- baseRefOid: pr.baseRefOid,
5565
- baseRefName: pr.baseRefName,
5566
- headRefName: pr.headRefName,
5567
- headRefOid: pr.headRefOid
5568
- };
6401
+ /** The real client, over `@octokit/core`. */
6402
+ function createGithubClient(token, { baseUrl, fetch: customFetch } = {}) {
6403
+ const octokit = new Octokit({
6404
+ auth: token,
6405
+ baseUrl: baseUrl || process.env.GITHUB_API_URL || void 0,
6406
+ userAgent: `diffle/${version$1}`,
6407
+ request: customFetch ? { fetch: customFetch } : {}
6408
+ });
6409
+ return { async graphql(query, variables, opts = {}) {
6410
+ const request = opts.timeoutMs ? { signal: AbortSignal.timeout(opts.timeoutMs) } : void 0;
6411
+ try {
6412
+ return await octokit.graphql(query, {
6413
+ ...variables,
6414
+ request
6415
+ });
6416
+ } catch (e) {
6417
+ throw toGithubError(e, opts.timeoutMs);
6418
+ }
6419
+ } };
6420
+ }
6421
+ /**
6422
+ * GitHub's answer, as a `GithubError`. Something the user can fix (a rejected token, a missing
6423
+ * pull request) is 409; GitHub refusing the content (422) or not answering at all is 502.
6424
+ */
6425
+ function toGithubError(e, timeoutMs) {
6426
+ if (e instanceof GithubError) return e;
6427
+ const err = e;
6428
+ if (err?.name === "GraphqlResponseError" && Array.isArray(err.errors) && err.errors.length) {
6429
+ const detail = err.errors.map((x) => x.message ?? "unknown error").join("; ");
6430
+ const fixable = err.errors.every((x) => x.type === "NOT_FOUND" || x.type === "FORBIDDEN");
6431
+ return new GithubError(`GitHub: ${detail}`, fixable ? 409 : 502);
6432
+ }
6433
+ if (err?.name === "HttpError" && typeof err.status === "number") {
6434
+ const detail = firstLine(err.message);
6435
+ if (err.response === void 0) {
6436
+ const cause = err.cause?.name;
6437
+ if (cause === "TimeoutError" || cause === "AbortError") return new GithubError(`GitHub did not answer within ${timeoutMs ?? "?"}ms`, 502);
6438
+ return new GithubError(`cannot reach GitHub: ${detail}`, 502);
6439
+ }
6440
+ if (err.status === 401) return new GithubError(`GitHub rejected the token (401): ${detail}`, 409);
6441
+ if (err.status === 403 || err.status === 404) return new GithubError(`GitHub ${err.status}: ${detail}`, 409);
6442
+ return new GithubError(`GitHub ${err.status}: ${detail}`, 502);
6443
+ }
6444
+ if (err?.name === "TimeoutError" || err?.name === "AbortError") return new GithubError(`GitHub did not answer within ${timeoutMs ?? "?"}ms`, 502);
6445
+ return new GithubError(`cannot reach GitHub: ${e instanceof Error ? firstLine(e.message) : String(e)}`, 502);
6446
+ }
6447
+ function firstLine(s) {
6448
+ return (s ?? "").trim().split("\n")[0] ?? "";
5569
6449
  }
5570
6450
  //#endregion
5571
6451
  //#region node_modules/zod/v4/core/util.js
@@ -16798,20 +17678,155 @@ function hasClientBuild() {
16798
17678
  return existsSync(join(resolveClientDir(), "index.html"));
16799
17679
  }
16800
17680
  //#endregion
16801
- //#region src/server/GithubMetadata.ts
16802
- const LOOKUP_TIMEOUT_MS = 5e3;
17681
+ //#region src/server/github/pulls.ts
16803
17682
  /** GitHub.com identity from a configured HTTPS, SSH, or scp-style remote URL. */
16804
17683
  function githubRepository(url) {
16805
17684
  const match = /^(?:https?:\/\/github\.com\/|ssh:\/\/(?:[^/@]+@)?github\.com(?::\d+)?\/|(?:[^/@]+@)?github\.com:)([^/]+)\/([^/]+?)\/?$/i.exec(url);
16806
17685
  return match ? `${match[1]}/${match[2].replace(/\.git$/i, "")}` : null;
16807
17686
  }
16808
- /** Local-only origin identity; works without gh, authentication, or a network connection. */
17687
+ /** Local-only origin identity; works without a token or a network connection. */
16809
17688
  async function originRepository(repo) {
16810
17689
  const origin = (await repo.remotes()).find((r) => r.name === "origin");
16811
17690
  return origin ? githubRepository(origin.url) : null;
16812
17691
  }
16813
- /** Enriches a comparison without fetching commits, changing its endpoints, or changing comment storage. */
16814
- async function discoverGithub(repo, snap, { run = runGh, prUrl } = {}) {
17692
+ /**
17693
+ * The GitHub repository pull requests are looked up in: `origin`, or the only GitHub remote
17694
+ * when origin is not one. Any other layout has to name the pull request by url.
17695
+ */
17696
+ async function baseRepository(repo) {
17697
+ if (!repo) throw new GithubError("not in a git repository; name the pull request by url", 400);
17698
+ const remotes = (await repo.remotes()).flatMap((r) => {
17699
+ const repository = githubRepository(r.url);
17700
+ return repository ? [{
17701
+ name: r.name,
17702
+ repository
17703
+ }] : [];
17704
+ });
17705
+ const base = remotes.find((r) => r.name === "origin") ?? (remotes.length === 1 ? remotes[0] : void 0);
17706
+ if (!base) throw new GithubError(`${remotes.length ? `${remotes.length} GitHub remotes and no origin` : "no GitHub remote"}; name the pull request by url`, 400);
17707
+ return base.repository;
17708
+ }
17709
+ /** The base repository the pull request lives in, taken from its url (a fork's own remote is not it). */
17710
+ function repoOfPrUrl(url) {
17711
+ const parsed = parsePrUrl(url);
17712
+ if (!parsed) throw new GithubError(`cannot read owner/repo from the pull request url: ${url}`, 502);
17713
+ return {
17714
+ owner: parsed.owner,
17715
+ repo: parsed.repo
17716
+ };
17717
+ }
17718
+ function parsePrUrl(url) {
17719
+ let pathname;
17720
+ try {
17721
+ pathname = new URL(url).pathname;
17722
+ } catch {
17723
+ return null;
17724
+ }
17725
+ const m = /^\/([^/]+)\/([^/]+)\/pull\/(\d+)(?:\/|$)/.exec(pathname);
17726
+ return m ? {
17727
+ owner: m[1],
17728
+ repo: m[2],
17729
+ number: Number(m[3])
17730
+ } : null;
17731
+ }
17732
+ const PR_FIELDS = "number url title state isDraft baseRefOid baseRefName headRefName headRefOid headRepository{name owner{login}}";
17733
+ const BY_NUMBER = `query($owner:String!,$repo:String!,$number:Int!){
17734
+ repository(owner:$owner,name:$repo){pullRequest(number:$number){${PR_FIELDS}}}
17735
+ }`;
17736
+ const BY_HEAD = `query($owner:String!,$repo:String!,$head:String!,$base:String){
17737
+ repository(owner:$owner,name:$repo){pullRequests(headRefName:$head,baseRefName:$base,states:[OPEN],first:100){nodes{${PR_FIELDS}}}}
17738
+ }`;
17739
+ /**
17740
+ * The pull request a selector names: a number, `#number`, a URL, a branch (`owner:branch`
17741
+ * for a fork's), or, without one, the checked-out branch's. Everything but a URL is
17742
+ * looked up in the base repository (see `baseRepository`). Option-shaped selectors are refused.
17743
+ */
17744
+ async function viewPr(selector, repo, github, timeoutMs) {
17745
+ if (selector == null) {
17746
+ if (!repo) throw new GithubError("not in a git repository; name the pull request by url", 400);
17747
+ const ref = await repo.branchRef("HEAD");
17748
+ if (!ref?.startsWith("refs/heads/")) throw new GithubError("HEAD is not on a branch; name the pull request by number or url", 400);
17749
+ const upstream = await repo.upstreamBranch("HEAD");
17750
+ const remotes = await repo.remotes();
17751
+ const remoteUrl = upstream && remotes.find((r) => r.name === upstream.remote)?.url;
17752
+ const headRepo = remoteUrl ? githubRepository(remoteUrl) : null;
17753
+ return byHead(github, await baseRepository(repo), upstream?.branch ?? ref.slice(11), {
17754
+ owner: headRepo?.split("/")[0],
17755
+ timeoutMs
17756
+ });
17757
+ }
17758
+ const s = selector.trim();
17759
+ if (s.startsWith("-")) throw new GithubError(`not a pull request: ${selector}`, 400);
17760
+ const url = /^https?:\/\//.test(s) ? parsePrUrl(s) : null;
17761
+ if (url) return byNumber(github, `${url.owner}/${url.repo}`, url.number, timeoutMs);
17762
+ if (/^https?:\/\//.test(s)) throw new GithubError(`not a pull request url: ${selector}`, 400);
17763
+ const num = /^#?(\d+)$/.exec(s);
17764
+ if (num) return byNumber(github, await baseRepository(repo), Number(num[1]), timeoutMs);
17765
+ const colon = s.indexOf(":");
17766
+ const owner = colon > 0 ? s.slice(0, colon) : void 0;
17767
+ return byHead(github, await baseRepository(repo), colon > 0 ? s.slice(colon + 1) : s, {
17768
+ owner,
17769
+ timeoutMs
17770
+ });
17771
+ }
17772
+ async function byNumber(github, repository, number, timeoutMs) {
17773
+ const [owner, repo] = repository.split("/");
17774
+ const data = await github.graphql(BY_NUMBER, {
17775
+ owner,
17776
+ repo,
17777
+ number
17778
+ }, { timeoutMs });
17779
+ if (!data.repository?.pullRequest) throw new GithubError(`no pull request ${repository}#${number}`, 409);
17780
+ return parsePullRequest(data.repository.pullRequest);
17781
+ }
17782
+ async function byHead(github, baseRepo, head, { owner, timeoutMs }) {
17783
+ const label = owner ? `${owner}:${head}` : head;
17784
+ const prs = (await pullRequestsForHead(github, baseRepo, head, void 0, timeoutMs)).filter((pr) => !owner || pr.headRepository?.split("/")[0]?.toLowerCase() === owner.toLowerCase());
17785
+ if (prs.length === 0) throw new GithubError(`no open pull request in ${baseRepo} for ${label}`, 409);
17786
+ if (prs.length > 1) throw new GithubError(`${prs.length} open pull requests in ${baseRepo} for ${label}; open one by number`, 409);
17787
+ return prs[0];
17788
+ }
17789
+ /** Open pull requests of `baseRepo` whose head is the named branch, from any head repository, into `base` if given. */
17790
+ async function pullRequestsForHead(github, baseRepo, head, base, timeoutMs) {
17791
+ const [owner, repo] = baseRepo.split("/");
17792
+ const nodes = (await github.graphql(BY_HEAD, {
17793
+ owner,
17794
+ repo,
17795
+ head,
17796
+ base: base ?? null
17797
+ }, { timeoutMs })).repository?.pullRequests?.nodes;
17798
+ if (!Array.isArray(nodes)) throw new GithubError("unexpected pull request data from GitHub", 502);
17799
+ return nodes.map(parsePullRequest);
17800
+ }
17801
+ const PullRequestNodeSchema = GithubPullRequestSchema.omit({ repository: true }).extend({
17802
+ baseRefOid: string(),
17803
+ baseRefName: string(),
17804
+ headRefName: string(),
17805
+ headRefOid: string(),
17806
+ headRepository: object({
17807
+ name: string(),
17808
+ owner: object({ login: string() })
17809
+ }).nullable().transform((source) => source ? `${source.owner.login}/${source.name}` : null)
17810
+ });
17811
+ function parsePullRequest(value) {
17812
+ const result = PullRequestNodeSchema.safeParse(value);
17813
+ if (!result.success) throw new GithubError("unexpected pull request data from GitHub", 502);
17814
+ const pr = result.data;
17815
+ const slug = /^https?:\/\/[^/]+\/([^/]+\/[^/]+)\/pull\/\d+/.exec(pr.url);
17816
+ if (!slug) throw new GithubError(`cannot read the repository from the pull request url: ${pr.url}`, 502);
17817
+ return {
17818
+ ...pr,
17819
+ repository: slug[1]
17820
+ };
17821
+ }
17822
+ //#endregion
17823
+ //#region src/server/GithubMetadata.ts
17824
+ const LOOKUP_TIMEOUT_MS = 5e3;
17825
+ /**
17826
+ * Enriches a comparison without fetching commits, changing its endpoints, or changing comment
17827
+ * storage. Without a client (no token), only the origin repository is known.
17828
+ */
17829
+ async function discoverGithub(repo, snap, { github, prUrl } = {}) {
16815
17830
  const repository = await originRepository(repo);
16816
17831
  const absent = (reason) => ({
16817
17832
  version: snap.version,
@@ -16819,9 +17834,10 @@ async function discoverGithub(repo, snap, { run = runGh, prUrl } = {}) {
16819
17834
  pullRequest: null,
16820
17835
  reason
16821
17836
  });
17837
+ if (!github) return absent(NO_TOKEN);
16822
17838
  try {
16823
17839
  let pr;
16824
- if (prUrl) pr = await viewPr(prUrl, repo.root, run, LOOKUP_TIMEOUT_MS);
17840
+ if (prUrl) pr = await viewPr(prUrl, repo, github, LOOKUP_TIMEOUT_MS);
16825
17841
  else {
16826
17842
  const [old, next, remotes] = await Promise.all([
16827
17843
  repo.upstreamBranch(snap.mode.old),
@@ -16836,7 +17852,7 @@ async function discoverGithub(repo, snap, { run = runGh, prUrl } = {}) {
16836
17852
  const baseRepo = repository(old.remote);
16837
17853
  const headRepo = repository(next.remote);
16838
17854
  if (!baseRepo || !headRepo) return absent("Both branches must have GitHub remotes");
16839
- const prs = await listPrsForHead(baseRepo, old.branch, next.branch, headRepo, repo.root, run, LOOKUP_TIMEOUT_MS);
17855
+ const prs = (await pullRequestsForHead(github, baseRepo, next.branch, old.branch, LOOKUP_TIMEOUT_MS)).filter((pr) => pr.headRepository?.toLowerCase() === headRepo.toLowerCase());
16840
17856
  if (!prs.length) return absent("No matching pull request");
16841
17857
  if (prs.length !== 1) return absent("Multiple matching pull requests; open one explicitly");
16842
17858
  pr = prs[0];
@@ -16870,6 +17886,300 @@ async function discoverGithub(repo, snap, { run = runGh, prUrl } = {}) {
16870
17886
  }
16871
17887
  }
16872
17888
  //#endregion
17889
+ //#region src/server/github/review.ts
17890
+ function isFileComment(c) {
17891
+ return "subjectType" in c;
17892
+ }
17893
+ /**
17894
+ * Threads → the comments of one pending GitHub review, one per thread. Stale threads are skipped:
17895
+ * their lines no longer sit in the diff, and GitHub would refuse them anyway. With
17896
+ * `threadIds`, only those (resolved included, the user asked for them by hand);
17897
+ * without, every unresolved thread.
17898
+ */
17899
+ function buildReview(threads, threadIds) {
17900
+ const skipped = [];
17901
+ let chosen;
17902
+ if (threadIds) {
17903
+ const byId = new Map(threads.map((t) => [t.id, t]));
17904
+ chosen = [];
17905
+ for (const id of threadIds) {
17906
+ const t = byId.get(id);
17907
+ if (t) chosen.push(t);
17908
+ else skipped.push({
17909
+ id,
17910
+ reason: "unknown thread"
17911
+ });
17912
+ }
17913
+ } else chosen = threads.filter((t) => !t.resolved);
17914
+ const comments = [];
17915
+ const ids = [];
17916
+ for (const t of [...chosen].sort(compareThreads)) {
17917
+ if (t.stale) {
17918
+ skipped.push({
17919
+ id: t.id,
17920
+ reason: "stale"
17921
+ });
17922
+ continue;
17923
+ }
17924
+ comments.push(toReviewComment(t));
17925
+ ids.push(t.id);
17926
+ }
17927
+ return {
17928
+ comments,
17929
+ ids,
17930
+ skipped
17931
+ };
17932
+ }
17933
+ function toReviewComment(t) {
17934
+ const { anchor } = t;
17935
+ const body = formatBody(t.messages);
17936
+ if (anchor.kind === "file") return {
17937
+ path: anchor.path,
17938
+ subjectType: "FILE",
17939
+ body
17940
+ };
17941
+ const side = anchor.side === "old" ? "LEFT" : "RIGHT";
17942
+ const c = {
17943
+ path: anchor.path,
17944
+ line: anchor.endLine,
17945
+ side,
17946
+ body
17947
+ };
17948
+ if (anchor.startLine !== anchor.endLine) {
17949
+ c.startLine = anchor.startLine;
17950
+ c.startSide = side;
17951
+ }
17952
+ return c;
17953
+ }
17954
+ /**
17955
+ * Messages joined as markdown. GitHub renders ```suggestion fences itself, so bodies
17956
+ * stay verbatim.
17957
+ */
17958
+ function formatBody(messages) {
17959
+ return messages.map((m) => m.body.trim()).join("\n\n---\n\n");
17960
+ }
17961
+ /** Serializes validation and writes so overlapping exports cannot add the same thread twice. */
17962
+ var GithubExporter = class {
17963
+ queue = Promise.resolve();
17964
+ export(prepare) {
17965
+ const result = this.queue.then(async () => exportToGithub(await prepare()));
17966
+ this.queue = result.catch(() => {});
17967
+ return result;
17968
+ }
17969
+ };
17970
+ /**
17971
+ * Adds the threads to a *pending* review on the active PR, creating the pending review
17972
+ * when there is none. The review is never submitted: the human opens the PR and submits
17973
+ * it themselves. The caller validates the comparison inside the export queue before posting.
17974
+ *
17975
+ * Re-exporting matches a hidden thread ID and its anchor in the pending review.
17976
+ * A matching comment is left alone when its body matches and rewritten when it does not, so
17977
+ * editing a thread and exporting again does not stack a second comment on the line. Comments
17978
+ * of an already-submitted review are out of reach — those would have to be replied to.
17979
+ */
17980
+ async function exportToGithub({ snap, pullRequest: pr, threads, threadIds, github }) {
17981
+ if (snap.newSha === "worktree") throw new GithubError("GitHub cannot anchor comments to the worktree");
17982
+ const { comments, ids, skipped } = buildReview(threads, threadIds);
17983
+ if (comments.length === 0) throw new GithubError(skipped.length ? `nothing to post: ${describe(skipped)}` : "nothing to post", 400);
17984
+ for (const [i, comment] of comments.entries()) comment.body += `\n\n${threadMarker(ids[i])}`;
17985
+ const { pullRequestId, pending } = await findPendingReview(github, pr);
17986
+ if (!pending) {
17987
+ const lines = comments.filter((c) => !isFileComment(c));
17988
+ const created = await createPendingReview(github, pullRequestId, snap.newSha, lines);
17989
+ let appended = 0;
17990
+ try {
17991
+ for (const c of comments.filter(isFileComment)) {
17992
+ await addToPendingReview(github, created.id, c);
17993
+ appended++;
17994
+ }
17995
+ } catch (e) {
17996
+ const detail = e instanceof Error ? e.message : String(e);
17997
+ throw new GithubError(`created the pending review with ${lines.length + appended} comments, then ${detail}`, 502);
17998
+ }
17999
+ return {
18000
+ url: pr.url,
18001
+ posted: comments.length,
18002
+ updated: 0,
18003
+ review: "created",
18004
+ skipped
18005
+ };
18006
+ }
18007
+ const existing = await pendingComments(github, pr, pending.id);
18008
+ const add = [];
18009
+ let updated = 0;
18010
+ let posted = 0;
18011
+ try {
18012
+ for (const [i, c] of comments.entries()) {
18013
+ const id = ids[i];
18014
+ const at = existing.get(anchorKey(c));
18015
+ const index = at?.findIndex((comment) => comment.body.trimEnd().endsWith(threadMarker(id))) ?? -1;
18016
+ const hit = index < 0 ? void 0 : at.splice(index, 1)[0];
18017
+ if (!hit) {
18018
+ add.push(c);
18019
+ continue;
18020
+ }
18021
+ if (hit.body.trim() === c.body.trim()) {
18022
+ skipped.push({
18023
+ id,
18024
+ reason: "already in the review"
18025
+ });
18026
+ continue;
18027
+ }
18028
+ await github.graphql(UPDATE_COMMENT, { input: {
18029
+ pullRequestReviewCommentId: hit.nodeId,
18030
+ body: c.body
18031
+ } });
18032
+ updated++;
18033
+ }
18034
+ for (const c of add) {
18035
+ await addToPendingReview(github, pending.id, c);
18036
+ posted++;
18037
+ }
18038
+ } catch (e) {
18039
+ if (updated === 0 && posted === 0) throw e;
18040
+ const detail = e instanceof Error ? e.message : String(e);
18041
+ throw new GithubError(`updated ${updated} and added ${posted} comments in the pending review, then ${detail}`, 502);
18042
+ }
18043
+ return {
18044
+ url: pr.url,
18045
+ posted,
18046
+ updated,
18047
+ review: "existing",
18048
+ skipped
18049
+ };
18050
+ }
18051
+ function threadMarker(id) {
18052
+ return `<!-- diffle-thread:${encodeURIComponent(id)} -->`;
18053
+ }
18054
+ /** The anchor must still match before we rewrite an exported thread. */
18055
+ function anchorKey(c) {
18056
+ if (isFileComment(c)) return [c.path, "FILE"].join("\0");
18057
+ return [
18058
+ c.path,
18059
+ c.side,
18060
+ c.startLine ?? c.line,
18061
+ c.line
18062
+ ].join("\0");
18063
+ }
18064
+ const PENDING_QUERY = `query($owner:String!,$repo:String!,$number:Int!){
18065
+ viewer{login}
18066
+ repository(owner:$owner,name:$repo){pullRequest(number:$number){id reviews(last:20,states:[PENDING]){nodes{id author{login}}}}}
18067
+ }`;
18068
+ /**
18069
+ * The PR's node id and the viewer's own pending review on it, or null. GitHub hides other
18070
+ * people's pending reviews, and the login check makes sure of it: appending to someone
18071
+ * else's draft would put our comments in their review.
18072
+ */
18073
+ async function findPendingReview(github, pr) {
18074
+ const { owner, repo } = repoOfPrUrl(pr.url);
18075
+ const data = await github.graphql(PENDING_QUERY, {
18076
+ owner,
18077
+ repo,
18078
+ number: pr.number
18079
+ });
18080
+ const pullRequestId = data.repository?.pullRequest?.id;
18081
+ if (typeof pullRequestId !== "string") throw new GithubError(`cannot read pull request ${pr.url} from GitHub`, 502);
18082
+ const login = data.viewer?.login;
18083
+ for (const node of data.repository?.pullRequest?.reviews?.nodes ?? []) if (node?.id && node.author?.login === login) return {
18084
+ pullRequestId,
18085
+ pending: { id: node.id }
18086
+ };
18087
+ return {
18088
+ pullRequestId,
18089
+ pending: null
18090
+ };
18091
+ }
18092
+ const THREADS_QUERY = `query($owner:String!,$repo:String!,$number:Int!,$after:String){
18093
+ repository(owner:$owner,name:$repo){pullRequest(number:$number){reviewThreads(first:100,after:$after){
18094
+ pageInfo{hasNextPage endCursor}
18095
+ nodes{path line startLine diffSide subjectType comments(first:1){nodes{id body pullRequestReview{id}}}}
18096
+ }}}
18097
+ }`;
18098
+ /**
18099
+ * The pending review's own comments, by anchor. It has to be `reviewThreads`: it is the only view
18100
+ * that reports a *pending* comment's real position — REST's review-comment list leaves `line`,
18101
+ * `side` and `start_line` null for one, and the GraphQL comment type has no side field at all.
18102
+ * A failed or incomplete lookup aborts the export: absence is safe to infer only from a complete read.
18103
+ */
18104
+ async function pendingComments(github, pr, reviewId) {
18105
+ const by = /* @__PURE__ */ new Map();
18106
+ const { owner, repo } = repoOfPrUrl(pr.url);
18107
+ let after = null;
18108
+ for (let page = 0; page < 10; page++) {
18109
+ const threads = (await github.graphql(THREADS_QUERY, {
18110
+ owner,
18111
+ repo,
18112
+ number: pr.number,
18113
+ after
18114
+ })).repository?.pullRequest?.reviewThreads;
18115
+ if (!threads?.nodes || typeof threads.pageInfo?.hasNextPage !== "boolean") throw new GithubError("cannot read the complete pending review", 502);
18116
+ for (const t of threads.nodes) {
18117
+ const c = t?.comments?.nodes?.[0];
18118
+ if (!t?.path || !c?.id || c.pullRequestReview?.id !== reviewId) continue;
18119
+ const key = pendingKey(t);
18120
+ if (key == null) continue;
18121
+ const at = by.get(key);
18122
+ if (at) at.push({
18123
+ nodeId: c.id,
18124
+ body: c.body ?? ""
18125
+ });
18126
+ else by.set(key, [{
18127
+ nodeId: c.id,
18128
+ body: c.body ?? ""
18129
+ }]);
18130
+ }
18131
+ if (!threads.pageInfo.hasNextPage) return by;
18132
+ if (!threads.pageInfo.endCursor || threads.pageInfo.endCursor === after) throw new GithubError("cannot read the next page of the pending review", 502);
18133
+ after = threads.pageInfo.endCursor;
18134
+ }
18135
+ throw new GithubError("pending review exceeds the 1000-thread lookup limit; export stopped without changes", 502);
18136
+ }
18137
+ /** The anchor key of a thread already in the review, or null for one GitHub reports without a position. */
18138
+ function pendingKey(t) {
18139
+ const path = t.path;
18140
+ if (t.subjectType === "FILE") return anchorKey({
18141
+ path,
18142
+ subjectType: "FILE",
18143
+ body: ""
18144
+ });
18145
+ if (t.line == null) return null;
18146
+ return anchorKey({
18147
+ path,
18148
+ side: t.diffSide === "LEFT" ? "LEFT" : "RIGHT",
18149
+ line: t.line,
18150
+ startLine: t.startLine ?? void 0,
18151
+ body: ""
18152
+ });
18153
+ }
18154
+ const UPDATE_COMMENT = `mutation($input:UpdatePullRequestReviewCommentInput!){updatePullRequestReviewComment(input:$input){pullRequestReviewComment{id}}}`;
18155
+ const CREATE_REVIEW = `mutation($input:AddPullRequestReviewInput!){addPullRequestReview(input:$input){pullRequestReview{id}}}`;
18156
+ /**
18157
+ * No `event` in the input, so GitHub keeps the new review pending with all of its threads.
18158
+ * Resolves the review's node id, for the comments a new review cannot carry.
18159
+ */
18160
+ async function createPendingReview(github, pullRequestId, commitOID, threads) {
18161
+ const id = (await github.graphql(CREATE_REVIEW, { input: {
18162
+ pullRequestId,
18163
+ commitOID,
18164
+ threads
18165
+ } })).addPullRequestReview?.pullRequestReview?.id;
18166
+ if (typeof id !== "string") throw new GithubError("GitHub did not return the created review", 502);
18167
+ return { id };
18168
+ }
18169
+ const ADD_THREAD = `mutation($input:AddPullRequestReviewThreadInput!){addPullRequestReviewThread(input:$input){thread{id}}}`;
18170
+ /** Appends one thread to the pending review; the only way to extend one, or to place a file comment. */
18171
+ async function addToPendingReview(github, reviewId, c) {
18172
+ await github.graphql(ADD_THREAD, { input: {
18173
+ pullRequestReviewId: reviewId,
18174
+ ...c
18175
+ } });
18176
+ }
18177
+ function describe(skipped) {
18178
+ const counts = /* @__PURE__ */ new Map();
18179
+ for (const s of skipped) counts.set(s.reason, (counts.get(s.reason) ?? 0) + 1);
18180
+ return [...counts].map(([reason, n]) => `${n} ${reason}`).join(", ");
18181
+ }
18182
+ //#endregion
16873
18183
  //#region src/server/mode.ts
16874
18184
  /**
16875
18185
  * Resolves a comparison to Git endpoints for initial mode setup, PR setup after
@@ -16926,8 +18236,8 @@ async function resolveComparison(repo, comparison) {
16926
18236
  };
16927
18237
  }
16928
18238
  /** Resolves an input command into a comparison and optional explicit PR identity. */
16929
- async function resolveReview(req, repo, gh = runGh) {
16930
- if (req.kind === "pr") return resolvePr(req, repo, gh);
18239
+ async function resolveReview(req, repo, github) {
18240
+ if (req.kind === "pr") return resolvePr(req, repo, github);
16931
18241
  const parsed = req.kind === "working" ? {
16932
18242
  old: "HEAD",
16933
18243
  new: "worktree",
@@ -16979,14 +18289,15 @@ function branchKey(old, next, mergeBase) {
16979
18289
  }
16980
18290
  /**
16981
18291
  * A GitHub pull request, as GitHub shows it: merge-base(base tip, head) vs head.
16982
- * `gh` names the PR, then one fetch brings the base tip and the PR head into
18292
+ * GitHub names the PR, then one fetch brings the base tip and the PR head into
16983
18293
  * session-owned refs named after the branches, so a PR nobody has checked out
16984
18294
  * is reviewable. These refs stay fixed until another fetch; review state uses
16985
18295
  * the same branch identities as ordinary comparisons, so comments survive
16986
18296
  * pushes and reopening by branch.
16987
18297
  */
16988
- async function resolvePr(req, repo, gh) {
16989
- const pr = await viewPr(req.pr, repo.root, gh);
18298
+ async function resolvePr(req, repo, github) {
18299
+ if (!github) throw new GithubError(NO_TOKEN);
18300
+ const pr = await viewPr(req.pr, repo, github);
16990
18301
  const head = `${repo.reviewRefs}/${pr.number}/head/${pr.headRefName}`;
16991
18302
  const base = `${repo.reviewRefs}/${pr.number}/base/${pr.baseRefName}`;
16992
18303
  await repo.fetch(await fetchSource(repo, pr), [`+refs/pull/${pr.number}/head:${head}`, `+refs/heads/${pr.baseRefName}:${base}`]);
@@ -17263,7 +18574,7 @@ var Session = class {
17263
18574
  const cached = active.github;
17264
18575
  if (cached?.snapshot === snap && cached.expires > Date.now()) return cached.promise;
17265
18576
  const promise = discoverGithub(this.repo, snap, {
17266
- run: this.opts.gh,
18577
+ github: this.opts.github,
17267
18578
  prUrl: active.prUrl
17268
18579
  });
17269
18580
  const entry = {
@@ -17283,8 +18594,10 @@ var Session = class {
17283
18594
  const active = this.require();
17284
18595
  const snap = await active.snapshotter.current();
17285
18596
  if (active !== this.active) throw new GithubError("Comparison changed; try again");
18597
+ const github = this.opts.github;
18598
+ if (!github) throw new GithubError(NO_TOKEN);
17286
18599
  const metadata = await discoverGithub(this.repo, snap, {
17287
- run: this.opts.gh,
18600
+ github,
17288
18601
  prUrl: active.prUrl
17289
18602
  });
17290
18603
  if (!metadata.pullRequest || metadata.reason !== null) throw new GithubError(metadata.reason ?? "No matching pull request");
@@ -17294,7 +18607,7 @@ var Session = class {
17294
18607
  pullRequest: metadata.pullRequest,
17295
18608
  threads: active.comments.threads({ state: "all" }),
17296
18609
  threadIds,
17297
- run: this.opts.gh
18610
+ github
17298
18611
  };
17299
18612
  });
17300
18613
  }
@@ -17354,7 +18667,7 @@ var Session = class {
17354
18667
  return this.run(() => this.transition(req));
17355
18668
  }
17356
18669
  async transition(req) {
17357
- const { mode, prUrl } = await resolveReview(req, this.repo, this.opts.gh);
18670
+ const { mode, prUrl } = await resolveReview(req, this.repo, this.opts.github);
17358
18671
  const snapshotter = new Snapshotter(this.repo, mode, ++this.version, this.opts.context);
17359
18672
  const [comments, snap] = await Promise.all([CommentStore.open(this.repo.gitDir, mode.commentKey), snapshotter.current()]);
17360
18673
  const next = {
@@ -21372,7 +22685,7 @@ function openBrowser(url) {
21372
22685
  //#endregion
21373
22686
  //#region src/cli/repository.ts
21374
22687
  /** Foreign PRs live in a disposable clone, never in the caller's object database. */
21375
- async function openReviewRepository(req, cwd, gh = runGh) {
22688
+ async function openReviewRepository(req, cwd, github) {
21376
22689
  let local;
21377
22690
  try {
21378
22691
  local = await GitRepo.open(cwd);
@@ -21383,7 +22696,8 @@ async function openReviewRepository(req, cwd, gh = runGh) {
21383
22696
  repo: local,
21384
22697
  close: () => local.cleanReviewRefs()
21385
22698
  };
21386
- const pr = await viewPr(req.pr, cwd, gh);
22699
+ if (!github) throw new GithubError(NO_TOKEN);
22700
+ const pr = await viewPr(req.pr, local ?? null, github);
21387
22701
  if (local && (await local.remotes()).some((r) => remoteSlug(r.url) === pr.repository.toLowerCase())) return {
21388
22702
  repo: local,
21389
22703
  close: () => local.cleanReviewRefs()
@@ -21540,14 +22854,16 @@ async function run(req, opts) {
21540
22854
  process.on("SIGTERM", interrupt);
21541
22855
  let review;
21542
22856
  try {
21543
- review = await openReviewRepository(req, opts.C ?? process.cwd());
22857
+ const github = await connectGithub();
22858
+ timing.mark("github token");
22859
+ review = await openReviewRepository(req, opts.C ?? process.cwd(), github.client);
21544
22860
  timing.mark("open repository");
21545
22861
  const config = await UserConfigStore.open();
21546
22862
  if (interrupted) {
21547
22863
  await review.close();
21548
22864
  return;
21549
22865
  }
21550
- await serve(req, opts, review.repo, review.close, config, timing);
22866
+ await serve(req, opts, review.repo, review.close, config, github, timing);
21551
22867
  } catch (e) {
21552
22868
  await review?.close();
21553
22869
  throw e;
@@ -21556,11 +22872,12 @@ async function run(req, opts) {
21556
22872
  process.off("SIGTERM", interrupt);
21557
22873
  }
21558
22874
  }
21559
- async function serve(req, opts, repo, closeRepo, config, timing) {
22875
+ async function serve(req, opts, repo, closeRepo, config, github, timing) {
21560
22876
  const hub = new WsHub();
21561
22877
  const session = new Session(repo, hub, {
21562
22878
  watch: opts.watch,
21563
- context: opts.context ?? config.get().contextLines
22879
+ context: opts.context ?? config.get().contextLines,
22880
+ github: github.client ?? void 0
21564
22881
  });
21565
22882
  const named = Array.isArray(opts.lsp) ? opts.lsp : [];
21566
22883
  const overrides = {
@@ -21609,6 +22926,8 @@ async function serve(req, opts, repo, closeRepo, config, timing) {
21609
22926
  }
21610
22927
  console.error(`🚀 diffle running at ${c.cyan(url.href)}`);
21611
22928
  console.error(`📂 ${c.dim("repo")} ${repo.root}`);
22929
+ if (github.client) console.error(`🐙 ${c.dim("github")} ${c.dim(`token from ${github.source}`)}`);
22930
+ else console.error(`🐙 ${c.dim("github")} ${c.dim("off: no token in GITHUB_TOKEN, GH_TOKEN or gh auth token")}`);
21612
22931
  const snap = await session.start(req);
21613
22932
  timing.mark("snapshot");
21614
22933
  const n = snap.changed.length;
@@ -21635,6 +22954,15 @@ async function serve(req, opts, repo, closeRepo, config, timing) {
21635
22954
  throw e;
21636
22955
  }
21637
22956
  }
22957
+ /** One token lookup per run; without one, GitHub features are off rather than failing later. */
22958
+ async function connectGithub() {
22959
+ const found = await resolveToken();
22960
+ if (!found) return { client: null };
22961
+ return {
22962
+ client: createGithubClient(found.token),
22963
+ source: found.source
22964
+ };
22965
+ }
21638
22966
  /**
21639
22967
  * The pool reads new-side text through the session, so the snapshot allowlist applies to
21640
22968
  * every LSP path. Each snapshot decides which languages run: it re-opens the diff's files