@hcrosse/opencode-pr-tracker 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -62
- package/dist/server.js +1215 -2838
- package/dist/server.js.map +30 -21
- package/dist/tui.js +1112 -4393
- package/dist/tui.js.map +20 -28
- package/package.json +34 -26
- package/server.js +1 -0
- package/tui.js +1 -0
package/dist/server.js
CHANGED
|
@@ -1,2939 +1,1316 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
-
function __accessProp(key) {
|
|
8
|
-
return this[key];
|
|
9
|
-
}
|
|
10
|
-
var __toESMCache_node;
|
|
11
|
-
var __toESMCache_esm;
|
|
12
|
-
var __toESM = (mod, isNodeMode, target) => {
|
|
13
|
-
var canCache = mod != null && typeof mod === "object";
|
|
14
|
-
if (canCache) {
|
|
15
|
-
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
16
|
-
var cached = cache.get(mod);
|
|
17
|
-
if (cached)
|
|
18
|
-
return cached;
|
|
19
|
-
}
|
|
20
|
-
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
21
|
-
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
22
|
-
for (let key of __getOwnPropNames(mod))
|
|
23
|
-
if (!__hasOwnProp.call(to, key))
|
|
24
|
-
__defProp(to, key, {
|
|
25
|
-
get: __accessProp.bind(mod, key),
|
|
26
|
-
enumerable: true
|
|
27
|
-
});
|
|
28
|
-
if (canCache)
|
|
29
|
-
cache.set(mod, to);
|
|
30
|
-
return to;
|
|
31
|
-
};
|
|
32
|
-
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
33
|
-
var __require = import.meta.require;
|
|
2
|
+
// src/server.ts
|
|
3
|
+
import { Plugin } from "@opencode/plugin/effect";
|
|
4
|
+
import { Context as Context7, Effect as Effect13, Layer as Layer7, Option as Option17, Schedule, Schema as Schema15, Stream as Stream2 } from "effect";
|
|
34
5
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
6
|
+
// src/adapters/github/Client.ts
|
|
7
|
+
import { Array as Arr3, Effect as Effect4, Layer as Layer3, Option as Option6, Redacted as Redacted2, Result as Result4, Schema as Schema7 } from "effect";
|
|
8
|
+
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
9
|
+
|
|
10
|
+
// src/ports/GitHub.ts
|
|
11
|
+
import { Context, Schema as Schema3 } from "effect";
|
|
12
|
+
|
|
13
|
+
// src/domain/Snapshot.ts
|
|
14
|
+
import { Duration, Match, Schema as Schema2 } from "effect";
|
|
15
|
+
|
|
16
|
+
// src/domain/PullRequest.ts
|
|
17
|
+
import { Option, Result, Schema } from "effect";
|
|
18
|
+
var canonicalSegment = /^(?!\.{1,2}$)[a-z0-9._-]+$/u;
|
|
19
|
+
var pullRequestUrl = /^(?:https:\/\/)?github\.com\/(?<owner>[\w.-]+)\/(?<repository>[\w.-]+)\/pull\/(?<number>\d+)$/iu;
|
|
20
|
+
var decimal = /^\d+$/u;
|
|
21
|
+
var printableAscii = /^[\u0021-\u007E]*$/u;
|
|
22
|
+
var Segment = Schema.String.check(Schema.isPattern(canonicalSegment));
|
|
23
|
+
var PullRequestNumber = Schema.Int.check(Schema.isBetween({ maximum: Number.MAX_SAFE_INTEGER, minimum: 1 }));
|
|
24
|
+
|
|
25
|
+
class PullRequestRef extends Schema.Class("PullRequestRef")({
|
|
26
|
+
number: PullRequestNumber,
|
|
27
|
+
owner: Segment,
|
|
28
|
+
repository: Segment
|
|
29
|
+
}) {
|
|
30
|
+
get url() {
|
|
31
|
+
return `https://github.com/${this.owner}/${this.repository}/pull/${String(this.number)}`;
|
|
57
32
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
function patch(fs) {
|
|
61
|
-
if (constants.hasOwnProperty("O_SYMLINK") && process.version.match(/^v0\.6\.[0-2]|^v0\.5\./)) {
|
|
62
|
-
patchLchmod(fs);
|
|
63
|
-
}
|
|
64
|
-
if (!fs.lutimes) {
|
|
65
|
-
patchLutimes(fs);
|
|
66
|
-
}
|
|
67
|
-
fs.chown = chownFix(fs.chown);
|
|
68
|
-
fs.fchown = chownFix(fs.fchown);
|
|
69
|
-
fs.lchown = chownFix(fs.lchown);
|
|
70
|
-
fs.chmod = chmodFix(fs.chmod);
|
|
71
|
-
fs.fchmod = chmodFix(fs.fchmod);
|
|
72
|
-
fs.lchmod = chmodFix(fs.lchmod);
|
|
73
|
-
fs.chownSync = chownFixSync(fs.chownSync);
|
|
74
|
-
fs.fchownSync = chownFixSync(fs.fchownSync);
|
|
75
|
-
fs.lchownSync = chownFixSync(fs.lchownSync);
|
|
76
|
-
fs.chmodSync = chmodFixSync(fs.chmodSync);
|
|
77
|
-
fs.fchmodSync = chmodFixSync(fs.fchmodSync);
|
|
78
|
-
fs.lchmodSync = chmodFixSync(fs.lchmodSync);
|
|
79
|
-
fs.stat = statFix(fs.stat);
|
|
80
|
-
fs.fstat = statFix(fs.fstat);
|
|
81
|
-
fs.lstat = statFix(fs.lstat);
|
|
82
|
-
fs.statSync = statFixSync(fs.statSync);
|
|
83
|
-
fs.fstatSync = statFixSync(fs.fstatSync);
|
|
84
|
-
fs.lstatSync = statFixSync(fs.lstatSync);
|
|
85
|
-
if (fs.chmod && !fs.lchmod) {
|
|
86
|
-
fs.lchmod = function(path, mode, cb) {
|
|
87
|
-
if (cb)
|
|
88
|
-
process.nextTick(cb);
|
|
89
|
-
};
|
|
90
|
-
fs.lchmodSync = function() {};
|
|
91
|
-
}
|
|
92
|
-
if (fs.chown && !fs.lchown) {
|
|
93
|
-
fs.lchown = function(path, uid, gid, cb) {
|
|
94
|
-
if (cb)
|
|
95
|
-
process.nextTick(cb);
|
|
96
|
-
};
|
|
97
|
-
fs.lchownSync = function() {};
|
|
98
|
-
}
|
|
99
|
-
if (platform === "win32") {
|
|
100
|
-
fs.rename = typeof fs.rename !== "function" ? fs.rename : function(fs$rename) {
|
|
101
|
-
function rename(from, to, cb) {
|
|
102
|
-
var start = Date.now();
|
|
103
|
-
var backoff = 0;
|
|
104
|
-
fs$rename(from, to, function CB(er) {
|
|
105
|
-
if (er && (er.code === "EACCES" || er.code === "EPERM" || er.code === "EBUSY") && Date.now() - start < 60000) {
|
|
106
|
-
setTimeout(function() {
|
|
107
|
-
fs.stat(to, function(stater, st) {
|
|
108
|
-
if (stater && stater.code === "ENOENT")
|
|
109
|
-
fs$rename(from, to, CB);
|
|
110
|
-
else
|
|
111
|
-
cb(er);
|
|
112
|
-
});
|
|
113
|
-
}, backoff);
|
|
114
|
-
if (backoff < 100)
|
|
115
|
-
backoff += 10;
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
if (cb)
|
|
119
|
-
cb(er);
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
if (Object.setPrototypeOf)
|
|
123
|
-
Object.setPrototypeOf(rename, fs$rename);
|
|
124
|
-
return rename;
|
|
125
|
-
}(fs.rename);
|
|
126
|
-
}
|
|
127
|
-
fs.read = typeof fs.read !== "function" ? fs.read : function(fs$read) {
|
|
128
|
-
function read(fd, buffer, offset, length, position, callback_) {
|
|
129
|
-
var callback;
|
|
130
|
-
if (callback_ && typeof callback_ === "function") {
|
|
131
|
-
var eagCounter = 0;
|
|
132
|
-
callback = function(er, _, __) {
|
|
133
|
-
if (er && er.code === "EAGAIN" && eagCounter < 10) {
|
|
134
|
-
eagCounter++;
|
|
135
|
-
return fs$read.call(fs, fd, buffer, offset, length, position, callback);
|
|
136
|
-
}
|
|
137
|
-
callback_.apply(this, arguments);
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
return fs$read.call(fs, fd, buffer, offset, length, position, callback);
|
|
141
|
-
}
|
|
142
|
-
if (Object.setPrototypeOf)
|
|
143
|
-
Object.setPrototypeOf(read, fs$read);
|
|
144
|
-
return read;
|
|
145
|
-
}(fs.read);
|
|
146
|
-
fs.readSync = typeof fs.readSync !== "function" ? fs.readSync : function(fs$readSync) {
|
|
147
|
-
return function(fd, buffer, offset, length, position) {
|
|
148
|
-
var eagCounter = 0;
|
|
149
|
-
while (true) {
|
|
150
|
-
try {
|
|
151
|
-
return fs$readSync.call(fs, fd, buffer, offset, length, position);
|
|
152
|
-
} catch (er) {
|
|
153
|
-
if (er.code === "EAGAIN" && eagCounter < 10) {
|
|
154
|
-
eagCounter++;
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
throw er;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
}(fs.readSync);
|
|
162
|
-
function patchLchmod(fs2) {
|
|
163
|
-
fs2.lchmod = function(path, mode, callback) {
|
|
164
|
-
fs2.open(path, constants.O_WRONLY | constants.O_SYMLINK, mode, function(err, fd) {
|
|
165
|
-
if (err) {
|
|
166
|
-
if (callback)
|
|
167
|
-
callback(err);
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
fs2.fchmod(fd, mode, function(err2) {
|
|
171
|
-
fs2.close(fd, function(err22) {
|
|
172
|
-
if (callback)
|
|
173
|
-
callback(err2 || err22);
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
};
|
|
178
|
-
fs2.lchmodSync = function(path, mode) {
|
|
179
|
-
var fd = fs2.openSync(path, constants.O_WRONLY | constants.O_SYMLINK, mode);
|
|
180
|
-
var threw = true;
|
|
181
|
-
var ret;
|
|
182
|
-
try {
|
|
183
|
-
ret = fs2.fchmodSync(fd, mode);
|
|
184
|
-
threw = false;
|
|
185
|
-
} finally {
|
|
186
|
-
if (threw) {
|
|
187
|
-
try {
|
|
188
|
-
fs2.closeSync(fd);
|
|
189
|
-
} catch (er) {}
|
|
190
|
-
} else {
|
|
191
|
-
fs2.closeSync(fd);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
return ret;
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
function patchLutimes(fs2) {
|
|
198
|
-
if (constants.hasOwnProperty("O_SYMLINK") && fs2.futimes) {
|
|
199
|
-
fs2.lutimes = function(path, at, mt, cb) {
|
|
200
|
-
fs2.open(path, constants.O_SYMLINK, function(er, fd) {
|
|
201
|
-
if (er) {
|
|
202
|
-
if (cb)
|
|
203
|
-
cb(er);
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
fs2.futimes(fd, at, mt, function(er2) {
|
|
207
|
-
fs2.close(fd, function(er22) {
|
|
208
|
-
if (cb)
|
|
209
|
-
cb(er2 || er22);
|
|
210
|
-
});
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
};
|
|
214
|
-
fs2.lutimesSync = function(path, at, mt) {
|
|
215
|
-
var fd = fs2.openSync(path, constants.O_SYMLINK);
|
|
216
|
-
var ret;
|
|
217
|
-
var threw = true;
|
|
218
|
-
try {
|
|
219
|
-
ret = fs2.futimesSync(fd, at, mt);
|
|
220
|
-
threw = false;
|
|
221
|
-
} finally {
|
|
222
|
-
if (threw) {
|
|
223
|
-
try {
|
|
224
|
-
fs2.closeSync(fd);
|
|
225
|
-
} catch (er) {}
|
|
226
|
-
} else {
|
|
227
|
-
fs2.closeSync(fd);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return ret;
|
|
231
|
-
};
|
|
232
|
-
} else if (fs2.futimes) {
|
|
233
|
-
fs2.lutimes = function(_a, _b, _c, cb) {
|
|
234
|
-
if (cb)
|
|
235
|
-
process.nextTick(cb);
|
|
236
|
-
};
|
|
237
|
-
fs2.lutimesSync = function() {};
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
function chmodFix(orig) {
|
|
241
|
-
if (!orig)
|
|
242
|
-
return orig;
|
|
243
|
-
return function(target, mode, cb) {
|
|
244
|
-
return orig.call(fs, target, mode, function(er) {
|
|
245
|
-
if (chownErOk(er))
|
|
246
|
-
er = null;
|
|
247
|
-
if (cb)
|
|
248
|
-
cb.apply(this, arguments);
|
|
249
|
-
});
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
function chmodFixSync(orig) {
|
|
253
|
-
if (!orig)
|
|
254
|
-
return orig;
|
|
255
|
-
return function(target, mode) {
|
|
256
|
-
try {
|
|
257
|
-
return orig.call(fs, target, mode);
|
|
258
|
-
} catch (er) {
|
|
259
|
-
if (!chownErOk(er))
|
|
260
|
-
throw er;
|
|
261
|
-
}
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
function chownFix(orig) {
|
|
265
|
-
if (!orig)
|
|
266
|
-
return orig;
|
|
267
|
-
return function(target, uid, gid, cb) {
|
|
268
|
-
return orig.call(fs, target, uid, gid, function(er) {
|
|
269
|
-
if (chownErOk(er))
|
|
270
|
-
er = null;
|
|
271
|
-
if (cb)
|
|
272
|
-
cb.apply(this, arguments);
|
|
273
|
-
});
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
function chownFixSync(orig) {
|
|
277
|
-
if (!orig)
|
|
278
|
-
return orig;
|
|
279
|
-
return function(target, uid, gid) {
|
|
280
|
-
try {
|
|
281
|
-
return orig.call(fs, target, uid, gid);
|
|
282
|
-
} catch (er) {
|
|
283
|
-
if (!chownErOk(er))
|
|
284
|
-
throw er;
|
|
285
|
-
}
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
function statFix(orig) {
|
|
289
|
-
if (!orig)
|
|
290
|
-
return orig;
|
|
291
|
-
return function(target, options, cb) {
|
|
292
|
-
if (typeof options === "function") {
|
|
293
|
-
cb = options;
|
|
294
|
-
options = null;
|
|
295
|
-
}
|
|
296
|
-
function callback(er, stats) {
|
|
297
|
-
if (stats) {
|
|
298
|
-
if (stats.uid < 0)
|
|
299
|
-
stats.uid += 4294967296;
|
|
300
|
-
if (stats.gid < 0)
|
|
301
|
-
stats.gid += 4294967296;
|
|
302
|
-
}
|
|
303
|
-
if (cb)
|
|
304
|
-
cb.apply(this, arguments);
|
|
305
|
-
}
|
|
306
|
-
return options ? orig.call(fs, target, options, callback) : orig.call(fs, target, callback);
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
function statFixSync(orig) {
|
|
310
|
-
if (!orig)
|
|
311
|
-
return orig;
|
|
312
|
-
return function(target, options) {
|
|
313
|
-
var stats = options ? orig.call(fs, target, options) : orig.call(fs, target);
|
|
314
|
-
if (stats) {
|
|
315
|
-
if (stats.uid < 0)
|
|
316
|
-
stats.uid += 4294967296;
|
|
317
|
-
if (stats.gid < 0)
|
|
318
|
-
stats.gid += 4294967296;
|
|
319
|
-
}
|
|
320
|
-
return stats;
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
function chownErOk(er) {
|
|
324
|
-
if (!er)
|
|
325
|
-
return true;
|
|
326
|
-
if (er.code === "ENOSYS")
|
|
327
|
-
return true;
|
|
328
|
-
var nonroot = !process.getuid || process.getuid() !== 0;
|
|
329
|
-
if (nonroot) {
|
|
330
|
-
if (er.code === "EINVAL" || er.code === "EPERM")
|
|
331
|
-
return true;
|
|
332
|
-
}
|
|
333
|
-
return false;
|
|
334
|
-
}
|
|
33
|
+
get label() {
|
|
34
|
+
return `${this.owner}/${this.repository}#${String(this.number)}`;
|
|
335
35
|
}
|
|
336
|
-
}
|
|
36
|
+
}
|
|
337
37
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
var Stream = __require("stream").Stream;
|
|
341
|
-
module.exports = legacy;
|
|
342
|
-
function legacy(fs) {
|
|
343
|
-
return {
|
|
344
|
-
ReadStream,
|
|
345
|
-
WriteStream
|
|
346
|
-
};
|
|
347
|
-
function ReadStream(path, options) {
|
|
348
|
-
if (!(this instanceof ReadStream))
|
|
349
|
-
return new ReadStream(path, options);
|
|
350
|
-
Stream.call(this);
|
|
351
|
-
var self = this;
|
|
352
|
-
this.path = path;
|
|
353
|
-
this.fd = null;
|
|
354
|
-
this.readable = true;
|
|
355
|
-
this.paused = false;
|
|
356
|
-
this.flags = "r";
|
|
357
|
-
this.mode = 438;
|
|
358
|
-
this.bufferSize = 64 * 1024;
|
|
359
|
-
options = options || {};
|
|
360
|
-
var keys = Object.keys(options);
|
|
361
|
-
for (var index = 0, length = keys.length;index < length; index++) {
|
|
362
|
-
var key = keys[index];
|
|
363
|
-
this[key] = options[key];
|
|
364
|
-
}
|
|
365
|
-
if (this.encoding)
|
|
366
|
-
this.setEncoding(this.encoding);
|
|
367
|
-
if (this.start !== undefined) {
|
|
368
|
-
if (typeof this.start !== "number") {
|
|
369
|
-
throw TypeError("start must be a Number");
|
|
370
|
-
}
|
|
371
|
-
if (this.end === undefined) {
|
|
372
|
-
this.end = Infinity;
|
|
373
|
-
} else if (typeof this.end !== "number") {
|
|
374
|
-
throw TypeError("end must be a Number");
|
|
375
|
-
}
|
|
376
|
-
if (this.start > this.end) {
|
|
377
|
-
throw new Error("start must be <= end");
|
|
378
|
-
}
|
|
379
|
-
this.pos = this.start;
|
|
380
|
-
}
|
|
381
|
-
if (this.fd !== null) {
|
|
382
|
-
process.nextTick(function() {
|
|
383
|
-
self._read();
|
|
384
|
-
});
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
fs.open(this.path, this.flags, this.mode, function(err, fd) {
|
|
388
|
-
if (err) {
|
|
389
|
-
self.emit("error", err);
|
|
390
|
-
self.readable = false;
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
self.fd = fd;
|
|
394
|
-
self.emit("open", fd);
|
|
395
|
-
self._read();
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
function WriteStream(path, options) {
|
|
399
|
-
if (!(this instanceof WriteStream))
|
|
400
|
-
return new WriteStream(path, options);
|
|
401
|
-
Stream.call(this);
|
|
402
|
-
this.path = path;
|
|
403
|
-
this.fd = null;
|
|
404
|
-
this.writable = true;
|
|
405
|
-
this.flags = "w";
|
|
406
|
-
this.encoding = "binary";
|
|
407
|
-
this.mode = 438;
|
|
408
|
-
this.bytesWritten = 0;
|
|
409
|
-
options = options || {};
|
|
410
|
-
var keys = Object.keys(options);
|
|
411
|
-
for (var index = 0, length = keys.length;index < length; index++) {
|
|
412
|
-
var key = keys[index];
|
|
413
|
-
this[key] = options[key];
|
|
414
|
-
}
|
|
415
|
-
if (this.start !== undefined) {
|
|
416
|
-
if (typeof this.start !== "number") {
|
|
417
|
-
throw TypeError("start must be a Number");
|
|
418
|
-
}
|
|
419
|
-
if (this.start < 0) {
|
|
420
|
-
throw new Error("start must be >= zero");
|
|
421
|
-
}
|
|
422
|
-
this.pos = this.start;
|
|
423
|
-
}
|
|
424
|
-
this.busy = false;
|
|
425
|
-
this._queue = [];
|
|
426
|
-
if (this.fd === null) {
|
|
427
|
-
this._open = fs.open;
|
|
428
|
-
this._queue.push([this._open, this.path, this.flags, this.mode, undefined]);
|
|
429
|
-
this.flush();
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
});
|
|
38
|
+
class InvalidPullRequestUrl extends Schema.TaggedError()("InvalidPullRequestUrl", { input: Schema.String }) {
|
|
39
|
+
}
|
|
434
40
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
41
|
+
class InvalidPullRequestInput extends Schema.TaggedError()("InvalidPullRequestInput", { input: Schema.String }) {
|
|
42
|
+
}
|
|
43
|
+
var decodeRef = Schema.decodeUnknownOption(PullRequestRef);
|
|
44
|
+
var decodeNumber = Schema.decodeUnknownOption(PullRequestNumber);
|
|
45
|
+
function samePullRequest(left, right) {
|
|
46
|
+
return left.url === right.url;
|
|
47
|
+
}
|
|
48
|
+
function parsePullRequestUrl(input) {
|
|
49
|
+
const match = printableAscii.test(input) ? pullRequestUrl.exec(input) : null;
|
|
50
|
+
const groups = match === null ? {} : match.groups ?? {};
|
|
51
|
+
const candidate = {
|
|
52
|
+
number: Number(groups["number"] ?? Number.NaN),
|
|
53
|
+
owner: (groups["owner"] ?? "").toLowerCase(),
|
|
54
|
+
repository: (groups["repository"] ?? "").toLowerCase()
|
|
440
55
|
};
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
56
|
+
return Result.fromOption(decodeRef(candidate), () => new InvalidPullRequestUrl({ input }));
|
|
57
|
+
}
|
|
58
|
+
function parsePullRequestInput(input) {
|
|
59
|
+
const reference = parsePullRequestUrl(input);
|
|
60
|
+
if (Result.isSuccess(reference))
|
|
61
|
+
return Result.succeed({ _tag: "Reference", ref: reference.success });
|
|
62
|
+
const number = decimal.test(input) ? decodeNumber(Number(input)) : Option.none();
|
|
63
|
+
return Result.fromOption(number, () => new InvalidPullRequestInput({ input })).pipe(Result.map((value) => ({ _tag: "Number", number: value })));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/domain/Snapshot.ts
|
|
67
|
+
var Ci = Schema2.Literals(["passed", "pending", "failed", "none"]);
|
|
68
|
+
var Mergeability = Schema2.Literals(["mergeable", "conflicting", "unknown"]);
|
|
69
|
+
var PullRequestState = Schema2.Union([
|
|
70
|
+
Schema2.TaggedStruct("Open", {
|
|
71
|
+
behind: Schema2.Boolean,
|
|
72
|
+
ci: Ci,
|
|
73
|
+
draft: Schema2.Boolean,
|
|
74
|
+
mergeability: Mergeability
|
|
75
|
+
}),
|
|
76
|
+
Schema2.TaggedStruct("Merged", {}),
|
|
77
|
+
Schema2.TaggedStruct("Closed", {})
|
|
78
|
+
]);
|
|
79
|
+
var Snapshot = Schema2.Struct({
|
|
80
|
+
ref: PullRequestRef,
|
|
81
|
+
state: PullRequestState,
|
|
82
|
+
title: Schema2.String
|
|
453
83
|
});
|
|
84
|
+
var Diagnostic = Schema2.Literals([
|
|
85
|
+
"GitHubCliMissing",
|
|
86
|
+
"AuthenticationRequired",
|
|
87
|
+
"GitHubUnavailable",
|
|
88
|
+
"NotFound",
|
|
89
|
+
"InvalidResponse"
|
|
90
|
+
]);
|
|
91
|
+
var Status = Schema2.Union([
|
|
92
|
+
Schema2.TaggedStruct("Pending", {}),
|
|
93
|
+
Schema2.TaggedStruct("Fresh", { snapshot: Snapshot }),
|
|
94
|
+
Schema2.TaggedStruct("Stale", {
|
|
95
|
+
diagnostic: Diagnostic,
|
|
96
|
+
failingSince: Schema2.Int,
|
|
97
|
+
snapshot: Snapshot
|
|
98
|
+
}),
|
|
99
|
+
Schema2.TaggedStruct("Unavailable", { diagnostic: Diagnostic })
|
|
100
|
+
]);
|
|
101
|
+
var staleLimit = Duration.minutes(5);
|
|
102
|
+
var pending = { _tag: "Pending" };
|
|
103
|
+
function succeeded(snapshot) {
|
|
104
|
+
return { _tag: "Fresh", snapshot };
|
|
105
|
+
}
|
|
106
|
+
function failed(status, diagnostic, now) {
|
|
107
|
+
return Match.valueTags(status, {
|
|
108
|
+
Fresh: ({ snapshot }) => ({ _tag: "Stale", diagnostic, failingSince: now, snapshot }),
|
|
109
|
+
Pending: () => ({ _tag: "Unavailable", diagnostic }),
|
|
110
|
+
Stale: ({ failingSince, snapshot }) => now - failingSince >= Duration.toMillis(staleLimit) ? { _tag: "Unavailable", diagnostic } : { _tag: "Stale", diagnostic, failingSince, snapshot },
|
|
111
|
+
Unavailable: () => ({ _tag: "Unavailable", diagnostic })
|
|
112
|
+
});
|
|
113
|
+
}
|
|
454
114
|
|
|
455
|
-
//
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
return fs$readFile(path2, options2, function(err) {
|
|
546
|
-
if (err && (err.code === "EMFILE" || err.code === "ENFILE"))
|
|
547
|
-
enqueue([go$readFile, [path2, options2, cb2], err, startTime || Date.now(), Date.now()]);
|
|
548
|
-
else {
|
|
549
|
-
if (typeof cb2 === "function")
|
|
550
|
-
cb2.apply(this, arguments);
|
|
551
|
-
}
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
var fs$writeFile = fs2.writeFile;
|
|
556
|
-
fs2.writeFile = writeFile;
|
|
557
|
-
function writeFile(path, data, options, cb) {
|
|
558
|
-
if (typeof options === "function")
|
|
559
|
-
cb = options, options = null;
|
|
560
|
-
return go$writeFile(path, data, options, cb);
|
|
561
|
-
function go$writeFile(path2, data2, options2, cb2, startTime) {
|
|
562
|
-
return fs$writeFile(path2, data2, options2, function(err) {
|
|
563
|
-
if (err && (err.code === "EMFILE" || err.code === "ENFILE"))
|
|
564
|
-
enqueue([go$writeFile, [path2, data2, options2, cb2], err, startTime || Date.now(), Date.now()]);
|
|
565
|
-
else {
|
|
566
|
-
if (typeof cb2 === "function")
|
|
567
|
-
cb2.apply(this, arguments);
|
|
568
|
-
}
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
var fs$appendFile = fs2.appendFile;
|
|
573
|
-
if (fs$appendFile)
|
|
574
|
-
fs2.appendFile = appendFile;
|
|
575
|
-
function appendFile(path, data, options, cb) {
|
|
576
|
-
if (typeof options === "function")
|
|
577
|
-
cb = options, options = null;
|
|
578
|
-
return go$appendFile(path, data, options, cb);
|
|
579
|
-
function go$appendFile(path2, data2, options2, cb2, startTime) {
|
|
580
|
-
return fs$appendFile(path2, data2, options2, function(err) {
|
|
581
|
-
if (err && (err.code === "EMFILE" || err.code === "ENFILE"))
|
|
582
|
-
enqueue([go$appendFile, [path2, data2, options2, cb2], err, startTime || Date.now(), Date.now()]);
|
|
583
|
-
else {
|
|
584
|
-
if (typeof cb2 === "function")
|
|
585
|
-
cb2.apply(this, arguments);
|
|
586
|
-
}
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
var fs$copyFile = fs2.copyFile;
|
|
591
|
-
if (fs$copyFile)
|
|
592
|
-
fs2.copyFile = copyFile;
|
|
593
|
-
function copyFile(src, dest, flags, cb) {
|
|
594
|
-
if (typeof flags === "function") {
|
|
595
|
-
cb = flags;
|
|
596
|
-
flags = 0;
|
|
597
|
-
}
|
|
598
|
-
return go$copyFile(src, dest, flags, cb);
|
|
599
|
-
function go$copyFile(src2, dest2, flags2, cb2, startTime) {
|
|
600
|
-
return fs$copyFile(src2, dest2, flags2, function(err) {
|
|
601
|
-
if (err && (err.code === "EMFILE" || err.code === "ENFILE"))
|
|
602
|
-
enqueue([go$copyFile, [src2, dest2, flags2, cb2], err, startTime || Date.now(), Date.now()]);
|
|
603
|
-
else {
|
|
604
|
-
if (typeof cb2 === "function")
|
|
605
|
-
cb2.apply(this, arguments);
|
|
606
|
-
}
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
var fs$readdir = fs2.readdir;
|
|
611
|
-
fs2.readdir = readdir;
|
|
612
|
-
var noReaddirOptionVersions = /^v[0-5]\./;
|
|
613
|
-
function readdir(path, options, cb) {
|
|
614
|
-
if (typeof options === "function")
|
|
615
|
-
cb = options, options = null;
|
|
616
|
-
var go$readdir = noReaddirOptionVersions.test(process.version) ? function go$readdir2(path2, options2, cb2, startTime) {
|
|
617
|
-
return fs$readdir(path2, fs$readdirCallback(path2, options2, cb2, startTime));
|
|
618
|
-
} : function go$readdir2(path2, options2, cb2, startTime) {
|
|
619
|
-
return fs$readdir(path2, options2, fs$readdirCallback(path2, options2, cb2, startTime));
|
|
620
|
-
};
|
|
621
|
-
return go$readdir(path, options, cb);
|
|
622
|
-
function fs$readdirCallback(path2, options2, cb2, startTime) {
|
|
623
|
-
return function(err, files) {
|
|
624
|
-
if (err && (err.code === "EMFILE" || err.code === "ENFILE"))
|
|
625
|
-
enqueue([
|
|
626
|
-
go$readdir,
|
|
627
|
-
[path2, options2, cb2],
|
|
628
|
-
err,
|
|
629
|
-
startTime || Date.now(),
|
|
630
|
-
Date.now()
|
|
631
|
-
]);
|
|
632
|
-
else {
|
|
633
|
-
if (files && files.sort)
|
|
634
|
-
files.sort();
|
|
635
|
-
if (typeof cb2 === "function")
|
|
636
|
-
cb2.call(this, err, files);
|
|
637
|
-
}
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
if (process.version.substr(0, 4) === "v0.8") {
|
|
642
|
-
var legStreams = legacy(fs2);
|
|
643
|
-
ReadStream = legStreams.ReadStream;
|
|
644
|
-
WriteStream = legStreams.WriteStream;
|
|
645
|
-
}
|
|
646
|
-
var fs$ReadStream = fs2.ReadStream;
|
|
647
|
-
if (fs$ReadStream) {
|
|
648
|
-
ReadStream.prototype = Object.create(fs$ReadStream.prototype);
|
|
649
|
-
ReadStream.prototype.open = ReadStream$open;
|
|
650
|
-
}
|
|
651
|
-
var fs$WriteStream = fs2.WriteStream;
|
|
652
|
-
if (fs$WriteStream) {
|
|
653
|
-
WriteStream.prototype = Object.create(fs$WriteStream.prototype);
|
|
654
|
-
WriteStream.prototype.open = WriteStream$open;
|
|
655
|
-
}
|
|
656
|
-
Object.defineProperty(fs2, "ReadStream", {
|
|
657
|
-
get: function() {
|
|
658
|
-
return ReadStream;
|
|
659
|
-
},
|
|
660
|
-
set: function(val) {
|
|
661
|
-
ReadStream = val;
|
|
662
|
-
},
|
|
663
|
-
enumerable: true,
|
|
664
|
-
configurable: true
|
|
665
|
-
});
|
|
666
|
-
Object.defineProperty(fs2, "WriteStream", {
|
|
667
|
-
get: function() {
|
|
668
|
-
return WriteStream;
|
|
669
|
-
},
|
|
670
|
-
set: function(val) {
|
|
671
|
-
WriteStream = val;
|
|
672
|
-
},
|
|
673
|
-
enumerable: true,
|
|
674
|
-
configurable: true
|
|
675
|
-
});
|
|
676
|
-
var FileReadStream = ReadStream;
|
|
677
|
-
Object.defineProperty(fs2, "FileReadStream", {
|
|
678
|
-
get: function() {
|
|
679
|
-
return FileReadStream;
|
|
680
|
-
},
|
|
681
|
-
set: function(val) {
|
|
682
|
-
FileReadStream = val;
|
|
683
|
-
},
|
|
684
|
-
enumerable: true,
|
|
685
|
-
configurable: true
|
|
686
|
-
});
|
|
687
|
-
var FileWriteStream = WriteStream;
|
|
688
|
-
Object.defineProperty(fs2, "FileWriteStream", {
|
|
689
|
-
get: function() {
|
|
690
|
-
return FileWriteStream;
|
|
691
|
-
},
|
|
692
|
-
set: function(val) {
|
|
693
|
-
FileWriteStream = val;
|
|
694
|
-
},
|
|
695
|
-
enumerable: true,
|
|
696
|
-
configurable: true
|
|
697
|
-
});
|
|
698
|
-
function ReadStream(path, options) {
|
|
699
|
-
if (this instanceof ReadStream)
|
|
700
|
-
return fs$ReadStream.apply(this, arguments), this;
|
|
701
|
-
else
|
|
702
|
-
return ReadStream.apply(Object.create(ReadStream.prototype), arguments);
|
|
703
|
-
}
|
|
704
|
-
function ReadStream$open() {
|
|
705
|
-
var that = this;
|
|
706
|
-
open(that.path, that.flags, that.mode, function(err, fd) {
|
|
707
|
-
if (err) {
|
|
708
|
-
if (that.autoClose)
|
|
709
|
-
that.destroy();
|
|
710
|
-
that.emit("error", err);
|
|
711
|
-
} else {
|
|
712
|
-
that.fd = fd;
|
|
713
|
-
that.emit("open", fd);
|
|
714
|
-
that.read();
|
|
715
|
-
}
|
|
716
|
-
});
|
|
717
|
-
}
|
|
718
|
-
function WriteStream(path, options) {
|
|
719
|
-
if (this instanceof WriteStream)
|
|
720
|
-
return fs$WriteStream.apply(this, arguments), this;
|
|
721
|
-
else
|
|
722
|
-
return WriteStream.apply(Object.create(WriteStream.prototype), arguments);
|
|
723
|
-
}
|
|
724
|
-
function WriteStream$open() {
|
|
725
|
-
var that = this;
|
|
726
|
-
open(that.path, that.flags, that.mode, function(err, fd) {
|
|
727
|
-
if (err) {
|
|
728
|
-
that.destroy();
|
|
729
|
-
that.emit("error", err);
|
|
730
|
-
} else {
|
|
731
|
-
that.fd = fd;
|
|
732
|
-
that.emit("open", fd);
|
|
115
|
+
// src/ports/GitHub.ts
|
|
116
|
+
class GitHubFailure extends Schema3.TaggedError()("GitHubFailure", {
|
|
117
|
+
diagnostic: Diagnostic
|
|
118
|
+
}) {
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
class RepositoryUnavailable extends Schema3.TaggedError()("RepositoryUnavailable", { directory: Schema3.String }) {
|
|
122
|
+
}
|
|
123
|
+
var maximumBatch = 20;
|
|
124
|
+
|
|
125
|
+
class GitHub extends Context.Service()("opencode-pr-tracker/GitHub") {
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/adapters/Command.ts
|
|
129
|
+
import { Context as Context2, Effect, Layer, Schema as Schema4 } from "effect";
|
|
130
|
+
|
|
131
|
+
class CommandMissing extends Schema4.TaggedError()("CommandMissing", {
|
|
132
|
+
command: Schema4.String
|
|
133
|
+
}) {
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
class CommandFailed extends Schema4.TaggedError()("CommandFailed", {
|
|
137
|
+
command: Schema4.String,
|
|
138
|
+
exitCode: Schema4.Int,
|
|
139
|
+
stderr: Schema4.String
|
|
140
|
+
}) {
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
class CommandRunner extends Context2.Service()("opencode-pr-tracker/CommandRunner") {
|
|
144
|
+
}
|
|
145
|
+
var isMissingExecutable = Schema4.is(Schema4.Struct({ code: Schema4.Literal("ENOENT") }));
|
|
146
|
+
function spawn(command, args, cwd) {
|
|
147
|
+
const process2 = Bun.spawn([command, ...args], {
|
|
148
|
+
cwd,
|
|
149
|
+
stderr: "pipe",
|
|
150
|
+
stdin: "ignore",
|
|
151
|
+
stdout: "pipe"
|
|
152
|
+
});
|
|
153
|
+
return {
|
|
154
|
+
completed: async () => {
|
|
155
|
+
const result = await Promise.all([
|
|
156
|
+
new Response(process2.stdout).text(),
|
|
157
|
+
new Response(process2.stderr).text(),
|
|
158
|
+
process2.exited
|
|
159
|
+
]);
|
|
160
|
+
return result;
|
|
161
|
+
},
|
|
162
|
+
kill: () => {
|
|
163
|
+
process2.kill();
|
|
164
|
+
},
|
|
165
|
+
running: () => process2.exitCode === null
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function start(command, args, cwd) {
|
|
169
|
+
const started = Effect.try({
|
|
170
|
+
catch: (cause) => isMissingExecutable(cause) ? new CommandMissing({ command }) : new CommandFailed({ command, exitCode: -1, stderr: String(cause) }),
|
|
171
|
+
try: () => spawn(command, args, cwd)
|
|
172
|
+
});
|
|
173
|
+
return Effect.acquireRelease(started, (child) => Effect.sync(() => {
|
|
174
|
+
if (child.running())
|
|
175
|
+
child.kill();
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
function output(command, child) {
|
|
179
|
+
return Effect.gen(function* () {
|
|
180
|
+
const [stdout, stderr, exitCode] = yield* Effect.promise(child.completed);
|
|
181
|
+
if (exitCode === 0)
|
|
182
|
+
return stdout;
|
|
183
|
+
return yield* new CommandFailed({ command, exitCode, stderr });
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
var layer = Layer.succeed(CommandRunner, CommandRunner.of({
|
|
187
|
+
run: (command, args, cwd) => Effect.scoped(Effect.flatMap(start(command, args, cwd), (child) => output(command, child)))
|
|
188
|
+
}));
|
|
189
|
+
|
|
190
|
+
// src/adapters/github/Query.ts
|
|
191
|
+
var defaultPageSize = 100;
|
|
192
|
+
var contexts = (pageSize, after) => `
|
|
193
|
+
contexts(first: ${String(pageSize)}${after}) {
|
|
194
|
+
pageInfo { hasNextPage endCursor }
|
|
195
|
+
nodes {
|
|
196
|
+
__typename
|
|
197
|
+
... on StatusContext { context state createdAt }
|
|
198
|
+
... on CheckRun {
|
|
199
|
+
name status conclusion
|
|
200
|
+
checkSuite {
|
|
201
|
+
id
|
|
202
|
+
createdAt
|
|
203
|
+
app { id }
|
|
204
|
+
workflowRun { event runNumber runAttempt workflow { id } }
|
|
733
205
|
}
|
|
734
|
-
});
|
|
735
|
-
}
|
|
736
|
-
function createReadStream(path, options) {
|
|
737
|
-
return new fs2.ReadStream(path, options);
|
|
738
|
-
}
|
|
739
|
-
function createWriteStream(path, options) {
|
|
740
|
-
return new fs2.WriteStream(path, options);
|
|
741
|
-
}
|
|
742
|
-
var fs$open = fs2.open;
|
|
743
|
-
fs2.open = open;
|
|
744
|
-
function open(path, flags, mode, cb) {
|
|
745
|
-
if (typeof mode === "function")
|
|
746
|
-
cb = mode, mode = null;
|
|
747
|
-
return go$open(path, flags, mode, cb);
|
|
748
|
-
function go$open(path2, flags2, mode2, cb2, startTime) {
|
|
749
|
-
return fs$open(path2, flags2, mode2, function(err, fd) {
|
|
750
|
-
if (err && (err.code === "EMFILE" || err.code === "ENFILE"))
|
|
751
|
-
enqueue([go$open, [path2, flags2, mode2, cb2], err, startTime || Date.now(), Date.now()]);
|
|
752
|
-
else {
|
|
753
|
-
if (typeof cb2 === "function")
|
|
754
|
-
cb2.apply(this, arguments);
|
|
755
|
-
}
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
return fs2;
|
|
760
|
-
}
|
|
761
|
-
function enqueue(elem) {
|
|
762
|
-
debug("ENQUEUE", elem[0].name, elem[1]);
|
|
763
|
-
fs[gracefulQueue].push(elem);
|
|
764
|
-
retry();
|
|
765
|
-
}
|
|
766
|
-
var retryTimer;
|
|
767
|
-
function resetQueue() {
|
|
768
|
-
var now = Date.now();
|
|
769
|
-
for (var i = 0;i < fs[gracefulQueue].length; ++i) {
|
|
770
|
-
if (fs[gracefulQueue][i].length > 2) {
|
|
771
|
-
fs[gracefulQueue][i][3] = now;
|
|
772
|
-
fs[gracefulQueue][i][4] = now;
|
|
773
206
|
}
|
|
774
207
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
var err = elem[2];
|
|
786
|
-
var startTime = elem[3];
|
|
787
|
-
var lastTime = elem[4];
|
|
788
|
-
if (startTime === undefined) {
|
|
789
|
-
debug("RETRY", fn.name, args);
|
|
790
|
-
fn.apply(null, args);
|
|
791
|
-
} else if (Date.now() - startTime >= 60000) {
|
|
792
|
-
debug("TIMEOUT", fn.name, args);
|
|
793
|
-
var cb = args.pop();
|
|
794
|
-
if (typeof cb === "function")
|
|
795
|
-
cb.call(null, err);
|
|
796
|
-
} else {
|
|
797
|
-
var sinceAttempt = Date.now() - lastTime;
|
|
798
|
-
var sinceStart = Math.max(lastTime - startTime, 1);
|
|
799
|
-
var desiredDelay = Math.min(sinceStart * 1.2, 100);
|
|
800
|
-
if (sinceAttempt >= desiredDelay) {
|
|
801
|
-
debug("RETRY", fn.name, args);
|
|
802
|
-
fn.apply(null, args.concat([startTime]));
|
|
803
|
-
} else {
|
|
804
|
-
fs[gracefulQueue].push(elem);
|
|
208
|
+
}`;
|
|
209
|
+
var pullRequest = (pageSize) => `
|
|
210
|
+
__typename
|
|
211
|
+
... on PullRequest {
|
|
212
|
+
url title state isDraft mergeable mergeStateStatus
|
|
213
|
+
stack {
|
|
214
|
+
id size
|
|
215
|
+
entries(first: 100) {
|
|
216
|
+
pageInfo { hasNextPage }
|
|
217
|
+
nodes { position pullRequest { url } }
|
|
805
218
|
}
|
|
806
219
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
220
|
+
statusCheckRollup { ${contexts(pageSize, "")} }
|
|
221
|
+
}`;
|
|
222
|
+
var alias = (index) => `pr${String(index)}`;
|
|
223
|
+
function batch(count, pageSize = defaultPageSize) {
|
|
224
|
+
const indexes = Array.from({ length: count }, (_, index) => index);
|
|
225
|
+
const variables = indexes.map((index) => `$${alias(index)}: URI!`).join(", ");
|
|
226
|
+
const fields = indexes.map((index) => `${alias(index)}: resource(url: $${alias(index)}) { ${pullRequest(pageSize)} }`).join(`
|
|
227
|
+
`);
|
|
228
|
+
return `query PullRequests(${variables}) {
|
|
229
|
+
${fields}
|
|
230
|
+
}`;
|
|
231
|
+
}
|
|
232
|
+
function continuation(pageSize = defaultPageSize) {
|
|
233
|
+
return `query CheckContexts($url: URI!, $cursor: String!) {
|
|
234
|
+
resource(url: $url) {
|
|
235
|
+
__typename
|
|
236
|
+
... on PullRequest { statusCheckRollup { ${contexts(pageSize, ", after: $cursor")} } }
|
|
810
237
|
}
|
|
238
|
+
}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/adapters/github/Repository.ts
|
|
242
|
+
import { Effect as Effect2, Result as Result2, Schema as Schema5 } from "effect";
|
|
243
|
+
var RepositoryView = Schema5.fromJsonString(Schema5.Struct({ url: Schema5.String }));
|
|
244
|
+
var resolveInRepository = Effect2.fn("resolveInRepository")(function* (directory, number) {
|
|
245
|
+
const runner = yield* CommandRunner;
|
|
246
|
+
const unavailable = new RepositoryUnavailable({ directory });
|
|
247
|
+
const output = yield* runner.run("gh", ["repo", "view", "--json", "url"], directory).pipe(Effect2.catchTags({
|
|
248
|
+
CommandFailed: () => Effect2.fail(unavailable),
|
|
249
|
+
CommandMissing: () => Effect2.fail(new GitHubFailure({ diagnostic: "GitHubCliMissing" }))
|
|
250
|
+
}));
|
|
251
|
+
const view = yield* Schema5.decodeUnknownEffect(RepositoryView)(output).pipe(Effect2.mapError(() => unavailable));
|
|
252
|
+
return yield* Result2.match(parsePullRequestUrl(`${view.url}/pull/${String(number)}`), {
|
|
253
|
+
onFailure: () => Effect2.fail(unavailable),
|
|
254
|
+
onSuccess: Effect2.succeed
|
|
255
|
+
});
|
|
811
256
|
});
|
|
812
257
|
|
|
813
|
-
//
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
if (this._options.forever) {
|
|
831
|
-
this._cachedTimeouts = this._timeouts.slice(0);
|
|
258
|
+
// src/adapters/github/Response.ts
|
|
259
|
+
import { Array as Arr2, Option as Option4, Result as Result3, Schema as Schema6 } from "effect";
|
|
260
|
+
|
|
261
|
+
// src/domain/Checks.ts
|
|
262
|
+
import { Array as Arr, Option as Option3, Order } from "effect";
|
|
263
|
+
var generationOrder = Arr.makeOrder(Order.Number);
|
|
264
|
+
function withRun(current, check) {
|
|
265
|
+
const replacement = { generation: check.generation, outcomes: [check.outcome] };
|
|
266
|
+
return Option3.match(current, {
|
|
267
|
+
onNone: () => replacement,
|
|
268
|
+
onSome: (existing) => {
|
|
269
|
+
const comparison = generationOrder(check.generation, existing.generation);
|
|
270
|
+
if (comparison > 0)
|
|
271
|
+
return replacement;
|
|
272
|
+
if (comparison < 0)
|
|
273
|
+
return existing;
|
|
274
|
+
return { generation: existing.generation, outcomes: [...existing.outcomes, check.outcome] };
|
|
832
275
|
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
function latestOutcomes(checks) {
|
|
279
|
+
const latest = new Map;
|
|
280
|
+
for (const check of checks) {
|
|
281
|
+
latest.set(check.identity, withRun(Option3.fromNullishOr(latest.get(check.identity)), check));
|
|
833
282
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
RetryOperation.prototype.retry = function(err) {
|
|
847
|
-
if (this._timeout) {
|
|
848
|
-
clearTimeout(this._timeout);
|
|
849
|
-
}
|
|
850
|
-
if (!err) {
|
|
851
|
-
return false;
|
|
852
|
-
}
|
|
853
|
-
var currentTime = new Date().getTime();
|
|
854
|
-
if (err && currentTime - this._operationStart >= this._maxRetryTime) {
|
|
855
|
-
this._errors.unshift(new Error("RetryOperation timeout occurred"));
|
|
856
|
-
return false;
|
|
857
|
-
}
|
|
858
|
-
this._errors.push(err);
|
|
859
|
-
var timeout = this._timeouts.shift();
|
|
860
|
-
if (timeout === undefined) {
|
|
861
|
-
if (this._cachedTimeouts) {
|
|
862
|
-
this._errors.splice(this._errors.length - 1, this._errors.length);
|
|
863
|
-
this._timeouts = this._cachedTimeouts.slice(0);
|
|
864
|
-
timeout = this._timeouts.shift();
|
|
865
|
-
} else {
|
|
866
|
-
return false;
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
var self = this;
|
|
870
|
-
var timer = setTimeout(function() {
|
|
871
|
-
self._attempts++;
|
|
872
|
-
if (self._operationTimeoutCb) {
|
|
873
|
-
self._timeout = setTimeout(function() {
|
|
874
|
-
self._operationTimeoutCb(self._attempts);
|
|
875
|
-
}, self._operationTimeout);
|
|
876
|
-
if (self._options.unref) {
|
|
877
|
-
self._timeout.unref();
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
self._fn(self._attempts);
|
|
881
|
-
}, timeout);
|
|
882
|
-
if (this._options.unref) {
|
|
883
|
-
timer.unref();
|
|
884
|
-
}
|
|
885
|
-
return true;
|
|
886
|
-
};
|
|
887
|
-
RetryOperation.prototype.attempt = function(fn, timeoutOps) {
|
|
888
|
-
this._fn = fn;
|
|
889
|
-
if (timeoutOps) {
|
|
890
|
-
if (timeoutOps.timeout) {
|
|
891
|
-
this._operationTimeout = timeoutOps.timeout;
|
|
892
|
-
}
|
|
893
|
-
if (timeoutOps.cb) {
|
|
894
|
-
this._operationTimeoutCb = timeoutOps.cb;
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
var self = this;
|
|
898
|
-
if (this._operationTimeoutCb) {
|
|
899
|
-
this._timeout = setTimeout(function() {
|
|
900
|
-
self._operationTimeoutCb();
|
|
901
|
-
}, self._operationTimeout);
|
|
902
|
-
}
|
|
903
|
-
this._operationStart = new Date().getTime();
|
|
904
|
-
this._fn(this._attempts);
|
|
905
|
-
};
|
|
906
|
-
RetryOperation.prototype.try = function(fn) {
|
|
907
|
-
console.log("Using RetryOperation.try() is deprecated");
|
|
908
|
-
this.attempt(fn);
|
|
909
|
-
};
|
|
910
|
-
RetryOperation.prototype.start = function(fn) {
|
|
911
|
-
console.log("Using RetryOperation.start() is deprecated");
|
|
912
|
-
this.attempt(fn);
|
|
913
|
-
};
|
|
914
|
-
RetryOperation.prototype.start = RetryOperation.prototype.try;
|
|
915
|
-
RetryOperation.prototype.errors = function() {
|
|
916
|
-
return this._errors;
|
|
917
|
-
};
|
|
918
|
-
RetryOperation.prototype.attempts = function() {
|
|
919
|
-
return this._attempts;
|
|
920
|
-
};
|
|
921
|
-
RetryOperation.prototype.mainError = function() {
|
|
922
|
-
if (this._errors.length === 0) {
|
|
923
|
-
return null;
|
|
924
|
-
}
|
|
925
|
-
var counts = {};
|
|
926
|
-
var mainError = null;
|
|
927
|
-
var mainErrorCount = 0;
|
|
928
|
-
for (var i = 0;i < this._errors.length; i++) {
|
|
929
|
-
var error = this._errors[i];
|
|
930
|
-
var message = error.message;
|
|
931
|
-
var count = (counts[message] || 0) + 1;
|
|
932
|
-
counts[message] = count;
|
|
933
|
-
if (count >= mainErrorCount) {
|
|
934
|
-
mainError = error;
|
|
935
|
-
mainErrorCount = count;
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
return mainError;
|
|
939
|
-
};
|
|
940
|
-
});
|
|
283
|
+
return new Set([...latest.values()].flatMap((entry) => entry.outcomes));
|
|
284
|
+
}
|
|
285
|
+
function classifyCi(checks) {
|
|
286
|
+
const outcomes = latestOutcomes(checks);
|
|
287
|
+
if (outcomes.has("failed"))
|
|
288
|
+
return "failed";
|
|
289
|
+
if (outcomes.has("pending"))
|
|
290
|
+
return "pending";
|
|
291
|
+
if (outcomes.has("passed"))
|
|
292
|
+
return "passed";
|
|
293
|
+
return "none";
|
|
294
|
+
}
|
|
941
295
|
|
|
942
|
-
//
|
|
943
|
-
var
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
296
|
+
// src/adapters/github/Response.ts
|
|
297
|
+
var PageInfo = Schema6.Struct({
|
|
298
|
+
endCursor: Schema6.NullOr(Schema6.String),
|
|
299
|
+
hasNextPage: Schema6.Boolean
|
|
300
|
+
});
|
|
301
|
+
var StatusState = Schema6.Literals(["EXPECTED", "PENDING", "SUCCESS", "ERROR", "FAILURE"]);
|
|
302
|
+
var MergeableState = Schema6.Literals(["MERGEABLE", "CONFLICTING", "UNKNOWN"]);
|
|
303
|
+
var StatusContextNode = Schema6.Struct({
|
|
304
|
+
__typename: Schema6.Literal("StatusContext"),
|
|
305
|
+
context: Schema6.String,
|
|
306
|
+
createdAt: Schema6.String,
|
|
307
|
+
state: StatusState
|
|
308
|
+
});
|
|
309
|
+
var CheckRunNode = Schema6.Struct({
|
|
310
|
+
__typename: Schema6.Literal("CheckRun"),
|
|
311
|
+
checkSuite: Schema6.Struct({
|
|
312
|
+
app: Schema6.NullOr(Schema6.Struct({ id: Schema6.String })),
|
|
313
|
+
createdAt: Schema6.String,
|
|
314
|
+
id: Schema6.String,
|
|
315
|
+
workflowRun: Schema6.NullOr(Schema6.Struct({
|
|
316
|
+
event: Schema6.String,
|
|
317
|
+
runAttempt: Schema6.Int,
|
|
318
|
+
runNumber: Schema6.Int,
|
|
319
|
+
workflow: Schema6.Struct({ id: Schema6.String })
|
|
320
|
+
}))
|
|
321
|
+
}),
|
|
322
|
+
conclusion: Schema6.NullOr(Schema6.String),
|
|
323
|
+
name: Schema6.String,
|
|
324
|
+
status: Schema6.String
|
|
325
|
+
});
|
|
326
|
+
var ContextNode = Schema6.Union([StatusContextNode, CheckRunNode]);
|
|
327
|
+
var Contexts = Schema6.Struct({
|
|
328
|
+
nodes: Schema6.Array(ContextNode),
|
|
329
|
+
pageInfo: PageInfo
|
|
330
|
+
});
|
|
331
|
+
var StackNode = Schema6.Struct({
|
|
332
|
+
entries: Schema6.Struct({
|
|
333
|
+
nodes: Schema6.Array(Schema6.Struct({ position: Schema6.Int, pullRequest: Schema6.Struct({ url: Schema6.String }) })),
|
|
334
|
+
pageInfo: Schema6.Struct({ hasNextPage: Schema6.Boolean })
|
|
335
|
+
}),
|
|
336
|
+
id: Schema6.String,
|
|
337
|
+
size: Schema6.Int
|
|
338
|
+
});
|
|
339
|
+
var PullRequestNode = Schema6.Struct({
|
|
340
|
+
__typename: Schema6.Literal("PullRequest"),
|
|
341
|
+
isDraft: Schema6.Boolean,
|
|
342
|
+
mergeStateStatus: Schema6.String,
|
|
343
|
+
mergeable: MergeableState,
|
|
344
|
+
stack: Schema6.NullOr(StackNode),
|
|
345
|
+
state: Schema6.Literals(["OPEN", "CLOSED", "MERGED"]),
|
|
346
|
+
statusCheckRollup: Schema6.NullOr(Schema6.Struct({ contexts: Contexts })),
|
|
347
|
+
title: Schema6.String,
|
|
348
|
+
url: Schema6.String
|
|
349
|
+
});
|
|
350
|
+
var failedConclusions = new Set([
|
|
351
|
+
"FAILURE",
|
|
352
|
+
"CANCELLED",
|
|
353
|
+
"TIMED_OUT",
|
|
354
|
+
"ACTION_REQUIRED",
|
|
355
|
+
"STARTUP_FAILURE",
|
|
356
|
+
"STALE"
|
|
357
|
+
]);
|
|
358
|
+
var timestamp = /^(?<seconds>[^.]+?)(?:\.(?<fraction>\d+))?Z$/u;
|
|
359
|
+
function generationOf(createdAt) {
|
|
360
|
+
const match = timestamp.exec(createdAt);
|
|
361
|
+
const groups = match === null ? {} : match.groups ?? {};
|
|
362
|
+
const seconds = Date.parse(`${groups["seconds"] ?? ""}Z`) / 1000;
|
|
363
|
+
const nanoseconds = Number((groups["fraction"] ?? "").padEnd(9, "0").slice(0, 9));
|
|
364
|
+
return [Number.isFinite(seconds) ? seconds : 0, nanoseconds];
|
|
365
|
+
}
|
|
366
|
+
function checkRunOutcome(status, conclusion) {
|
|
367
|
+
if (status !== "COMPLETED")
|
|
368
|
+
return "pending";
|
|
369
|
+
if (conclusion === "SUCCESS")
|
|
370
|
+
return "passed";
|
|
371
|
+
return failedConclusions.has(conclusion ?? "") ? "failed" : "ignored";
|
|
372
|
+
}
|
|
373
|
+
var mergeabilities = {
|
|
374
|
+
CONFLICTING: "conflicting",
|
|
375
|
+
MERGEABLE: "mergeable",
|
|
376
|
+
UNKNOWN: "unknown"
|
|
377
|
+
};
|
|
378
|
+
var statusOutcomes = {
|
|
379
|
+
ERROR: "failed",
|
|
380
|
+
EXPECTED: "pending",
|
|
381
|
+
FAILURE: "failed",
|
|
382
|
+
PENDING: "pending",
|
|
383
|
+
SUCCESS: "passed"
|
|
384
|
+
};
|
|
385
|
+
function toCheck(node) {
|
|
386
|
+
if (node.__typename === "StatusContext") {
|
|
387
|
+
const identity = `status ${node.context.toLowerCase()}`;
|
|
388
|
+
return {
|
|
389
|
+
generation: generationOf(node.createdAt),
|
|
390
|
+
identity,
|
|
391
|
+
outcome: statusOutcomes[node.state]
|
|
963
392
|
};
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
393
|
+
}
|
|
394
|
+
const outcome = checkRunOutcome(node.status, node.conclusion);
|
|
395
|
+
const source = node.checkSuite.app === null ? `suite ${node.checkSuite.id}` : `app ${node.checkSuite.app.id}`;
|
|
396
|
+
return Option4.match(Option4.fromNullishOr(node.checkSuite.workflowRun), {
|
|
397
|
+
onNone: () => ({
|
|
398
|
+
generation: generationOf(node.checkSuite.createdAt),
|
|
399
|
+
identity: `check ${source} ${node.name}`,
|
|
400
|
+
outcome
|
|
401
|
+
}),
|
|
402
|
+
onSome: (run) => ({
|
|
403
|
+
generation: [run.runNumber, run.runAttempt],
|
|
404
|
+
identity: `workflow ${source} ${run.workflow.id} ${run.event} ${node.name}`,
|
|
405
|
+
outcome
|
|
406
|
+
})
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
function toState(node, contexts) {
|
|
410
|
+
if (node.state === "MERGED")
|
|
411
|
+
return { _tag: "Merged" };
|
|
412
|
+
if (node.state === "CLOSED")
|
|
413
|
+
return { _tag: "Closed" };
|
|
414
|
+
return {
|
|
415
|
+
_tag: "Open",
|
|
416
|
+
behind: node.mergeStateStatus === "BEHIND",
|
|
417
|
+
ci: classifyCi(contexts.map((context) => toCheck(context))),
|
|
418
|
+
draft: node.isDraft,
|
|
419
|
+
mergeability: mergeabilities[node.mergeable]
|
|
987
420
|
};
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
var method = methods[i];
|
|
1003
|
-
var original = obj[method];
|
|
1004
|
-
obj[method] = function retryWrapper(original2) {
|
|
1005
|
-
var op = exports.operation(options);
|
|
1006
|
-
var args = Array.prototype.slice.call(arguments, 1);
|
|
1007
|
-
var callback = args.pop();
|
|
1008
|
-
args.push(function(err) {
|
|
1009
|
-
if (op.retry(err)) {
|
|
1010
|
-
return;
|
|
1011
|
-
}
|
|
1012
|
-
if (err) {
|
|
1013
|
-
arguments[0] = op.mainError();
|
|
1014
|
-
}
|
|
1015
|
-
callback.apply(this, arguments);
|
|
1016
|
-
});
|
|
1017
|
-
op.attempt(function() {
|
|
1018
|
-
original2.apply(obj, args);
|
|
1019
|
-
});
|
|
1020
|
-
}.bind(obj, original);
|
|
1021
|
-
obj[method].options = options;
|
|
1022
|
-
}
|
|
421
|
+
}
|
|
422
|
+
function toMembership(node) {
|
|
423
|
+
if (node.stack === null)
|
|
424
|
+
return Option4.some({ _tag: "Standalone" });
|
|
425
|
+
const { entries, id, size } = node.stack;
|
|
426
|
+
const ordered = entries.nodes.toSorted((left, right) => left.position - right.position);
|
|
427
|
+
const members = ordered.flatMap((entry) => Option4.toArray(Result3.getSuccess(parsePullRequestUrl(entry.pullRequest.url))));
|
|
428
|
+
const complete = !entries.pageInfo.hasNextPage && members.length === size;
|
|
429
|
+
return complete && Arr2.isArrayNonEmpty(members) ? Option4.some({ _tag: "Stack", id, members }) : Option4.none();
|
|
430
|
+
}
|
|
431
|
+
function toReport(ref, node, contexts) {
|
|
432
|
+
return {
|
|
433
|
+
membership: toMembership(node),
|
|
434
|
+
snapshot: { ref, state: toState(node, contexts), title: node.title }
|
|
1023
435
|
};
|
|
1024
|
-
}
|
|
436
|
+
}
|
|
437
|
+
function combined(outcomes) {
|
|
438
|
+
const failures = outcomes.flatMap((outcome) => Option4.toArray(outcome.failure));
|
|
439
|
+
const everyBatchFailed = failures.length === outcomes.length;
|
|
440
|
+
return Option4.match(Option4.filter(Arr2.head(failures), () => everyBatchFailed), {
|
|
441
|
+
onNone: () => Result3.succeed(new Map(outcomes.flatMap((outcome) => outcome.entries))),
|
|
442
|
+
onSome: (diagnostic) => Result3.fail(diagnostic)
|
|
443
|
+
});
|
|
444
|
+
}
|
|
1025
445
|
|
|
1026
|
-
//
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
446
|
+
// src/adapters/github/Token.ts
|
|
447
|
+
import { Config, Context as Context3, Effect as Effect3, Layer as Layer2, Option as Option5, Redacted, Ref } from "effect";
|
|
448
|
+
class Token extends Context3.Service()("opencode-pr-tracker/Token") {
|
|
449
|
+
}
|
|
450
|
+
var variable = (name) => Config.option(Config.redacted(name)).pipe(Config.map(Option5.filter((token) => Redacted.value(token).trim() !== "")));
|
|
451
|
+
var environmentToken = Config.all([variable("GH_TOKEN"), variable("GITHUB_TOKEN")]).pipe(Config.map(([ghToken, githubToken]) => Option5.orElse(ghToken, () => githubToken)));
|
|
452
|
+
var layer2 = Layer2.effect(Token, Effect3.gen(function* () {
|
|
453
|
+
const runner = yield* CommandRunner;
|
|
454
|
+
const cached = yield* Ref.make(Option5.none());
|
|
455
|
+
const fromGh = runner.run("gh", ["auth", "token"], process.cwd()).pipe(Effect3.map((output) => output.trim()), Effect3.filterOrFail((token) => token !== "", () => new GitHubFailure({ diagnostic: "AuthenticationRequired" })), Effect3.catchTags({
|
|
456
|
+
CommandFailed: () => Effect3.fail(new GitHubFailure({ diagnostic: "AuthenticationRequired" })),
|
|
457
|
+
CommandMissing: () => Effect3.fail(new GitHubFailure({ diagnostic: "GitHubCliMissing" }))
|
|
458
|
+
}));
|
|
459
|
+
const fromEnvironment = yield* environmentToken.pipe(Effect3.orElseSucceed(() => Option5.none()));
|
|
460
|
+
const load = Option5.match(fromEnvironment, {
|
|
461
|
+
onNone: () => Effect3.map(fromGh, Redacted.make),
|
|
462
|
+
onSome: Effect3.succeed
|
|
463
|
+
}).pipe(Effect3.tap((token) => Ref.set(cached, Option5.some(token))));
|
|
464
|
+
return Token.of({
|
|
465
|
+
get: Ref.get(cached).pipe(Effect3.flatMap(Option5.match({ onNone: () => load, onSome: Effect3.succeed }))),
|
|
466
|
+
invalidate: Ref.set(cached, Option5.none())
|
|
467
|
+
});
|
|
468
|
+
}));
|
|
469
|
+
|
|
470
|
+
// src/adapters/github/Client.ts
|
|
471
|
+
var endpoint = "https://api.github.com/graphql";
|
|
472
|
+
var GraphQlError = Schema7.Struct({
|
|
473
|
+
path: Schema7.optional(Schema7.Array(Schema7.Union([Schema7.String, Schema7.Number]))),
|
|
474
|
+
type: Schema7.optional(Schema7.String)
|
|
475
|
+
});
|
|
476
|
+
var Envelope = Schema7.Struct({
|
|
477
|
+
data: Schema7.optional(Schema7.NullOr(Schema7.Record(Schema7.String, Schema7.Unknown))),
|
|
478
|
+
errors: Schema7.optional(Schema7.Array(GraphQlError))
|
|
479
|
+
});
|
|
480
|
+
var ContinuationData = Schema7.Struct({
|
|
481
|
+
resource: Schema7.Struct({ statusCheckRollup: Schema7.Struct({ contexts: Contexts }) })
|
|
1041
482
|
});
|
|
483
|
+
var failure = (diagnostic) => new GitHubFailure({ diagnostic });
|
|
1042
484
|
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
485
|
+
class Unauthorized extends Schema7.TaggedError()("Unauthorized", {}) {
|
|
486
|
+
}
|
|
487
|
+
var failed2 = (diagnostic) => ({ _tag: "Failed", diagnostic });
|
|
488
|
+
var decodeEnvelope = Schema7.decodeUnknownEffect(Envelope);
|
|
489
|
+
var makePost = Effect4.fn("makePost")(function* () {
|
|
490
|
+
const http = yield* HttpClient.HttpClient;
|
|
491
|
+
const token = yield* Token;
|
|
492
|
+
const attempt = Effect4.fn("attempt")(function* (query, variables) {
|
|
493
|
+
const bearer = yield* token.get;
|
|
494
|
+
const request = HttpClientRequest.post(endpoint).pipe(HttpClientRequest.bearerToken(Redacted2.value(bearer)), HttpClientRequest.bodyJsonUnsafe({ query, variables }));
|
|
495
|
+
const response = yield* http.execute(request).pipe(Effect4.mapError(() => failure("GitHubUnavailable")));
|
|
496
|
+
if (response.status === 401)
|
|
497
|
+
return yield* new Unauthorized;
|
|
498
|
+
if (response.status < 200 || response.status >= 300)
|
|
499
|
+
return yield* failure("GitHubUnavailable");
|
|
500
|
+
const body = yield* response.json.pipe(Effect4.mapError(() => failure("InvalidResponse")));
|
|
501
|
+
return yield* decodeEnvelope(body).pipe(Effect4.mapError(() => failure("InvalidResponse")));
|
|
502
|
+
});
|
|
503
|
+
return (query, variables) => attempt(query, variables).pipe(Effect4.catchTag("Unauthorized", () => Effect4.andThen(token.invalidate, attempt(query, variables))), Effect4.catchTag("Unauthorized", () => Effect4.fail(failure("AuthenticationRequired"))));
|
|
504
|
+
});
|
|
505
|
+
var allContexts = Effect4.fn("allContexts")(function* (post, node) {
|
|
506
|
+
const collected = [];
|
|
507
|
+
const followed = new Set;
|
|
508
|
+
let page = Option6.map(Option6.fromNullishOr(node.statusCheckRollup), (rollup) => rollup.contexts);
|
|
509
|
+
while (Option6.isSome(page)) {
|
|
510
|
+
collected.push(...page.value.nodes);
|
|
511
|
+
if (!page.value.pageInfo.hasNextPage)
|
|
512
|
+
break;
|
|
513
|
+
const after = page.value.pageInfo.endCursor ?? "";
|
|
514
|
+
if (after === "" || followed.has(after))
|
|
515
|
+
return yield* failure("InvalidResponse");
|
|
516
|
+
followed.add(after);
|
|
517
|
+
const envelope = yield* post(continuation(), { cursor: after, url: node.url });
|
|
518
|
+
if ((envelope.errors ?? []).length > 0)
|
|
519
|
+
return yield* failure("InvalidResponse");
|
|
520
|
+
const data = yield* Schema7.decodeUnknownEffect(ContinuationData)(envelope.data).pipe(Effect4.mapError(() => failure("InvalidResponse")));
|
|
521
|
+
page = Option6.some(data.resource.statusCheckRollup.contexts);
|
|
522
|
+
}
|
|
523
|
+
return collected;
|
|
524
|
+
});
|
|
525
|
+
var inaccessible = new Set(["NOT_FOUND", "FORBIDDEN"]);
|
|
526
|
+
function aliasFailure(request, key) {
|
|
527
|
+
const diagnostics = (request.envelope.errors ?? []).flatMap((error) => {
|
|
528
|
+
const root = String((error.path ?? [])[0] ?? "");
|
|
529
|
+
if (root === key)
|
|
530
|
+
return [inaccessible.has(error.type ?? "") ? "NotFound" : "InvalidResponse"];
|
|
531
|
+
return request.aliases.has(root) ? [] : ["InvalidResponse"];
|
|
532
|
+
});
|
|
533
|
+
if (diagnostics.length === 0)
|
|
534
|
+
return Option6.none();
|
|
535
|
+
return Option6.some(diagnostics.includes("InvalidResponse") ? "InvalidResponse" : "NotFound");
|
|
536
|
+
}
|
|
537
|
+
var itemResult = Effect4.fn("itemResult")(function* (request, ref, key) {
|
|
538
|
+
const { envelope, post } = request;
|
|
539
|
+
const reported = aliasFailure(request, key);
|
|
540
|
+
const raw = Option6.fromNullishOr((envelope.data ?? {})[key]);
|
|
541
|
+
if (Option6.isSome(reported))
|
|
542
|
+
return failed2(reported.value);
|
|
543
|
+
if (Option6.isNone(raw))
|
|
544
|
+
return failed2("NotFound");
|
|
545
|
+
const node = Schema7.decodeUnknownOption(PullRequestNode)(raw.value);
|
|
546
|
+
if (Option6.isNone(node))
|
|
547
|
+
return failed2("InvalidResponse");
|
|
548
|
+
const contexts = yield* Effect4.result(allContexts(post, node.value));
|
|
549
|
+
if (Result4.isFailure(contexts))
|
|
550
|
+
return failed2(contexts.failure.diagnostic);
|
|
551
|
+
return {
|
|
552
|
+
_tag: "Reported",
|
|
553
|
+
report: toReport(ref, node.value, contexts.success)
|
|
1048
554
|
};
|
|
1049
|
-
if (!processOk(process2)) {
|
|
1050
|
-
module.exports = function() {
|
|
1051
|
-
return function() {};
|
|
1052
|
-
};
|
|
1053
|
-
} else {
|
|
1054
|
-
assert = __require("assert");
|
|
1055
|
-
signals = require_signals();
|
|
1056
|
-
isWin = /^win/i.test(process2.platform);
|
|
1057
|
-
EE = __require("events");
|
|
1058
|
-
if (typeof EE !== "function") {
|
|
1059
|
-
EE = EE.EventEmitter;
|
|
1060
|
-
}
|
|
1061
|
-
if (process2.__signal_exit_emitter__) {
|
|
1062
|
-
emitter = process2.__signal_exit_emitter__;
|
|
1063
|
-
} else {
|
|
1064
|
-
emitter = process2.__signal_exit_emitter__ = new EE;
|
|
1065
|
-
emitter.count = 0;
|
|
1066
|
-
emitter.emitted = {};
|
|
1067
|
-
}
|
|
1068
|
-
if (!emitter.infinite) {
|
|
1069
|
-
emitter.setMaxListeners(Infinity);
|
|
1070
|
-
emitter.infinite = true;
|
|
1071
|
-
}
|
|
1072
|
-
module.exports = function(cb, opts) {
|
|
1073
|
-
if (!processOk(global.process)) {
|
|
1074
|
-
return function() {};
|
|
1075
|
-
}
|
|
1076
|
-
assert.equal(typeof cb, "function", "a callback must be provided for exit handler");
|
|
1077
|
-
if (loaded === false) {
|
|
1078
|
-
load();
|
|
1079
|
-
}
|
|
1080
|
-
var ev = "exit";
|
|
1081
|
-
if (opts && opts.alwaysLast) {
|
|
1082
|
-
ev = "afterexit";
|
|
1083
|
-
}
|
|
1084
|
-
var remove = function() {
|
|
1085
|
-
emitter.removeListener(ev, cb);
|
|
1086
|
-
if (emitter.listeners("exit").length === 0 && emitter.listeners("afterexit").length === 0) {
|
|
1087
|
-
unload();
|
|
1088
|
-
}
|
|
1089
|
-
};
|
|
1090
|
-
emitter.on(ev, cb);
|
|
1091
|
-
return remove;
|
|
1092
|
-
};
|
|
1093
|
-
unload = function unload2() {
|
|
1094
|
-
if (!loaded || !processOk(global.process)) {
|
|
1095
|
-
return;
|
|
1096
|
-
}
|
|
1097
|
-
loaded = false;
|
|
1098
|
-
signals.forEach(function(sig) {
|
|
1099
|
-
try {
|
|
1100
|
-
process2.removeListener(sig, sigListeners[sig]);
|
|
1101
|
-
} catch (er) {}
|
|
1102
|
-
});
|
|
1103
|
-
process2.emit = originalProcessEmit;
|
|
1104
|
-
process2.reallyExit = originalProcessReallyExit;
|
|
1105
|
-
emitter.count -= 1;
|
|
1106
|
-
};
|
|
1107
|
-
module.exports.unload = unload;
|
|
1108
|
-
emit = function emit2(event, code, signal) {
|
|
1109
|
-
if (emitter.emitted[event]) {
|
|
1110
|
-
return;
|
|
1111
|
-
}
|
|
1112
|
-
emitter.emitted[event] = true;
|
|
1113
|
-
emitter.emit(event, code, signal);
|
|
1114
|
-
};
|
|
1115
|
-
sigListeners = {};
|
|
1116
|
-
signals.forEach(function(sig) {
|
|
1117
|
-
sigListeners[sig] = function listener() {
|
|
1118
|
-
if (!processOk(global.process)) {
|
|
1119
|
-
return;
|
|
1120
|
-
}
|
|
1121
|
-
var listeners = process2.listeners(sig);
|
|
1122
|
-
if (listeners.length === emitter.count) {
|
|
1123
|
-
unload();
|
|
1124
|
-
emit("exit", null, sig);
|
|
1125
|
-
emit("afterexit", null, sig);
|
|
1126
|
-
if (isWin && sig === "SIGHUP") {
|
|
1127
|
-
sig = "SIGINT";
|
|
1128
|
-
}
|
|
1129
|
-
process2.kill(process2.pid, sig);
|
|
1130
|
-
}
|
|
1131
|
-
};
|
|
1132
|
-
});
|
|
1133
|
-
module.exports.signals = function() {
|
|
1134
|
-
return signals;
|
|
1135
|
-
};
|
|
1136
|
-
loaded = false;
|
|
1137
|
-
load = function load2() {
|
|
1138
|
-
if (loaded || !processOk(global.process)) {
|
|
1139
|
-
return;
|
|
1140
|
-
}
|
|
1141
|
-
loaded = true;
|
|
1142
|
-
emitter.count += 1;
|
|
1143
|
-
signals = signals.filter(function(sig) {
|
|
1144
|
-
try {
|
|
1145
|
-
process2.on(sig, sigListeners[sig]);
|
|
1146
|
-
return true;
|
|
1147
|
-
} catch (er) {
|
|
1148
|
-
return false;
|
|
1149
|
-
}
|
|
1150
|
-
});
|
|
1151
|
-
process2.emit = processEmit;
|
|
1152
|
-
process2.reallyExit = processReallyExit;
|
|
1153
|
-
};
|
|
1154
|
-
module.exports.load = load;
|
|
1155
|
-
originalProcessReallyExit = process2.reallyExit;
|
|
1156
|
-
processReallyExit = function processReallyExit2(code) {
|
|
1157
|
-
if (!processOk(global.process)) {
|
|
1158
|
-
return;
|
|
1159
|
-
}
|
|
1160
|
-
process2.exitCode = code || 0;
|
|
1161
|
-
emit("exit", process2.exitCode, null);
|
|
1162
|
-
emit("afterexit", process2.exitCode, null);
|
|
1163
|
-
originalProcessReallyExit.call(process2, process2.exitCode);
|
|
1164
|
-
};
|
|
1165
|
-
originalProcessEmit = process2.emit;
|
|
1166
|
-
processEmit = function processEmit2(ev, arg) {
|
|
1167
|
-
if (ev === "exit" && processOk(global.process)) {
|
|
1168
|
-
if (arg !== undefined) {
|
|
1169
|
-
process2.exitCode = arg;
|
|
1170
|
-
}
|
|
1171
|
-
var ret = originalProcessEmit.apply(this, arguments);
|
|
1172
|
-
emit("exit", process2.exitCode, null);
|
|
1173
|
-
emit("afterexit", process2.exitCode, null);
|
|
1174
|
-
return ret;
|
|
1175
|
-
} else {
|
|
1176
|
-
return originalProcessEmit.apply(this, arguments);
|
|
1177
|
-
}
|
|
1178
|
-
};
|
|
1179
|
-
}
|
|
1180
|
-
var assert;
|
|
1181
|
-
var signals;
|
|
1182
|
-
var isWin;
|
|
1183
|
-
var EE;
|
|
1184
|
-
var emitter;
|
|
1185
|
-
var unload;
|
|
1186
|
-
var emit;
|
|
1187
|
-
var sigListeners;
|
|
1188
|
-
var loaded;
|
|
1189
|
-
var load;
|
|
1190
|
-
var originalProcessReallyExit;
|
|
1191
|
-
var processReallyExit;
|
|
1192
|
-
var originalProcessEmit;
|
|
1193
|
-
var processEmit;
|
|
1194
555
|
});
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
return callback(err);
|
|
1205
|
-
}
|
|
1206
|
-
callback(null, stat.mtime, cachedPrecision);
|
|
1207
|
-
});
|
|
1208
|
-
}
|
|
1209
|
-
const mtime = new Date(Math.ceil(Date.now() / 1000) * 1000 + 5);
|
|
1210
|
-
fs.utimes(file, mtime, mtime, (err) => {
|
|
1211
|
-
if (err) {
|
|
1212
|
-
return callback(err);
|
|
1213
|
-
}
|
|
1214
|
-
fs.stat(file, (err2, stat) => {
|
|
1215
|
-
if (err2) {
|
|
1216
|
-
return callback(err2);
|
|
1217
|
-
}
|
|
1218
|
-
const precision = stat.mtime.getTime() % 1000 === 0 ? "s" : "ms";
|
|
1219
|
-
Object.defineProperty(fs, cacheSymbol, { value: precision });
|
|
1220
|
-
callback(null, stat.mtime, precision);
|
|
1221
|
-
});
|
|
1222
|
-
});
|
|
1223
|
-
}
|
|
1224
|
-
function getMtime(precision) {
|
|
1225
|
-
let now = Date.now();
|
|
1226
|
-
if (precision === "s") {
|
|
1227
|
-
now = Math.ceil(now / 1000) * 1000;
|
|
1228
|
-
}
|
|
1229
|
-
return new Date(now);
|
|
1230
|
-
}
|
|
1231
|
-
exports.probe = probe;
|
|
1232
|
-
exports.getMtime = getMtime;
|
|
556
|
+
var fetchBatch = Effect4.fn("fetchBatch")(function* (post, refs) {
|
|
557
|
+
const variables = Object.fromEntries(refs.map((ref, index) => [alias(index), ref.url]));
|
|
558
|
+
const envelope = yield* post(batch(refs.length), variables);
|
|
559
|
+
if (Option6.isNone(Option6.fromNullishOr(envelope.data)))
|
|
560
|
+
return yield* failure("GitHubUnavailable");
|
|
561
|
+
const results = yield* Effect4.forEach(refs, (ref, index) => itemResult({ aliases: new Set(Object.keys(variables)), envelope, post }, ref, alias(index)), {
|
|
562
|
+
concurrency: 4
|
|
563
|
+
});
|
|
564
|
+
return Arr3.zip(refs.map((ref) => ref.url), results);
|
|
1233
565
|
});
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
const lockfilePath = getLockFile(file, options);
|
|
1254
|
-
options.fs.mkdir(lockfilePath, (err) => {
|
|
1255
|
-
if (!err) {
|
|
1256
|
-
return mtimePrecision.probe(lockfilePath, options.fs, (err2, mtime, mtimePrecision2) => {
|
|
1257
|
-
if (err2) {
|
|
1258
|
-
options.fs.rmdir(lockfilePath, () => {});
|
|
1259
|
-
return callback(err2);
|
|
1260
|
-
}
|
|
1261
|
-
callback(null, mtime, mtimePrecision2);
|
|
1262
|
-
});
|
|
1263
|
-
}
|
|
1264
|
-
if (err.code !== "EEXIST") {
|
|
1265
|
-
return callback(err);
|
|
1266
|
-
}
|
|
1267
|
-
if (options.stale <= 0) {
|
|
1268
|
-
return callback(Object.assign(new Error("Lock file is already being held"), { code: "ELOCKED", file }));
|
|
1269
|
-
}
|
|
1270
|
-
options.fs.stat(lockfilePath, (err2, stat) => {
|
|
1271
|
-
if (err2) {
|
|
1272
|
-
if (err2.code === "ENOENT") {
|
|
1273
|
-
return acquireLock(file, { ...options, stale: 0 }, callback);
|
|
1274
|
-
}
|
|
1275
|
-
return callback(err2);
|
|
1276
|
-
}
|
|
1277
|
-
if (!isLockStale(stat, options)) {
|
|
1278
|
-
return callback(Object.assign(new Error("Lock file is already being held"), { code: "ELOCKED", file }));
|
|
1279
|
-
}
|
|
1280
|
-
removeLock(file, options, (err3) => {
|
|
1281
|
-
if (err3) {
|
|
1282
|
-
return callback(err3);
|
|
1283
|
-
}
|
|
1284
|
-
acquireLock(file, { ...options, stale: 0 }, callback);
|
|
1285
|
-
});
|
|
1286
|
-
});
|
|
1287
|
-
});
|
|
1288
|
-
}
|
|
1289
|
-
function isLockStale(stat, options) {
|
|
1290
|
-
return stat.mtime.getTime() < Date.now() - options.stale;
|
|
1291
|
-
}
|
|
1292
|
-
function removeLock(file, options, callback) {
|
|
1293
|
-
options.fs.rmdir(getLockFile(file, options), (err) => {
|
|
1294
|
-
if (err && err.code !== "ENOENT") {
|
|
1295
|
-
return callback(err);
|
|
1296
|
-
}
|
|
1297
|
-
callback();
|
|
1298
|
-
});
|
|
1299
|
-
}
|
|
1300
|
-
function updateLock(file, options) {
|
|
1301
|
-
const lock2 = locks[file];
|
|
1302
|
-
if (lock2.updateTimeout) {
|
|
1303
|
-
return;
|
|
1304
|
-
}
|
|
1305
|
-
lock2.updateDelay = lock2.updateDelay || options.update;
|
|
1306
|
-
lock2.updateTimeout = setTimeout(() => {
|
|
1307
|
-
lock2.updateTimeout = null;
|
|
1308
|
-
options.fs.stat(lock2.lockfilePath, (err, stat) => {
|
|
1309
|
-
const isOverThreshold = lock2.lastUpdate + options.stale < Date.now();
|
|
1310
|
-
if (err) {
|
|
1311
|
-
if (err.code === "ENOENT" || isOverThreshold) {
|
|
1312
|
-
return setLockAsCompromised(file, lock2, Object.assign(err, { code: "ECOMPROMISED" }));
|
|
1313
|
-
}
|
|
1314
|
-
lock2.updateDelay = 1000;
|
|
1315
|
-
return updateLock(file, options);
|
|
1316
|
-
}
|
|
1317
|
-
const isMtimeOurs = lock2.mtime.getTime() === stat.mtime.getTime();
|
|
1318
|
-
if (!isMtimeOurs) {
|
|
1319
|
-
return setLockAsCompromised(file, lock2, Object.assign(new Error("Unable to update lock within the stale threshold"), { code: "ECOMPROMISED" }));
|
|
1320
|
-
}
|
|
1321
|
-
const mtime = mtimePrecision.getMtime(lock2.mtimePrecision);
|
|
1322
|
-
options.fs.utimes(lock2.lockfilePath, mtime, mtime, (err2) => {
|
|
1323
|
-
const isOverThreshold2 = lock2.lastUpdate + options.stale < Date.now();
|
|
1324
|
-
if (lock2.released) {
|
|
1325
|
-
return;
|
|
1326
|
-
}
|
|
1327
|
-
if (err2) {
|
|
1328
|
-
if (err2.code === "ENOENT" || isOverThreshold2) {
|
|
1329
|
-
return setLockAsCompromised(file, lock2, Object.assign(err2, { code: "ECOMPROMISED" }));
|
|
1330
|
-
}
|
|
1331
|
-
lock2.updateDelay = 1000;
|
|
1332
|
-
return updateLock(file, options);
|
|
1333
|
-
}
|
|
1334
|
-
lock2.mtime = mtime;
|
|
1335
|
-
lock2.lastUpdate = Date.now();
|
|
1336
|
-
lock2.updateDelay = null;
|
|
1337
|
-
updateLock(file, options);
|
|
1338
|
-
});
|
|
1339
|
-
});
|
|
1340
|
-
}, lock2.updateDelay);
|
|
1341
|
-
if (lock2.updateTimeout.unref) {
|
|
1342
|
-
lock2.updateTimeout.unref();
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
function setLockAsCompromised(file, lock2, err) {
|
|
1346
|
-
lock2.released = true;
|
|
1347
|
-
if (lock2.updateTimeout) {
|
|
1348
|
-
clearTimeout(lock2.updateTimeout);
|
|
1349
|
-
}
|
|
1350
|
-
if (locks[file] === lock2) {
|
|
1351
|
-
delete locks[file];
|
|
1352
|
-
}
|
|
1353
|
-
lock2.options.onCompromised(err);
|
|
1354
|
-
}
|
|
1355
|
-
function lock(file, options, callback) {
|
|
1356
|
-
options = {
|
|
1357
|
-
stale: 1e4,
|
|
1358
|
-
update: null,
|
|
1359
|
-
realpath: true,
|
|
1360
|
-
retries: 0,
|
|
1361
|
-
fs,
|
|
1362
|
-
onCompromised: (err) => {
|
|
1363
|
-
throw err;
|
|
1364
|
-
},
|
|
1365
|
-
...options
|
|
1366
|
-
};
|
|
1367
|
-
options.retries = options.retries || 0;
|
|
1368
|
-
options.retries = typeof options.retries === "number" ? { retries: options.retries } : options.retries;
|
|
1369
|
-
options.stale = Math.max(options.stale || 0, 2000);
|
|
1370
|
-
options.update = options.update == null ? options.stale / 2 : options.update || 0;
|
|
1371
|
-
options.update = Math.max(Math.min(options.update, options.stale / 2), 1000);
|
|
1372
|
-
resolveCanonicalPath(file, options, (err, file2) => {
|
|
1373
|
-
if (err) {
|
|
1374
|
-
return callback(err);
|
|
1375
|
-
}
|
|
1376
|
-
const operation = retry.operation(options.retries);
|
|
1377
|
-
operation.attempt(() => {
|
|
1378
|
-
acquireLock(file2, options, (err2, mtime, mtimePrecision2) => {
|
|
1379
|
-
if (operation.retry(err2)) {
|
|
1380
|
-
return;
|
|
1381
|
-
}
|
|
1382
|
-
if (err2) {
|
|
1383
|
-
return callback(operation.mainError());
|
|
1384
|
-
}
|
|
1385
|
-
const lock2 = locks[file2] = {
|
|
1386
|
-
lockfilePath: getLockFile(file2, options),
|
|
1387
|
-
mtime,
|
|
1388
|
-
mtimePrecision: mtimePrecision2,
|
|
1389
|
-
options,
|
|
1390
|
-
lastUpdate: Date.now()
|
|
1391
|
-
};
|
|
1392
|
-
updateLock(file2, options);
|
|
1393
|
-
callback(null, (releasedCallback) => {
|
|
1394
|
-
if (lock2.released) {
|
|
1395
|
-
return releasedCallback && releasedCallback(Object.assign(new Error("Lock is already released"), { code: "ERELEASED" }));
|
|
1396
|
-
}
|
|
1397
|
-
unlock(file2, { ...options, realpath: false }, releasedCallback);
|
|
1398
|
-
});
|
|
1399
|
-
});
|
|
1400
|
-
});
|
|
1401
|
-
});
|
|
1402
|
-
}
|
|
1403
|
-
function unlock(file, options, callback) {
|
|
1404
|
-
options = {
|
|
1405
|
-
fs,
|
|
1406
|
-
realpath: true,
|
|
1407
|
-
...options
|
|
1408
|
-
};
|
|
1409
|
-
resolveCanonicalPath(file, options, (err, file2) => {
|
|
1410
|
-
if (err) {
|
|
1411
|
-
return callback(err);
|
|
1412
|
-
}
|
|
1413
|
-
const lock2 = locks[file2];
|
|
1414
|
-
if (!lock2) {
|
|
1415
|
-
return callback(Object.assign(new Error("Lock is not acquired/owned by you"), { code: "ENOTACQUIRED" }));
|
|
1416
|
-
}
|
|
1417
|
-
lock2.updateTimeout && clearTimeout(lock2.updateTimeout);
|
|
1418
|
-
lock2.released = true;
|
|
1419
|
-
delete locks[file2];
|
|
1420
|
-
removeLock(file2, options, callback);
|
|
1421
|
-
});
|
|
1422
|
-
}
|
|
1423
|
-
function check(file, options, callback) {
|
|
1424
|
-
options = {
|
|
1425
|
-
stale: 1e4,
|
|
1426
|
-
realpath: true,
|
|
1427
|
-
fs,
|
|
1428
|
-
...options
|
|
1429
|
-
};
|
|
1430
|
-
options.stale = Math.max(options.stale || 0, 2000);
|
|
1431
|
-
resolveCanonicalPath(file, options, (err, file2) => {
|
|
1432
|
-
if (err) {
|
|
1433
|
-
return callback(err);
|
|
1434
|
-
}
|
|
1435
|
-
options.fs.stat(getLockFile(file2, options), (err2, stat) => {
|
|
1436
|
-
if (err2) {
|
|
1437
|
-
return err2.code === "ENOENT" ? callback(null, false) : callback(err2);
|
|
1438
|
-
}
|
|
1439
|
-
return callback(null, !isLockStale(stat, options));
|
|
1440
|
-
});
|
|
1441
|
-
});
|
|
1442
|
-
}
|
|
1443
|
-
function getLocks() {
|
|
1444
|
-
return locks;
|
|
1445
|
-
}
|
|
1446
|
-
onExit(() => {
|
|
1447
|
-
for (const file in locks) {
|
|
1448
|
-
const options = locks[file].options;
|
|
1449
|
-
try {
|
|
1450
|
-
options.fs.rmdirSync(getLockFile(file, options));
|
|
1451
|
-
} catch (e) {}
|
|
1452
|
-
}
|
|
566
|
+
var outcomeOf = (post, refs) => Effect4.gen(function* () {
|
|
567
|
+
const result = yield* Effect4.result(fetchBatch(post, refs));
|
|
568
|
+
if (Result4.isSuccess(result))
|
|
569
|
+
return { entries: result.success, failure: Option6.none() };
|
|
570
|
+
const { diagnostic } = result.failure;
|
|
571
|
+
return {
|
|
572
|
+
entries: refs.map((ref) => [ref.url, { _tag: "Failed", diagnostic }]),
|
|
573
|
+
failure: Option6.some(diagnostic)
|
|
574
|
+
};
|
|
575
|
+
});
|
|
576
|
+
var layer3 = Layer3.effect(GitHub, Effect4.gen(function* () {
|
|
577
|
+
const post = yield* makePost();
|
|
578
|
+
const runner = yield* CommandRunner;
|
|
579
|
+
return GitHub.of({
|
|
580
|
+
fetch: (refs) => {
|
|
581
|
+
const batches = Arr3.chunksOf(Arr3.dedupeWith(refs, (left, right) => left.url === right.url), maximumBatch);
|
|
582
|
+
return Effect4.forEach(batches, (refsInBatch) => outcomeOf(post, refsInBatch)).pipe(Effect4.flatMap((outcomes) => Effect4.fromResult(combined(outcomes))), Effect4.mapError(failure));
|
|
583
|
+
},
|
|
584
|
+
pullRequestInRepository: (directory, number) => resolveInRepository(directory, number).pipe(Effect4.provideService(CommandRunner, runner))
|
|
1453
585
|
});
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
586
|
+
}));
|
|
587
|
+
var live = layer3.pipe(Layer3.provide(layer2), Layer3.provide([layer, FetchHttpClient.layer]));
|
|
588
|
+
|
|
589
|
+
// src/adapters/Storage.ts
|
|
590
|
+
import { Array as Arr5, Effect as Effect5, Layer as Layer4, Option as Option8, Result as Result6, Schema as Schema10 } from "effect";
|
|
591
|
+
|
|
592
|
+
// src/domain/Tracking.ts
|
|
593
|
+
import { Array as Arr4, Option as Option7, Result as Result5, Schema as Schema8 } from "effect";
|
|
594
|
+
var maximumAttachments = 20;
|
|
595
|
+
|
|
596
|
+
class Attachment extends Schema8.Class("Attachment")({
|
|
597
|
+
attachedAt: Schema8.Int,
|
|
598
|
+
ref: PullRequestRef
|
|
599
|
+
}) {
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
class AttachmentLimitReached extends Schema8.TaggedError()("AttachmentLimitReached", { limit: Schema8.Int, requested: Schema8.Int }) {
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
class AmbiguousPullRequestNumber extends Schema8.TaggedError()("AmbiguousPullRequestNumber", { matches: Schema8.Array(PullRequestRef), number: Schema8.Int }) {
|
|
606
|
+
}
|
|
607
|
+
function isMember(stack, attachment) {
|
|
608
|
+
return stack.some((ref) => samePullRequest(ref, attachment.ref));
|
|
609
|
+
}
|
|
610
|
+
function attach(tracking, stack, now) {
|
|
611
|
+
const members = Arr4.dedupeWith(stack, samePullRequest);
|
|
612
|
+
const others = tracking.filter((attachment) => !isMember(members, attachment));
|
|
613
|
+
const requested = others.length + members.length;
|
|
614
|
+
if (requested > maximumAttachments) {
|
|
615
|
+
return Result5.fail(new AttachmentLimitReached({ limit: maximumAttachments, requested }));
|
|
616
|
+
}
|
|
617
|
+
const position = tracking.findIndex((attachment) => isMember(members, attachment));
|
|
618
|
+
const insertAt = position === -1 ? others.length : position;
|
|
619
|
+
const placed = members.map((ref) => Option7.getOrElse(Arr4.findFirst(tracking, (attachment) => samePullRequest(attachment.ref, ref)), () => new Attachment({ attachedAt: now, ref })));
|
|
620
|
+
const next = [...others.slice(0, insertAt), ...placed, ...others.slice(insertAt)];
|
|
621
|
+
const changed = next.length !== tracking.length || next.some((attachment, index) => attachment !== tracking[index]);
|
|
622
|
+
return Result5.succeed({ changed, tracking: next });
|
|
623
|
+
}
|
|
624
|
+
function detach(tracking, ref) {
|
|
625
|
+
const next = tracking.filter((attachment) => !samePullRequest(attachment.ref, ref));
|
|
626
|
+
const removed = next.length === tracking.length ? Option7.none() : Option7.some(ref);
|
|
627
|
+
return { removed, tracking: next };
|
|
628
|
+
}
|
|
629
|
+
function detachNumber(tracking, number) {
|
|
630
|
+
const matches = tracking.filter((attachment) => attachment.ref.number === number);
|
|
631
|
+
if (matches.length > 1) {
|
|
632
|
+
const refs = matches.map((attachment) => attachment.ref);
|
|
633
|
+
return Result5.fail(new AmbiguousPullRequestNumber({ matches: refs, number }));
|
|
634
|
+
}
|
|
635
|
+
return Result5.succeed(Option7.match(Arr4.head(matches), {
|
|
636
|
+
onNone: () => ({ removed: Option7.none(), tracking }),
|
|
637
|
+
onSome: (attachment) => detach(tracking, attachment.ref)
|
|
638
|
+
}));
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// src/ports/TrackingRepository.ts
|
|
642
|
+
import { Context as Context4, Schema as Schema9 } from "effect";
|
|
643
|
+
|
|
644
|
+
class StoredStateInvalid extends Schema9.TaggedError()("StoredStateInvalid", {
|
|
645
|
+
sessionID: Schema9.String
|
|
646
|
+
}) {
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
class TrackingRepository extends Context4.Service()("opencode-pr-tracker/TrackingRepository") {
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// src/adapters/Storage.ts
|
|
653
|
+
var Stored = Schema10.Struct({
|
|
654
|
+
pullRequests: Schema10.Array(Schema10.Struct({ attachedAt: Schema10.Int, url: Schema10.String })).check(Schema10.isMaxLength(maximumAttachments)),
|
|
655
|
+
version: Schema10.Literal(1)
|
|
1458
656
|
});
|
|
657
|
+
var keyOf = (sessionID) => `session/${sessionID}`;
|
|
658
|
+
function toTracking(stored) {
|
|
659
|
+
const attachments = stored.pullRequests.map((entry) => Result6.map(parsePullRequestUrl(entry.url), (ref) => new Attachment({ attachedAt: entry.attachedAt, ref })));
|
|
660
|
+
const valid = attachments.flatMap((attachment) => Option8.toArray(Result6.getSuccess(attachment)));
|
|
661
|
+
const unique = Arr5.dedupeWith(valid, (left, right) => left.ref.url === right.ref.url);
|
|
662
|
+
return unique.length === stored.pullRequests.length ? Option8.some(unique) : Option8.none();
|
|
663
|
+
}
|
|
664
|
+
function toStored(tracking) {
|
|
665
|
+
return {
|
|
666
|
+
pullRequests: tracking.map((attachment) => ({
|
|
667
|
+
attachedAt: attachment.attachedAt,
|
|
668
|
+
url: attachment.ref.url
|
|
669
|
+
})),
|
|
670
|
+
version: 1
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
function layer4(storage) {
|
|
674
|
+
return Layer4.succeed(TrackingRepository, TrackingRepository.of({
|
|
675
|
+
load: (sessionID) => storage.get(keyOf(sessionID)).pipe(Effect5.flatMap((value) => Option8.match(Option8.fromNullishOr(value), {
|
|
676
|
+
onNone: () => Effect5.succeed([]),
|
|
677
|
+
onSome: (json) => Schema10.decodeUnknownOption(Stored)(json).pipe(Option8.flatMap(toTracking), Option8.match({
|
|
678
|
+
onNone: () => Effect5.fail(new StoredStateInvalid({ sessionID })),
|
|
679
|
+
onSome: (tracking) => Effect5.succeed(tracking)
|
|
680
|
+
}))
|
|
681
|
+
}))),
|
|
682
|
+
remove: (sessionID) => storage.remove(keyOf(sessionID)),
|
|
683
|
+
save: (sessionID, tracking) => storage.set(keyOf(sessionID), toStored(tracking))
|
|
684
|
+
}));
|
|
685
|
+
}
|
|
1459
686
|
|
|
1460
|
-
//
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
reject(err);
|
|
1485
|
-
} else {
|
|
1486
|
-
resolve(result);
|
|
1487
|
-
}
|
|
1488
|
-
});
|
|
1489
|
-
method(...args);
|
|
687
|
+
// src/application/Monitor.ts
|
|
688
|
+
import { Context as Context6, Effect as Effect9, Layer as Layer6, Option as Option13, PubSub, Ref as Ref2, Result as Result7, Stream } from "effect";
|
|
689
|
+
|
|
690
|
+
// src/application/FetchQueue.ts
|
|
691
|
+
import { Deferred, Effect as Effect6, Exit, Option as Option9 } from "effect";
|
|
692
|
+
|
|
693
|
+
class FetchQueue {
|
|
694
|
+
running = false;
|
|
695
|
+
queued = Option9.none();
|
|
696
|
+
fetchNow;
|
|
697
|
+
scope;
|
|
698
|
+
constructor(fetchNow, scope) {
|
|
699
|
+
this.fetchNow = fetchNow;
|
|
700
|
+
this.scope = scope;
|
|
701
|
+
}
|
|
702
|
+
fetch(refs) {
|
|
703
|
+
return Effect6.suspend(() => {
|
|
704
|
+
if (refs.length === 0)
|
|
705
|
+
return Effect6.void;
|
|
706
|
+
if (this.running)
|
|
707
|
+
return Effect6.asVoid(Deferred.await(this.join(refs)));
|
|
708
|
+
this.running = true;
|
|
709
|
+
const done = Deferred.makeUnsafe();
|
|
710
|
+
return Effect6.andThen(Effect6.forkIn(this.drain(refs, done), this.scope), Deferred.await(done));
|
|
1490
711
|
});
|
|
1491
712
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
713
|
+
join(refs) {
|
|
714
|
+
const next = Option9.getOrElse(this.queued, () => ({
|
|
715
|
+
done: Deferred.makeUnsafe(),
|
|
716
|
+
refs: new Map
|
|
717
|
+
}));
|
|
718
|
+
for (const ref of refs)
|
|
719
|
+
next.refs.set(ref.url, ref);
|
|
720
|
+
this.queued = Option9.some(next);
|
|
721
|
+
return next.done;
|
|
722
|
+
}
|
|
723
|
+
drain(refs, done) {
|
|
724
|
+
return this.fetchNow(refs).pipe(Effect6.andThen(Deferred.succeed(done, true)), Effect6.andThen(Effect6.suspend(() => this.next())), Effect6.onExit((exit) => Exit.isSuccess(exit) ? Effect6.void : Effect6.suspend(() => this.reset(done, exit.cause))));
|
|
725
|
+
}
|
|
726
|
+
next() {
|
|
727
|
+
return Option9.match(this.queued, {
|
|
728
|
+
onNone: () => Effect6.sync(() => {
|
|
729
|
+
this.running = false;
|
|
730
|
+
}),
|
|
731
|
+
onSome: (queued) => {
|
|
732
|
+
this.queued = Option9.none();
|
|
733
|
+
return this.drain([...queued.refs.values()], queued.done);
|
|
1503
734
|
}
|
|
1504
|
-
|
|
1505
|
-
};
|
|
735
|
+
});
|
|
1506
736
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
737
|
+
reset(done, cause) {
|
|
738
|
+
const waiting = [done, ...Option9.toArray(Option9.map(this.queued, (queued) => queued.done))];
|
|
739
|
+
this.running = false;
|
|
740
|
+
this.queued = Option9.none();
|
|
741
|
+
return Effect6.forEach(waiting, (deferred) => Deferred.failCause(deferred, cause), {
|
|
742
|
+
discard: true
|
|
743
|
+
});
|
|
1514
744
|
}
|
|
1515
|
-
|
|
1516
|
-
toPromise,
|
|
1517
|
-
toSync,
|
|
1518
|
-
toSyncOptions
|
|
1519
|
-
};
|
|
1520
|
-
});
|
|
745
|
+
}
|
|
1521
746
|
|
|
1522
|
-
//
|
|
1523
|
-
|
|
1524
|
-
var lockfile = require_lockfile();
|
|
1525
|
-
var { toPromise, toSync, toSyncOptions } = require_adapter();
|
|
1526
|
-
async function lock(file, options) {
|
|
1527
|
-
const release = await toPromise(lockfile.lock)(file, options);
|
|
1528
|
-
return toPromise(release);
|
|
1529
|
-
}
|
|
1530
|
-
function lockSync(file, options) {
|
|
1531
|
-
const release = toSync(lockfile.lock)(file, toSyncOptions(options));
|
|
1532
|
-
return toSync(release);
|
|
1533
|
-
}
|
|
1534
|
-
function unlock(file, options) {
|
|
1535
|
-
return toPromise(lockfile.unlock)(file, options);
|
|
1536
|
-
}
|
|
1537
|
-
function unlockSync(file, options) {
|
|
1538
|
-
return toSync(lockfile.unlock)(file, toSyncOptions(options));
|
|
1539
|
-
}
|
|
1540
|
-
function check(file, options) {
|
|
1541
|
-
return toPromise(lockfile.check)(file, options);
|
|
1542
|
-
}
|
|
1543
|
-
function checkSync(file, options) {
|
|
1544
|
-
return toSync(lockfile.check)(file, toSyncOptions(options));
|
|
1545
|
-
}
|
|
1546
|
-
module.exports = lock;
|
|
1547
|
-
module.exports.lock = lock;
|
|
1548
|
-
module.exports.unlock = unlock;
|
|
1549
|
-
module.exports.lockSync = lockSync;
|
|
1550
|
-
module.exports.unlockSync = unlockSync;
|
|
1551
|
-
module.exports.check = check;
|
|
1552
|
-
module.exports.checkSync = checkSync;
|
|
1553
|
-
});
|
|
747
|
+
// src/application/Known.ts
|
|
748
|
+
import { Duration as Duration3, Option as Option11 } from "effect";
|
|
1554
749
|
|
|
1555
|
-
// src/
|
|
1556
|
-
import {
|
|
750
|
+
// src/domain/RefreshPolicy.ts
|
|
751
|
+
import { Duration as Duration2, Option as Option10 } from "effect";
|
|
752
|
+
var refreshInterval = Duration2.seconds(15);
|
|
753
|
+
function nextRefresh(status) {
|
|
754
|
+
const merged = (status._tag === "Fresh" || status._tag === "Stale") && status.snapshot.state._tag === "Merged";
|
|
755
|
+
return merged ? Option10.none() : Option10.some(refreshInterval);
|
|
756
|
+
}
|
|
1557
757
|
|
|
1558
|
-
// src/
|
|
1559
|
-
|
|
758
|
+
// src/application/Known.ts
|
|
759
|
+
var unknown = { dueAt: Option11.some(0), membership: Option11.none(), status: pending };
|
|
760
|
+
function afterRefresh(previous, result, now) {
|
|
761
|
+
const status = result._tag === "Reported" ? succeeded(result.report.snapshot) : failed(previous.status, result.diagnostic, now);
|
|
762
|
+
const membership = result._tag === "Reported" ? result.report.membership : previous.membership;
|
|
763
|
+
return {
|
|
764
|
+
dueAt: Option11.map(nextRefresh(status), (delay) => now + Duration3.toMillis(delay)),
|
|
765
|
+
membership,
|
|
766
|
+
status
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
var isDue = (known, now, ref) => Option11.match((known.get(ref.url) ?? unknown).dueAt, {
|
|
770
|
+
onNone: () => false,
|
|
771
|
+
onSome: (at) => at <= now
|
|
772
|
+
});
|
|
773
|
+
function withoutUnattached(entries, before, attached) {
|
|
774
|
+
return new Map([...entries].filter(([url, entry]) => attached.has(url) || entry !== before.get(url)));
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// src/application/Time.ts
|
|
778
|
+
import { Clock, Effect as Effect7 } from "effect";
|
|
779
|
+
var currentMillis = Effect7.map(Clock.currentTimeMillis, Math.floor);
|
|
1560
780
|
|
|
1561
|
-
// src/
|
|
1562
|
-
|
|
1563
|
-
|
|
781
|
+
// src/application/Tracker.ts
|
|
782
|
+
import { Array as Arr6, Context as Context5, Effect as Effect8, Layer as Layer5, Option as Option12, Schema as Schema11, Semaphore } from "effect";
|
|
783
|
+
class PullRequestUnavailable extends Schema11.TaggedError()("PullRequestUnavailable", { diagnostic: Diagnostic, url: Schema11.String }) {
|
|
1564
784
|
}
|
|
1565
785
|
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
ok: false,
|
|
1570
|
-
error: {
|
|
1571
|
-
tag: "InvalidPullRequestUrl",
|
|
1572
|
-
message: expectedPullRequestUrl
|
|
1573
|
-
}
|
|
1574
|
-
};
|
|
1575
|
-
var segmentPattern = /^[A-Za-z0-9._-]+$/;
|
|
1576
|
-
var schemeLessPrefix = "github.com/";
|
|
1577
|
-
function parsePullRequestUrl(input) {
|
|
1578
|
-
if (/\s/.test(input))
|
|
1579
|
-
return invalidPullRequestUrl;
|
|
1580
|
-
if (input.includes("\\"))
|
|
1581
|
-
return invalidPullRequestUrl;
|
|
1582
|
-
const candidate = input.slice(0, schemeLessPrefix.length).toLowerCase() === schemeLessPrefix ? `https://${input}` : input;
|
|
1583
|
-
if (!candidate.startsWith("https://"))
|
|
1584
|
-
return invalidPullRequestUrl;
|
|
1585
|
-
const authorityEnd = candidate.indexOf("/", "https://".length);
|
|
1586
|
-
if (authorityEnd === -1)
|
|
1587
|
-
return invalidPullRequestUrl;
|
|
1588
|
-
if (candidate.slice("https://".length, authorityEnd).toLowerCase() !== "github.com") {
|
|
1589
|
-
return invalidPullRequestUrl;
|
|
1590
|
-
}
|
|
1591
|
-
const rawPath = candidate.slice(authorityEnd).split(/[?#]/, 1).join("");
|
|
1592
|
-
for (const segment of rawPath.split("/")) {
|
|
1593
|
-
let decoded;
|
|
1594
|
-
try {
|
|
1595
|
-
decoded = decodeURIComponent(segment);
|
|
1596
|
-
} catch {
|
|
1597
|
-
return invalidPullRequestUrl;
|
|
1598
|
-
}
|
|
1599
|
-
if (decoded === "." || decoded === "..")
|
|
1600
|
-
return invalidPullRequestUrl;
|
|
1601
|
-
}
|
|
1602
|
-
let parsed;
|
|
1603
|
-
try {
|
|
1604
|
-
parsed = new URL(candidate);
|
|
1605
|
-
} catch {
|
|
1606
|
-
return invalidPullRequestUrl;
|
|
1607
|
-
}
|
|
1608
|
-
if (parsed.protocol !== "https:" || parsed.hostname !== "github.com" || parsed.username !== "" || parsed.password !== "" || parsed.port !== "" || parsed.search !== "" || parsed.hash !== "") {
|
|
1609
|
-
return invalidPullRequestUrl;
|
|
1610
|
-
}
|
|
1611
|
-
const segments = parsed.pathname.split("/");
|
|
1612
|
-
if (segments.length !== 5 || segments[0] !== "" || segments[3] !== "pull") {
|
|
1613
|
-
return invalidPullRequestUrl;
|
|
1614
|
-
}
|
|
1615
|
-
const rawOwner = segments[1];
|
|
1616
|
-
const rawRepository = segments[2];
|
|
1617
|
-
const numberText = segments[4];
|
|
1618
|
-
if (rawOwner === undefined || rawRepository === undefined || numberText === undefined || !segmentPattern.test(rawOwner) || !segmentPattern.test(rawRepository) || !/^\d+$/.test(numberText)) {
|
|
1619
|
-
return invalidPullRequestUrl;
|
|
1620
|
-
}
|
|
1621
|
-
const number = Number(numberText);
|
|
1622
|
-
if (!Number.isSafeInteger(number) || number <= 0)
|
|
1623
|
-
return invalidPullRequestUrl;
|
|
1624
|
-
const owner = rawOwner.toLowerCase();
|
|
1625
|
-
const repository = rawRepository.toLowerCase();
|
|
1626
|
-
const url = `https://github.com/${owner}/${repository}/pull/${number}`;
|
|
1627
|
-
const value = { url, owner, repository, number };
|
|
1628
|
-
return { ok: true, value };
|
|
1629
|
-
}
|
|
1630
|
-
function formatPullRequestRef(pullRequest) {
|
|
1631
|
-
return `${pullRequest.owner}/${pullRequest.repository}#${pullRequest.number}`;
|
|
786
|
+
class StackIncomplete extends Schema11.TaggedError()("StackIncomplete", {
|
|
787
|
+
url: Schema11.String
|
|
788
|
+
}) {
|
|
1632
789
|
}
|
|
1633
790
|
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
var
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
if (!isRecord(value) || value.tag !== "ProcessExecutionFailed" || value.code !== null && typeof value.code !== "string" && typeof value.code !== "number" || typeof value.stderr !== "string" || typeof value.stdout !== "string" || !("cause" in value)) {
|
|
1669
|
-
return;
|
|
1670
|
-
}
|
|
791
|
+
class Tracker extends Context5.Service()("opencode-pr-tracker/Tracker") {
|
|
792
|
+
}
|
|
793
|
+
function discovered(ref, result) {
|
|
794
|
+
if (result._tag === "Failed")
|
|
795
|
+
return Effect8.fail(new PullRequestUnavailable({ diagnostic: result.diagnostic, url: ref.url }));
|
|
796
|
+
const { report } = result;
|
|
797
|
+
return Option12.match(report.membership, {
|
|
798
|
+
onNone: () => Effect8.fail(new StackIncomplete({ url: ref.url })),
|
|
799
|
+
onSome: (membership) => Effect8.succeed({
|
|
800
|
+
report,
|
|
801
|
+
stack: membership._tag === "Stack" ? membership.members : Arr6.of(ref)
|
|
802
|
+
})
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
function sessionLocks() {
|
|
806
|
+
const locks = new Map;
|
|
807
|
+
return (sessionID) => (effect) => Effect8.suspend(() => {
|
|
808
|
+
const lock = locks.get(sessionID) ?? Semaphore.makeUnsafe(1);
|
|
809
|
+
locks.set(sessionID, lock);
|
|
810
|
+
return lock.withPermit(effect);
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
function resolve({ github }, input, directory) {
|
|
814
|
+
return input._tag === "Reference" ? Effect8.succeed(input.ref) : github.pullRequestInRepository(directory, input.number);
|
|
815
|
+
}
|
|
816
|
+
var attachTo = Effect8.fn("Tracker.attach")(function* (services, sessionID, target) {
|
|
817
|
+
const ref = yield* resolve(services, target.input, target.directory);
|
|
818
|
+
const reports = yield* services.github.fetch([ref]);
|
|
819
|
+
const missing = { _tag: "Failed", diagnostic: "NotFound" };
|
|
820
|
+
const { report, stack } = yield* discovered(ref, reports.get(ref.url) ?? missing);
|
|
821
|
+
const current = yield* services.repository.load(sessionID);
|
|
822
|
+
const change = yield* Effect8.fromResult(attach(current, stack, yield* currentMillis));
|
|
823
|
+
if (change.changed)
|
|
824
|
+
yield* services.repository.save(sessionID, change.tracking);
|
|
1671
825
|
return {
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
826
|
+
changed: change.changed,
|
|
827
|
+
ref,
|
|
828
|
+
report,
|
|
829
|
+
stackSize: stack.length,
|
|
830
|
+
tracking: change.tracking
|
|
1677
831
|
};
|
|
832
|
+
});
|
|
833
|
+
var detachFrom = Effect8.fn("Tracker.detach")(function* ({ repository }, sessionID, input) {
|
|
834
|
+
const current = yield* repository.load(sessionID);
|
|
835
|
+
const removal = input._tag === "Reference" ? detach(current, input.ref) : yield* Effect8.fromResult(detachNumber(current, input.number));
|
|
836
|
+
if (Option12.isSome(removal.removed))
|
|
837
|
+
yield* repository.save(sessionID, removal.tracking);
|
|
838
|
+
return removal;
|
|
839
|
+
});
|
|
840
|
+
var layer5 = Layer5.effect(Tracker, Effect8.gen(function* () {
|
|
841
|
+
const services = { github: yield* GitHub, repository: yield* TrackingRepository };
|
|
842
|
+
const locked = sessionLocks();
|
|
843
|
+
return Tracker.of({
|
|
844
|
+
attach: (sessionID, input, directory) => locked(sessionID)(attachTo(services, sessionID, { directory, input })),
|
|
845
|
+
detach: (sessionID, input) => locked(sessionID)(detachFrom(services, sessionID, input)),
|
|
846
|
+
forget: (sessionID) => locked(sessionID)(services.repository.remove(sessionID)),
|
|
847
|
+
list: (sessionID) => services.repository.load(sessionID)
|
|
848
|
+
});
|
|
849
|
+
}));
|
|
850
|
+
|
|
851
|
+
// src/application/Monitor.ts
|
|
852
|
+
class Monitor extends Context6.Service()("opencode-pr-tracker/Monitor") {
|
|
1678
853
|
}
|
|
1679
|
-
function
|
|
1680
|
-
|
|
1681
|
-
}
|
|
1682
|
-
function parseDate(input) {
|
|
1683
|
-
if (typeof input !== "string")
|
|
1684
|
-
return;
|
|
1685
|
-
const match = /^(\d{4})-(\d{2})-(\d{2})[Tt](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(?:[Zz]|[+-](\d{2}):(\d{2}))$/.exec(input);
|
|
1686
|
-
if (match === null)
|
|
1687
|
-
return;
|
|
1688
|
-
const year = Number(match[1]);
|
|
1689
|
-
const month = Number(match[2]);
|
|
1690
|
-
const day = Number(match[3]);
|
|
1691
|
-
const hour = Number(match[4]);
|
|
1692
|
-
const minute = Number(match[5]);
|
|
1693
|
-
const second = Number(match[6]);
|
|
1694
|
-
const offsetHour = Number(match[8] ?? 0);
|
|
1695
|
-
const offsetMinute = Number(match[9] ?? 0);
|
|
1696
|
-
const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
1697
|
-
const daysInMonth = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
|
|
1698
|
-
if (daysInMonth === undefined || day < 1 || day > daysInMonth || hour > 23 || minute > 59 || second > 59 || offsetHour > 23 || offsetMinute > 59) {
|
|
1699
|
-
return;
|
|
1700
|
-
}
|
|
1701
|
-
if (Number.isNaN(new Date(input).valueOf()))
|
|
1702
|
-
return;
|
|
1703
|
-
const epochSeconds = new Date(input.replace(/\.\d+/, "")).valueOf() / 1000;
|
|
1704
|
-
if (!Number.isInteger(epochSeconds))
|
|
1705
|
-
return;
|
|
854
|
+
function entryOf(known, attachment) {
|
|
855
|
+
const current = known.get(attachment.ref.url) ?? unknown;
|
|
1706
856
|
return {
|
|
1707
|
-
|
|
1708
|
-
|
|
857
|
+
attachedAt: attachment.attachedAt,
|
|
858
|
+
membership: current.membership,
|
|
859
|
+
ref: attachment.ref,
|
|
860
|
+
status: current.status
|
|
1709
861
|
};
|
|
1710
862
|
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
return 1;
|
|
1723
|
-
return 0;
|
|
1724
|
-
}
|
|
1725
|
-
function parseStatusContextState(input) {
|
|
1726
|
-
switch (input) {
|
|
1727
|
-
case "EXPECTED":
|
|
1728
|
-
case "PENDING":
|
|
1729
|
-
case "SUCCESS":
|
|
1730
|
-
case "ERROR":
|
|
1731
|
-
case "FAILURE":
|
|
1732
|
-
return input;
|
|
1733
|
-
default:
|
|
1734
|
-
return;
|
|
1735
|
-
}
|
|
863
|
+
var urlsOf = (tracking) => tracking.map((attachment) => attachment.ref.url);
|
|
864
|
+
function remember(cache, results) {
|
|
865
|
+
return Effect9.gen(function* () {
|
|
866
|
+
const now = yield* currentMillis;
|
|
867
|
+
yield* Ref2.update(cache.known, (current) => {
|
|
868
|
+
const next = new Map(current);
|
|
869
|
+
for (const [url, result] of results)
|
|
870
|
+
next.set(url, afterRefresh(current.get(url) ?? unknown, result, now));
|
|
871
|
+
return next;
|
|
872
|
+
});
|
|
873
|
+
});
|
|
1736
874
|
}
|
|
1737
|
-
function
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
875
|
+
function update(cache, refs) {
|
|
876
|
+
return Effect9.gen(function* () {
|
|
877
|
+
const outcome = yield* Effect9.result(cache.github.fetch(refs));
|
|
878
|
+
const results = Result7.isSuccess(outcome) ? outcome.success : new Map(refs.map((ref) => [ref.url, { _tag: "Failed", diagnostic: outcome.failure.diagnostic }]));
|
|
879
|
+
yield* remember(cache, results);
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
function viewOf(state, sessionID) {
|
|
883
|
+
return Effect9.gen(function* () {
|
|
884
|
+
const tracking = yield* state.tracker.list(sessionID);
|
|
885
|
+
const current = yield* Ref2.get(state.known);
|
|
886
|
+
return { entries: tracking.map((attachment) => entryOf(current, attachment)), sessionID };
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
function publish(state, sessionID) {
|
|
890
|
+
return viewOf(state, sessionID).pipe(Effect9.flatMap((view) => PubSub.publish(state.published, view)), Effect9.ignore);
|
|
1749
891
|
}
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
return input;
|
|
1763
|
-
default:
|
|
892
|
+
var use = (state, sessionID) => Ref2.update(state.working, (sessions) => new Set(sessions).add(sessionID));
|
|
893
|
+
function poll(state) {
|
|
894
|
+
return Effect9.gen(function* () {
|
|
895
|
+
const sessions = [...yield* Ref2.get(state.working)];
|
|
896
|
+
const known = yield* Ref2.get(state.known);
|
|
897
|
+
const trackings = yield* Effect9.forEach(sessions, (sessionID) => state.tracker.list(sessionID).pipe(Effect9.orElseSucceed(() => [])));
|
|
898
|
+
const attached = new Map(trackings.flat().map((attachment) => [attachment.ref.url, attachment.ref]));
|
|
899
|
+
const now = yield* currentMillis;
|
|
900
|
+
const current = yield* Ref2.get(state.known);
|
|
901
|
+
const due = [...attached.values()].filter((ref) => isDue(current, now, ref));
|
|
902
|
+
yield* Ref2.update(state.known, (entries) => withoutUnattached(entries, known, attached));
|
|
903
|
+
if (due.length === 0)
|
|
1764
904
|
return;
|
|
1765
|
-
|
|
905
|
+
yield* state.fetch(due);
|
|
906
|
+
const dueUrls = new Set(due.map((ref) => ref.url));
|
|
907
|
+
const affected = sessions.filter((_, index) => urlsOf(trackings[index] ?? []).some((url) => dueUrls.has(url)));
|
|
908
|
+
yield* Effect9.forEach(affected, (sessionID) => publish(state, sessionID), {
|
|
909
|
+
discard: true
|
|
910
|
+
});
|
|
911
|
+
});
|
|
1766
912
|
}
|
|
1767
|
-
function
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
const
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
}
|
|
1785
|
-
const suiteId = parseNonBlankString(input.checkSuite.id);
|
|
1786
|
-
const suiteCreatedAt = parseDate(input.checkSuite.createdAt);
|
|
1787
|
-
if (suiteId === undefined || suiteCreatedAt === undefined)
|
|
1788
|
-
return invalidGitHubResponse;
|
|
1789
|
-
let sourceIdentity;
|
|
1790
|
-
if (input.checkSuite.app === null) {
|
|
1791
|
-
sourceIdentity = ["suite", suiteId];
|
|
1792
|
-
} else {
|
|
1793
|
-
if (!isRecord(input.checkSuite.app))
|
|
1794
|
-
return invalidGitHubResponse;
|
|
1795
|
-
const appId = parseNonBlankString(input.checkSuite.app.id);
|
|
1796
|
-
if (appId === undefined)
|
|
1797
|
-
return invalidGitHubResponse;
|
|
1798
|
-
sourceIdentity = ["app", appId];
|
|
1799
|
-
}
|
|
1800
|
-
let workflowRun;
|
|
1801
|
-
if (input.checkSuite.workflowRun === null) {
|
|
1802
|
-
workflowRun = undefined;
|
|
1803
|
-
} else {
|
|
1804
|
-
if (!isRecord(input.checkSuite.workflowRun) || !isRecord(input.checkSuite.workflowRun.workflow)) {
|
|
1805
|
-
return invalidGitHubResponse;
|
|
1806
|
-
}
|
|
1807
|
-
const event = parseNonBlankString(input.checkSuite.workflowRun.event);
|
|
1808
|
-
const workflowId = parseNonBlankString(input.checkSuite.workflowRun.workflow.id);
|
|
1809
|
-
const runNumber = input.checkSuite.workflowRun.runNumber;
|
|
1810
|
-
const runAttempt = input.checkSuite.workflowRun.runAttempt;
|
|
1811
|
-
if (event === undefined || workflowId === undefined || !Number.isInteger(runNumber) || Number(runNumber) <= 0 || !Number.isInteger(runAttempt) || Number(runAttempt) <= 0) {
|
|
1812
|
-
return invalidGitHubResponse;
|
|
1813
|
-
}
|
|
1814
|
-
workflowRun = { event, workflowId, runNumber: Number(runNumber), runAttempt: Number(runAttempt) };
|
|
1815
|
-
}
|
|
1816
|
-
return {
|
|
1817
|
-
ok: true,
|
|
1818
|
-
value: { tag: "CheckRun", id, name, status, conclusion, suiteId, suiteCreatedAt, sourceIdentity, workflowRun }
|
|
913
|
+
function fetchAndShow(state, sessionID, select) {
|
|
914
|
+
return Effect9.gen(function* () {
|
|
915
|
+
yield* use(state, sessionID);
|
|
916
|
+
const tracking = yield* state.tracker.list(sessionID);
|
|
917
|
+
const known = yield* Ref2.get(state.known);
|
|
918
|
+
yield* state.fetch(tracking.map((attachment) => attachment.ref).filter((ref) => select(Option13.fromNullishOr(known.get(ref.url)))));
|
|
919
|
+
const view = yield* viewOf(state, sessionID);
|
|
920
|
+
yield* PubSub.publish(state.published, view);
|
|
921
|
+
return view;
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
var refreshable = (known) => Option13.isSome(Option13.getOrElse(known, () => unknown).dueAt);
|
|
925
|
+
var notYetKnown = (known) => Option13.isNone(known);
|
|
926
|
+
var layer6 = Layer6.effect(Monitor, Effect9.gen(function* () {
|
|
927
|
+
const cache = {
|
|
928
|
+
github: yield* GitHub,
|
|
929
|
+
known: yield* Ref2.make(new Map)
|
|
1819
930
|
};
|
|
931
|
+
const queue = new FetchQueue((refs) => update(cache, refs), yield* Effect9.scope);
|
|
932
|
+
const state = {
|
|
933
|
+
fetch: (refs) => queue.fetch(refs),
|
|
934
|
+
github: cache.github,
|
|
935
|
+
known: cache.known,
|
|
936
|
+
published: yield* PubSub.unbounded(),
|
|
937
|
+
tracker: yield* Tracker,
|
|
938
|
+
working: yield* Ref2.make(new Set)
|
|
939
|
+
};
|
|
940
|
+
return Monitor.of({
|
|
941
|
+
changes: Stream.fromPubSub(state.published),
|
|
942
|
+
forget: (sessionID) => Ref2.update(state.working, (sessions) => new Set([...sessions].filter((id) => id !== sessionID))),
|
|
943
|
+
poll: poll(state),
|
|
944
|
+
attached: (sessionID, ref, report) => Effect9.andThen(remember(cache, new Map([[ref.url, { _tag: "Reported", report }]])), fetchAndShow(state, sessionID, notYetKnown)),
|
|
945
|
+
refresh: (sessionID) => fetchAndShow(state, sessionID, refreshable),
|
|
946
|
+
show: (sessionID) => fetchAndShow(state, sessionID, notYetKnown),
|
|
947
|
+
view: (sessionID) => Effect9.andThen(use(state, sessionID), viewOf(state, sessionID))
|
|
948
|
+
});
|
|
949
|
+
}));
|
|
950
|
+
|
|
951
|
+
// src/rpc.ts
|
|
952
|
+
import { Rpc } from "@opencode/plugin/rpc";
|
|
953
|
+
import { Schema as Schema13 } from "effect";
|
|
954
|
+
|
|
955
|
+
// src/domain/StackLayout.ts
|
|
956
|
+
import { Array as Arr7, Option as Option14, Schema as Schema12 } from "effect";
|
|
957
|
+
var Membership = Schema12.Union([
|
|
958
|
+
Schema12.TaggedStruct("Standalone", {}),
|
|
959
|
+
Schema12.TaggedStruct("Stack", {
|
|
960
|
+
id: Schema12.String,
|
|
961
|
+
members: Schema12.NonEmptyArray(PullRequestRef)
|
|
962
|
+
})
|
|
963
|
+
]);
|
|
964
|
+
var stackOf = (entry) => Option14.filter(entry.membership, (membership) => membership._tag === "Stack");
|
|
965
|
+
var urlsOf2 = (stack) => stack.members.map((member) => member.url);
|
|
966
|
+
function reportsById(entries) {
|
|
967
|
+
const byId = new Map;
|
|
968
|
+
for (const [index, entry] of entries.entries()) {
|
|
969
|
+
for (const stack of Option14.toArray(stackOf(entry))) {
|
|
970
|
+
byId.set(stack.id, [...byId.get(stack.id) ?? [], { index, stack, url: entry.ref.url }]);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return byId;
|
|
1820
974
|
}
|
|
1821
|
-
function
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
if (!Array.isArray(nodes) || nodes.length > maximumCheckContextsPerPage || nodes.length > Number(input.totalCount)) {
|
|
1827
|
-
return invalidGitHubResponse;
|
|
1828
|
-
}
|
|
1829
|
-
const contexts = [];
|
|
1830
|
-
const ids = new Set;
|
|
1831
|
-
for (const node of nodes) {
|
|
1832
|
-
if (!isRecord(node))
|
|
1833
|
-
return invalidGitHubResponse;
|
|
1834
|
-
let parsed;
|
|
1835
|
-
switch (node.__typename) {
|
|
1836
|
-
case "StatusContext":
|
|
1837
|
-
if (checkRunOnlyFields.some((field) => (field in node)))
|
|
1838
|
-
return invalidGitHubResponse;
|
|
1839
|
-
parsed = parseStatusContext(node);
|
|
1840
|
-
break;
|
|
1841
|
-
case "CheckRun":
|
|
1842
|
-
if (statusContextOnlyFields.some((field) => (field in node)))
|
|
1843
|
-
return invalidGitHubResponse;
|
|
1844
|
-
parsed = parseCheckRun(node);
|
|
1845
|
-
break;
|
|
1846
|
-
default:
|
|
1847
|
-
return invalidGitHubResponse;
|
|
975
|
+
function claims(byId) {
|
|
976
|
+
const claimed = new Map;
|
|
977
|
+
for (const [id, reports] of byId) {
|
|
978
|
+
for (const url of reports.flatMap((report) => urlsOf2(report.stack))) {
|
|
979
|
+
claimed.set(url, new Set([...claimed.get(url) ?? [], id]));
|
|
1848
980
|
}
|
|
1849
|
-
if (!parsed.ok || ids.has(parsed.value.id))
|
|
1850
|
-
return invalidGitHubResponse;
|
|
1851
|
-
ids.add(parsed.value.id);
|
|
1852
|
-
contexts.push(parsed.value);
|
|
1853
981
|
}
|
|
1854
|
-
|
|
1855
|
-
if (input.pageInfo.hasNextPage && nextCursor === undefined)
|
|
1856
|
-
return invalidGitHubResponse;
|
|
1857
|
-
if (nextCursor !== undefined && contexts.length === 0)
|
|
1858
|
-
return invalidGitHubResponse;
|
|
1859
|
-
return {
|
|
1860
|
-
ok: true,
|
|
1861
|
-
value: {
|
|
1862
|
-
contexts,
|
|
1863
|
-
totalCount: Number(input.totalCount),
|
|
1864
|
-
...nextCursor === undefined ? {} : { nextCursor }
|
|
1865
|
-
}
|
|
1866
|
-
};
|
|
982
|
+
return claimed;
|
|
1867
983
|
}
|
|
1868
|
-
function
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
default:
|
|
1879
|
-
return casesHandled(state);
|
|
1880
|
-
}
|
|
984
|
+
function agrees(reports, entries, claimed) {
|
|
985
|
+
const members = Option14.match(Arr7.head(reports), {
|
|
986
|
+
onNone: () => [],
|
|
987
|
+
onSome: (report) => urlsOf2(report.stack)
|
|
988
|
+
});
|
|
989
|
+
const listed = new Set(members);
|
|
990
|
+
const attachedMembers = entries.filter((entry) => listed.has(entry.ref.url));
|
|
991
|
+
return listed.size === members.length && reports.every((report) => urlsOf2(report.stack).join(`
|
|
992
|
+
`) === members.join(`
|
|
993
|
+
`)) && members.every((url) => (claimed.get(url) ?? new Set).size === 1) && attachedMembers.length === reports.length;
|
|
1881
994
|
}
|
|
1882
|
-
function
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
case "STALE":
|
|
1892
|
-
return "failed";
|
|
1893
|
-
case "SUCCESS":
|
|
1894
|
-
return "passed";
|
|
1895
|
-
case "NEUTRAL":
|
|
1896
|
-
case "SKIPPED":
|
|
1897
|
-
case null:
|
|
1898
|
-
return "ignored";
|
|
1899
|
-
default:
|
|
1900
|
-
return casesHandled(checkRun.conclusion);
|
|
1901
|
-
}
|
|
995
|
+
function placements(reports) {
|
|
996
|
+
const placed = reports.map((report) => ({
|
|
997
|
+
index: report.index,
|
|
998
|
+
position: urlsOf2(report.stack).indexOf(report.url),
|
|
999
|
+
size: report.stack.members.length
|
|
1000
|
+
}));
|
|
1001
|
+
const adjacent = Arr7.zipWith(placed, placed.slice(1), (before, after) => after.index === before.index + 1 && after.position > before.position);
|
|
1002
|
+
const ordered = placed.every((member) => member.position >= 0) && adjacent.every(Boolean);
|
|
1003
|
+
return ordered ? Option14.some(placed) : Option14.none();
|
|
1902
1004
|
}
|
|
1903
|
-
function
|
|
1904
|
-
const
|
|
1905
|
-
const
|
|
1906
|
-
const
|
|
1907
|
-
for (const
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
const
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
}
|
|
1918
|
-
continue;
|
|
1919
|
-
}
|
|
1920
|
-
const bucket = classifyCheckRun(context);
|
|
1921
|
-
if (context.workflowRun !== undefined) {
|
|
1922
|
-
const identity2 = JSON.stringify([
|
|
1923
|
-
"workflow",
|
|
1924
|
-
context.sourceIdentity,
|
|
1925
|
-
context.workflowRun.workflowId,
|
|
1926
|
-
context.workflowRun.event,
|
|
1927
|
-
context.name
|
|
1928
|
-
]);
|
|
1929
|
-
const existing2 = workflowChecks.get(identity2);
|
|
1930
|
-
const isNewer = existing2 === undefined || context.workflowRun.runNumber > existing2.runNumber || context.workflowRun.runNumber === existing2.runNumber && context.workflowRun.runAttempt > existing2.runAttempt;
|
|
1931
|
-
if (isNewer) {
|
|
1932
|
-
workflowChecks.set(identity2, {
|
|
1933
|
-
runNumber: context.workflowRun.runNumber,
|
|
1934
|
-
runAttempt: context.workflowRun.runAttempt,
|
|
1935
|
-
buckets: [bucket]
|
|
1005
|
+
function consistentStacks(entries) {
|
|
1006
|
+
const byId = reportsById(entries);
|
|
1007
|
+
const claimed = claims(byId);
|
|
1008
|
+
const places = new Map;
|
|
1009
|
+
for (const reports of byId.values()) {
|
|
1010
|
+
const placed = agrees(reports, entries, claimed) ? placements(reports) : Option14.none();
|
|
1011
|
+
for (const group of Option14.toArray(placed)) {
|
|
1012
|
+
for (const [step, current] of group.entries()) {
|
|
1013
|
+
places.set(current.index, {
|
|
1014
|
+
attached: group.length,
|
|
1015
|
+
current,
|
|
1016
|
+
first: step === 0,
|
|
1017
|
+
last: step === group.length - 1,
|
|
1018
|
+
previous: Arr7.get(group, step - 1)
|
|
1936
1019
|
});
|
|
1937
|
-
} else if (context.workflowRun.runNumber === existing2.runNumber && context.workflowRun.runAttempt === existing2.runAttempt) {
|
|
1938
|
-
existing2.buckets.push(bucket);
|
|
1939
1020
|
}
|
|
1940
|
-
continue;
|
|
1941
1021
|
}
|
|
1942
|
-
const identity = JSON.stringify(["check", context.sourceIdentity, context.name]);
|
|
1943
|
-
const existing = nonWorkflowChecks.get(identity);
|
|
1944
|
-
const ordering = existing === undefined ? 1 : compareTimestamps(context.suiteCreatedAt, existing.suiteCreatedAt);
|
|
1945
|
-
if (ordering > 0) {
|
|
1946
|
-
nonWorkflowChecks.set(identity, { suiteCreatedAt: context.suiteCreatedAt, buckets: [bucket] });
|
|
1947
|
-
} else if (ordering === 0 && existing !== undefined) {
|
|
1948
|
-
existing.buckets.push(bucket);
|
|
1949
|
-
}
|
|
1950
|
-
}
|
|
1951
|
-
const buckets = new Set;
|
|
1952
|
-
for (const selection of [...statusContexts.values(), ...workflowChecks.values(), ...nonWorkflowChecks.values()]) {
|
|
1953
|
-
for (const bucket of selection.buckets)
|
|
1954
|
-
buckets.add(bucket);
|
|
1955
1022
|
}
|
|
1956
|
-
|
|
1957
|
-
return "failed";
|
|
1958
|
-
if (buckets.has("pending"))
|
|
1959
|
-
return "pending";
|
|
1960
|
-
if (buckets.has("passed"))
|
|
1961
|
-
return "passed";
|
|
1962
|
-
return "none";
|
|
1023
|
+
return places;
|
|
1963
1024
|
}
|
|
1964
|
-
function
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1025
|
+
function marker(place) {
|
|
1026
|
+
const { current } = place;
|
|
1027
|
+
if (place.attached === 1 && current.size > 1)
|
|
1028
|
+
return "middle";
|
|
1029
|
+
if (place.first && current.position === 0)
|
|
1030
|
+
return "first";
|
|
1031
|
+
if (place.last && current.position === current.size - 1)
|
|
1032
|
+
return "last";
|
|
1033
|
+
return "middle";
|
|
1970
1034
|
}
|
|
1971
|
-
function
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
switch (input) {
|
|
1976
|
-
case "MERGEABLE":
|
|
1977
|
-
return { ok: true, value: "mergeable" };
|
|
1978
|
-
case "CONFLICTING":
|
|
1979
|
-
return { ok: true, value: "conflicting" };
|
|
1980
|
-
case "UNKNOWN":
|
|
1981
|
-
return { ok: true, value: "unknown" };
|
|
1982
|
-
default:
|
|
1983
|
-
return invalidGitHubResponse;
|
|
1984
|
-
}
|
|
1035
|
+
function connector(place) {
|
|
1036
|
+
if (!place.last)
|
|
1037
|
+
return "continues";
|
|
1038
|
+
return place.current.position < place.current.size - 1 ? "open" : "none";
|
|
1985
1039
|
}
|
|
1986
|
-
function
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
default:
|
|
1999
|
-
return invalidGitHubResponse;
|
|
2000
|
-
}
|
|
1040
|
+
function stackRows(entry, place) {
|
|
1041
|
+
const skipped = Option14.match(place.previous, {
|
|
1042
|
+
onNone: () => 0,
|
|
1043
|
+
onSome: (before) => place.current.position - before.position - 1
|
|
1044
|
+
});
|
|
1045
|
+
const row = {
|
|
1046
|
+
_tag: "PullRequest",
|
|
1047
|
+
connector: connector(place),
|
|
1048
|
+
entry,
|
|
1049
|
+
marker: marker(place)
|
|
1050
|
+
};
|
|
1051
|
+
return skipped > 0 ? [{ _tag: "Gap", count: skipped }, row] : [row];
|
|
2001
1052
|
}
|
|
2002
|
-
function
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
}
|
|
2009
|
-
}
|
|
2010
|
-
return { ok: true, value: input.length > 0 };
|
|
1053
|
+
function layout(entries) {
|
|
1054
|
+
const places = consistentStacks(entries);
|
|
1055
|
+
return entries.flatMap((entry, index) => Option14.match(Option14.fromNullishOr(places.get(index)), {
|
|
1056
|
+
onNone: () => [{ _tag: "PullRequest", connector: "none", entry, marker: "bullet" }],
|
|
1057
|
+
onSome: (place) => stackRows(entry, place)
|
|
1058
|
+
}));
|
|
2011
1059
|
}
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
function parseUpdatePolicy(input) {
|
|
2059
|
-
if (input === null)
|
|
2060
|
-
return { ok: true, value: { strict: false, incomplete: false } };
|
|
2061
|
-
if (!isRecord(input))
|
|
2062
|
-
return invalidGitHubResponse;
|
|
2063
|
-
const refUpdateHasRequiredChecks = parseRefUpdateRule(input.refUpdateRule);
|
|
2064
|
-
if (!refUpdateHasRequiredChecks.ok)
|
|
2065
|
-
return refUpdateHasRequiredChecks;
|
|
2066
|
-
let branchProtectionIsStrict = false;
|
|
2067
|
-
if (input.branchProtectionRule !== null) {
|
|
2068
|
-
if (!isRecord(input.branchProtectionRule) || typeof input.branchProtectionRule.requiresStatusChecks !== "boolean" || typeof input.branchProtectionRule.requiresStrictStatusChecks !== "boolean") {
|
|
2069
|
-
return invalidGitHubResponse;
|
|
1060
|
+
|
|
1061
|
+
// src/rpc.ts
|
|
1062
|
+
var Layout = Schema13.Literals(["default", "compact"]);
|
|
1063
|
+
var EntryView = Schema13.Struct({
|
|
1064
|
+
attachedAt: Schema13.Int,
|
|
1065
|
+
membership: Schema13.NullOr(Membership),
|
|
1066
|
+
ref: PullRequestRef,
|
|
1067
|
+
status: Status
|
|
1068
|
+
});
|
|
1069
|
+
var View = Schema13.Struct({
|
|
1070
|
+
entries: Schema13.Array(EntryView),
|
|
1071
|
+
layout: Layout,
|
|
1072
|
+
sessionID: Schema13.String
|
|
1073
|
+
});
|
|
1074
|
+
var Session = Schema13.Struct({ sessionID: Schema13.String });
|
|
1075
|
+
var Target = Schema13.Struct({ sessionID: Schema13.String, target: Schema13.String });
|
|
1076
|
+
var Changed = Schema13.Struct({ message: Schema13.String, view: View });
|
|
1077
|
+
var Rejected = Schema13.Struct({ message: Schema13.String });
|
|
1078
|
+
function portable(schema) {
|
|
1079
|
+
return { "~standard": Schema13.toStandardSchemaV1(Schema13.toEncoded(schema))["~standard"] };
|
|
1080
|
+
}
|
|
1081
|
+
var PullRequestTracker = Rpc.define({
|
|
1082
|
+
events: {
|
|
1083
|
+
updated: { schema: portable(View) }
|
|
1084
|
+
},
|
|
1085
|
+
id: "opencode-pr-tracker",
|
|
1086
|
+
methods: {
|
|
1087
|
+
attach: {
|
|
1088
|
+
errors: { rejected: portable(Rejected) },
|
|
1089
|
+
input: portable(Target),
|
|
1090
|
+
output: portable(Changed)
|
|
1091
|
+
},
|
|
1092
|
+
detach: {
|
|
1093
|
+
errors: { rejected: portable(Rejected) },
|
|
1094
|
+
input: portable(Target),
|
|
1095
|
+
output: portable(Changed)
|
|
1096
|
+
},
|
|
1097
|
+
list: {
|
|
1098
|
+
errors: { rejected: portable(Rejected) },
|
|
1099
|
+
input: portable(Session),
|
|
1100
|
+
output: portable(View)
|
|
1101
|
+
},
|
|
1102
|
+
refresh: {
|
|
1103
|
+
errors: { rejected: portable(Rejected) },
|
|
1104
|
+
input: portable(Session),
|
|
1105
|
+
output: portable(View)
|
|
2070
1106
|
}
|
|
2071
|
-
branchProtectionIsStrict = input.branchProtectionRule.requiresStatusChecks && input.branchProtectionRule.requiresStrictStatusChecks;
|
|
2072
1107
|
}
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
// src/server/Requests.ts
|
|
1111
|
+
import { Effect as Effect10, Option as Option16 } from "effect";
|
|
1112
|
+
|
|
1113
|
+
// src/messages.ts
|
|
1114
|
+
import { Match as Match2, Option as Option15 } from "effect";
|
|
1115
|
+
var diagnosticMessages = {
|
|
1116
|
+
AuthenticationRequired: "GitHub needs you to sign in: run `gh auth login`, or set GH_TOKEN.",
|
|
1117
|
+
GitHubCliMissing: "Install the GitHub CLI (`gh`), or set GH_TOKEN.",
|
|
1118
|
+
GitHubUnavailable: "GitHub is not responding right now. Try again shortly.",
|
|
1119
|
+
InvalidResponse: "GitHub returned a response the tracker could not read.",
|
|
1120
|
+
NotFound: "The pull request does not exist, or your GitHub account cannot see it."
|
|
1121
|
+
};
|
|
1122
|
+
var list = (refs) => {
|
|
1123
|
+
const labels = refs.map((ref) => ref.label);
|
|
1124
|
+
return labels.length <= 2 ? labels.join(" and ") : `${labels.slice(0, -1).join(", ")}, and ${labels.at(-1) ?? ""}`;
|
|
1125
|
+
};
|
|
1126
|
+
function failureMessage(failure) {
|
|
1127
|
+
return Match2.valueTags(failure, {
|
|
1128
|
+
AmbiguousPullRequestNumber: ({ matches, number }) => `#${String(number)} matches ${list(matches)}. Use the pull request URL instead.`,
|
|
1129
|
+
AttachmentLimitReached: ({ limit, requested }) => `A session can track at most ${String(limit)} pull requests; this would make ${String(requested)}.`,
|
|
1130
|
+
GitHubFailure: ({ diagnostic }) => diagnosticMessages[diagnostic],
|
|
1131
|
+
InvalidPullRequestInput: () => "Expected a pull request URL, such as github.com/owner/repository/pull/123, or a pull request number.",
|
|
1132
|
+
PullRequestUnavailable: ({ diagnostic, url }) => `${url}: ${diagnosticMessages[diagnostic]}`,
|
|
1133
|
+
RepositoryUnavailable: () => "This session's directory is not a GitHub repository that `gh` can see. Use the pull request URL instead.",
|
|
1134
|
+
StackIncomplete: ({ url }) => `GitHub returned only part of the Stack of ${url}, so nothing was attached. Try again later.`,
|
|
1135
|
+
StoredStateInvalid: () => "The tracker's saved state for this session cannot be read. It has been left unchanged."
|
|
1136
|
+
});
|
|
2083
1137
|
}
|
|
2084
|
-
function
|
|
2085
|
-
const
|
|
2086
|
-
if (!
|
|
2087
|
-
return
|
|
2088
|
-
|
|
2089
|
-
return { ok: true, value: "none" };
|
|
2090
|
-
const updatePolicy = parseUpdatePolicy(baseRefInput);
|
|
2091
|
-
if (!updatePolicy.ok)
|
|
2092
|
-
return updatePolicy;
|
|
2093
|
-
if (updatePolicy.value.strict)
|
|
2094
|
-
return { ok: true, value: "behind" };
|
|
2095
|
-
return updatePolicy.value.incomplete ? invalidGitHubResponse : { ok: true, value: "none" };
|
|
2096
|
-
}
|
|
2097
|
-
function parsePullRequestMetadata(input, pullRequest) {
|
|
2098
|
-
if (!isRecord(input) || input.__typename !== "PullRequest" || typeof input.title !== "string" || input.title.trim() === "") {
|
|
2099
|
-
return invalidGitHubResponse;
|
|
2100
|
-
}
|
|
2101
|
-
if (input.state !== "OPEN" && input.state !== "CLOSED" && input.state !== "MERGED") {
|
|
2102
|
-
return invalidGitHubResponse;
|
|
2103
|
-
}
|
|
2104
|
-
if (input.mergedAt !== null && typeof input.mergedAt !== "string")
|
|
2105
|
-
return invalidGitHubResponse;
|
|
2106
|
-
if (typeof input.mergedAt === "string" && Number.isNaN(new Date(input.mergedAt).valueOf())) {
|
|
2107
|
-
return invalidGitHubResponse;
|
|
2108
|
-
}
|
|
2109
|
-
if (input.state === "MERGED" && typeof input.mergedAt !== "string")
|
|
2110
|
-
return invalidGitHubResponse;
|
|
2111
|
-
if (input.state !== "MERGED" && input.mergedAt !== null)
|
|
2112
|
-
return invalidGitHubResponse;
|
|
2113
|
-
if (typeof input.url !== "string")
|
|
2114
|
-
return invalidGitHubResponse;
|
|
2115
|
-
const responseUrl = parsePullRequestUrl(input.url);
|
|
2116
|
-
if (!responseUrl.ok || !samePullRequest(responseUrl.value, pullRequest))
|
|
2117
|
-
return invalidGitHubResponse;
|
|
2118
|
-
const mergeability = parseMergeability(input.mergeable);
|
|
2119
|
-
if (!mergeability.ok)
|
|
2120
|
-
return mergeability;
|
|
2121
|
-
return {
|
|
2122
|
-
ok: true,
|
|
2123
|
-
value: {
|
|
2124
|
-
title: input.title,
|
|
2125
|
-
state: input.state,
|
|
2126
|
-
mergeability: mergeability.value,
|
|
2127
|
-
mergeStateStatus: input.mergeStateStatus,
|
|
2128
|
-
baseRef: input.baseRef
|
|
2129
|
-
}
|
|
2130
|
-
};
|
|
1138
|
+
function attachedMessage(attached) {
|
|
1139
|
+
const label = attached.ref.label;
|
|
1140
|
+
if (!attached.changed)
|
|
1141
|
+
return `${label} is already attached.`;
|
|
1142
|
+
return attached.stackSize > 1 ? `Attached ${label} with the rest of its Stack (${String(attached.stackSize)} pull requests).` : `Attached ${label}.`;
|
|
2131
1143
|
}
|
|
2132
|
-
function
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
blocker = parsedBlocker.value;
|
|
2142
|
-
}
|
|
2143
|
-
state = { tag: "Open", ci, mergeability: metadata.mergeability, blocker };
|
|
2144
|
-
break;
|
|
2145
|
-
}
|
|
2146
|
-
case "MERGED":
|
|
2147
|
-
state = { tag: "Merged" };
|
|
2148
|
-
break;
|
|
2149
|
-
case "CLOSED":
|
|
2150
|
-
state = { tag: "Closed" };
|
|
2151
|
-
break;
|
|
2152
|
-
default:
|
|
2153
|
-
return casesHandled(metadata.state);
|
|
2154
|
-
}
|
|
1144
|
+
function detachedMessage(removal, target) {
|
|
1145
|
+
return Option15.match(removal.removed, {
|
|
1146
|
+
onNone: () => `${target} is not attached.`,
|
|
1147
|
+
onSome: (ref) => `Detached ${ref.label}.`
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// src/server/Requests.ts
|
|
1152
|
+
function toView(view, layout) {
|
|
2155
1153
|
return {
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
1154
|
+
entries: view.entries.map((entry) => ({
|
|
1155
|
+
attachedAt: entry.attachedAt,
|
|
1156
|
+
membership: Option16.getOrNull(entry.membership),
|
|
1157
|
+
ref: entry.ref,
|
|
1158
|
+
status: entry.status
|
|
1159
|
+
})),
|
|
1160
|
+
layout,
|
|
1161
|
+
sessionID: view.sessionID
|
|
2164
1162
|
};
|
|
2165
1163
|
}
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
return pullRequestNotFound;
|
|
2169
|
-
if (!isRecord(input))
|
|
2170
|
-
return invalidGitHubResponse;
|
|
2171
|
-
const contextPage = parseStatusCheckRollup(input.statusCheckRollup);
|
|
2172
|
-
if (!contextPage.ok)
|
|
2173
|
-
return contextPage;
|
|
2174
|
-
if (contextPage.value !== null && contextPage.value.nextCursor === undefined && contextPage.value.contexts.length !== contextPage.value.totalCount) {
|
|
2175
|
-
return invalidGitHubResponse;
|
|
2176
|
-
}
|
|
2177
|
-
const metadata = parsePullRequestMetadata(input, pullRequest);
|
|
2178
|
-
return metadata.ok ? { ok: true, value: { pullRequest, metadata: metadata.value, contextPage: contextPage.value } } : metadata;
|
|
2179
|
-
}
|
|
2180
|
-
function createBatchQuery(size) {
|
|
2181
|
-
const variables = Array.from({ length: size }, (_, index) => `$url${index}: URI!`).join(", ");
|
|
2182
|
-
const fields = Array.from({ length: size }, (_, index) => `pr${index}: resource(url: $url${index}) { ${pullRequestSelection} }`).join(" ");
|
|
2183
|
-
return `query BatchPullRequests(${variables}) { ${fields} }`;
|
|
2184
|
-
}
|
|
2185
|
-
function parseGraphqlErrorAliases(input, size) {
|
|
2186
|
-
if (input === undefined)
|
|
2187
|
-
return { ok: true, value: new Set };
|
|
2188
|
-
if (!Array.isArray(input))
|
|
2189
|
-
return invalidGitHubResponse;
|
|
2190
|
-
const aliases = new Set;
|
|
2191
|
-
for (const error of input) {
|
|
2192
|
-
if (!isRecord(error) || typeof error.message !== "string" || !Array.isArray(error.path) || typeof error.path[0] !== "string") {
|
|
2193
|
-
return invalidGitHubResponse;
|
|
2194
|
-
}
|
|
2195
|
-
const match = /^pr([0-9]+)$/.exec(error.path[0]);
|
|
2196
|
-
if (match === null)
|
|
2197
|
-
return invalidGitHubResponse;
|
|
2198
|
-
const index = Number(match[1]);
|
|
2199
|
-
if (!Number.isInteger(index) || index < 0 || index >= size)
|
|
2200
|
-
return invalidGitHubResponse;
|
|
2201
|
-
aliases.add(index);
|
|
2202
|
-
}
|
|
2203
|
-
return { ok: true, value: aliases };
|
|
2204
|
-
}
|
|
2205
|
-
function parseBatchResponse(input, pullRequests) {
|
|
2206
|
-
if (!isRecord(input) || !isRecord(input.data))
|
|
2207
|
-
return invalidGitHubResponse;
|
|
2208
|
-
const data = input.data;
|
|
2209
|
-
const errorAliases = parseGraphqlErrorAliases(input.errors, pullRequests.length);
|
|
2210
|
-
if (!errorAliases.ok)
|
|
2211
|
-
return errorAliases;
|
|
1164
|
+
var parseTarget = (target) => Effect10.fromResult(parsePullRequestInput(target));
|
|
1165
|
+
function requests({ monitor, tracker }, settings) {
|
|
2212
1166
|
return {
|
|
2213
|
-
|
|
2214
|
-
|
|
1167
|
+
attach: (sessionID, target) => Effect10.gen(function* () {
|
|
1168
|
+
const attached = yield* tracker.attach(sessionID, yield* parseTarget(target), settings.directory);
|
|
1169
|
+
const view = yield* monitor.attached(sessionID, attached.ref, attached.report);
|
|
1170
|
+
return { message: attachedMessage(attached), view: toView(view, settings.layout) };
|
|
1171
|
+
}),
|
|
1172
|
+
detach: (sessionID, target) => Effect10.gen(function* () {
|
|
1173
|
+
const removal = yield* tracker.detach(sessionID, yield* parseTarget(target));
|
|
1174
|
+
const view = yield* monitor.show(sessionID);
|
|
1175
|
+
return { message: detachedMessage(removal, target), view: toView(view, settings.layout) };
|
|
1176
|
+
})
|
|
2215
1177
|
};
|
|
2216
1178
|
}
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
const
|
|
2222
|
-
|
|
2223
|
-
return invalidGitHubResponse;
|
|
2224
|
-
const responseUrl = parsePullRequestUrl(resource.url);
|
|
2225
|
-
if (!responseUrl.ok || !samePullRequest(responseUrl.value, pullRequest) || !isRecord(resource.statusCheckRollup)) {
|
|
2226
|
-
return invalidGitHubResponse;
|
|
2227
|
-
}
|
|
2228
|
-
return parseCheckContexts(resource.statusCheckRollup.contexts);
|
|
2229
|
-
}
|
|
2230
|
-
function isCancellation(cause, signal) {
|
|
2231
|
-
if (signal?.aborted)
|
|
2232
|
-
return true;
|
|
2233
|
-
const originalCause = parseProcessExecutionFailed(cause)?.cause ?? cause;
|
|
2234
|
-
return originalCause instanceof Error && originalCause.name === "AbortError";
|
|
2235
|
-
}
|
|
2236
|
-
var authenticationFailureMarkers = ["http 401", "bad credentials", "not logged into", "gh auth login"];
|
|
2237
|
-
function isAuthenticationFailure(failure) {
|
|
2238
|
-
if (failure.code === 4)
|
|
2239
|
-
return true;
|
|
2240
|
-
const stderr = failure.stderr.toLowerCase();
|
|
2241
|
-
return authenticationFailureMarkers.some((marker) => stderr.includes(marker));
|
|
2242
|
-
}
|
|
2243
|
-
function classifyProcessFailure(cause) {
|
|
2244
|
-
const failure = parseProcessExecutionFailed(cause);
|
|
2245
|
-
if (failure?.code === "ENOENT") {
|
|
2246
|
-
return { tag: "GitHubCliMissing", message: "GitHub CLI is not installed", cause };
|
|
2247
|
-
}
|
|
2248
|
-
if (failure && isAuthenticationFailure(failure)) {
|
|
2249
|
-
return { tag: "GitHubAuthenticationRequired", message: "GitHub CLI authentication required", cause };
|
|
2250
|
-
}
|
|
2251
|
-
return { tag: "GitHubUnavailable", message: "GitHub status unavailable", cause };
|
|
2252
|
-
}
|
|
2253
|
-
function processFailureStdout(cause) {
|
|
2254
|
-
if (!isRecord(cause) || typeof cause.stdout !== "string" || cause.stdout.trim() === "")
|
|
2255
|
-
return;
|
|
2256
|
-
return cause.stdout;
|
|
2257
|
-
}
|
|
2258
|
-
async function runAndDecode(runner, args, options) {
|
|
2259
|
-
let stdout;
|
|
2260
|
-
let processFailure;
|
|
2261
|
-
try {
|
|
2262
|
-
const output = await runner("gh", args, options);
|
|
2263
|
-
stdout = output.stdout;
|
|
2264
|
-
} catch (cause) {
|
|
2265
|
-
if (isCancellation(cause, options.signal)) {
|
|
2266
|
-
return {
|
|
2267
|
-
ok: false,
|
|
2268
|
-
error: {
|
|
2269
|
-
tag: "GitHubCancelled",
|
|
2270
|
-
message: "GitHub status request cancelled",
|
|
2271
|
-
cause
|
|
2272
|
-
}
|
|
2273
|
-
};
|
|
2274
|
-
}
|
|
2275
|
-
processFailure = classifyProcessFailure(cause);
|
|
2276
|
-
const partialStdout = processFailureStdout(cause);
|
|
2277
|
-
if (partialStdout === undefined)
|
|
2278
|
-
return { ok: false, error: processFailure };
|
|
2279
|
-
stdout = partialStdout;
|
|
2280
|
-
}
|
|
2281
|
-
let decoded;
|
|
2282
|
-
try {
|
|
2283
|
-
decoded = JSON.parse(stdout);
|
|
2284
|
-
} catch {
|
|
2285
|
-
return processFailure === undefined ? invalidGitHubResponse : { ok: false, error: processFailure };
|
|
2286
|
-
}
|
|
2287
|
-
return { ok: true, value: { decoded, ...processFailure === undefined ? {} : { processFailure } } };
|
|
2288
|
-
}
|
|
2289
|
-
async function continuePullRequest(runner, initial, options) {
|
|
2290
|
-
if (initial.contextPage?.nextCursor === undefined) {
|
|
2291
|
-
const ci = initial.contextPage === null ? "none" : classifyContexts(initial.contextPage.contexts);
|
|
2292
|
-
return { tag: "Item", result: finalizeResponse(initial.metadata, initial.pullRequest, ci) };
|
|
2293
|
-
}
|
|
2294
|
-
const contexts = [...initial.contextPage.contexts];
|
|
2295
|
-
const totalCount = initial.contextPage.totalCount;
|
|
2296
|
-
const contextIds = new Set(contexts.map((context) => context.id));
|
|
2297
|
-
const cursors = new Set([initial.contextPage.nextCursor]);
|
|
2298
|
-
let cursor = initial.contextPage.nextCursor;
|
|
2299
|
-
while (cursor !== undefined) {
|
|
2300
|
-
const args = [
|
|
2301
|
-
"api",
|
|
2302
|
-
"graphql",
|
|
2303
|
-
"--method",
|
|
2304
|
-
"POST",
|
|
2305
|
-
"-f",
|
|
2306
|
-
`query=${continuationQuery}`,
|
|
2307
|
-
"-f",
|
|
2308
|
-
`url=${initial.pullRequest.url}`,
|
|
2309
|
-
"-f",
|
|
2310
|
-
`cursor=${cursor}`
|
|
2311
|
-
];
|
|
2312
|
-
const output = await runAndDecode(runner, args, options);
|
|
2313
|
-
if (!output.ok) {
|
|
2314
|
-
return output.error.tag === "GitHubCancelled" ? { tag: "Cancelled", error: output.error } : { tag: "Item", result: { ok: false, error: output.error } };
|
|
2315
|
-
}
|
|
2316
|
-
const page = parseContinuationResponse(output.value.decoded, initial.pullRequest);
|
|
2317
|
-
if (!page.ok) {
|
|
2318
|
-
return {
|
|
2319
|
-
tag: "Item",
|
|
2320
|
-
result: { ok: false, error: output.value.processFailure ?? page.error }
|
|
2321
|
-
};
|
|
2322
|
-
}
|
|
2323
|
-
if (page.value.totalCount !== totalCount)
|
|
2324
|
-
return { tag: "Item", result: invalidGitHubResponse };
|
|
2325
|
-
for (const context of page.value.contexts) {
|
|
2326
|
-
if (contextIds.has(context.id))
|
|
2327
|
-
return { tag: "Item", result: invalidGitHubResponse };
|
|
2328
|
-
contextIds.add(context.id);
|
|
2329
|
-
}
|
|
2330
|
-
if (contexts.length + page.value.contexts.length > totalCount) {
|
|
2331
|
-
return { tag: "Item", result: invalidGitHubResponse };
|
|
2332
|
-
}
|
|
2333
|
-
if (page.value.nextCursor !== undefined) {
|
|
2334
|
-
if (cursors.has(page.value.nextCursor))
|
|
2335
|
-
return { tag: "Item", result: invalidGitHubResponse };
|
|
2336
|
-
cursors.add(page.value.nextCursor);
|
|
2337
|
-
}
|
|
2338
|
-
contexts.push(...page.value.contexts);
|
|
2339
|
-
cursor = page.value.nextCursor;
|
|
2340
|
-
}
|
|
2341
|
-
if (contexts.length !== totalCount)
|
|
2342
|
-
return { tag: "Item", result: invalidGitHubResponse };
|
|
2343
|
-
const status = finalizeResponse(initial.metadata, initial.pullRequest, classifyContexts(contexts));
|
|
2344
|
-
return { tag: "Item", result: status };
|
|
2345
|
-
}
|
|
2346
|
-
var execFileRunner = (file, args, options) => new Promise((resolve, reject) => {
|
|
2347
|
-
execFile(file, [...args], {
|
|
2348
|
-
encoding: "utf8",
|
|
2349
|
-
...options.signal ? { signal: options.signal } : {},
|
|
2350
|
-
...options.cwd ? { cwd: options.cwd } : {}
|
|
2351
|
-
}, (error, stdout, stderr) => {
|
|
2352
|
-
if (error) {
|
|
2353
|
-
reject({
|
|
2354
|
-
tag: "ProcessExecutionFailed",
|
|
2355
|
-
code: error.code ?? null,
|
|
2356
|
-
stderr,
|
|
2357
|
-
stdout,
|
|
2358
|
-
cause: error
|
|
2359
|
-
});
|
|
2360
|
-
return;
|
|
2361
|
-
}
|
|
2362
|
-
resolve({ stdout });
|
|
2363
|
-
});
|
|
2364
|
-
});
|
|
2365
|
-
function createGitHubClient(runner = execFileRunner) {
|
|
1179
|
+
|
|
1180
|
+
// src/server/Rpc.ts
|
|
1181
|
+
import { Effect as Effect11 } from "effect";
|
|
1182
|
+
function handlers(services, settings) {
|
|
1183
|
+
const viewOf = (view) => toView(view, settings.layout);
|
|
1184
|
+
const { attach, detach } = requests(services, settings);
|
|
2366
1185
|
return {
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
return githubBatchLimitExceeded;
|
|
2372
|
-
const query = createBatchQuery(pullRequests.length);
|
|
2373
|
-
const args = ["api", "graphql", "--method", "POST", "-f", `query=${query}`];
|
|
2374
|
-
for (const [index, pullRequest] of pullRequests.entries()) {
|
|
2375
|
-
args.push("-f", `url${index}=${pullRequest.url}`);
|
|
2376
|
-
}
|
|
2377
|
-
const output = await runAndDecode(runner, args, options);
|
|
2378
|
-
if (!output.ok)
|
|
2379
|
-
return output;
|
|
2380
|
-
const parsed = parseBatchResponse(output.value.decoded, pullRequests);
|
|
2381
|
-
if (!parsed.ok)
|
|
2382
|
-
return { ok: false, error: output.value.processFailure ?? parsed.error };
|
|
2383
|
-
const outcomes = await Promise.all(parsed.value.map((item) => {
|
|
2384
|
-
if (!item.ok)
|
|
2385
|
-
return Promise.resolve({ tag: "Item", result: item });
|
|
2386
|
-
return continuePullRequest(runner, item.value, options);
|
|
2387
|
-
}));
|
|
2388
|
-
const batch = [];
|
|
2389
|
-
let cancellation;
|
|
2390
|
-
for (const outcome of outcomes) {
|
|
2391
|
-
if (outcome.tag === "Cancelled")
|
|
2392
|
-
cancellation ??= outcome.error;
|
|
2393
|
-
else
|
|
2394
|
-
batch.push(outcome.result);
|
|
2395
|
-
}
|
|
2396
|
-
return cancellation === undefined ? { ok: true, value: batch } : { ok: false, error: cancellation };
|
|
2397
|
-
}
|
|
1186
|
+
attach: ({ sessionID, target }, context) => attach(sessionID, target).pipe(Effect11.mapError(failureMessage), Effect11.mapError((message) => context.error("rejected", message, { message }))),
|
|
1187
|
+
detach: ({ sessionID, target }, context) => detach(sessionID, target).pipe(Effect11.mapError(failureMessage), Effect11.mapError((message) => context.error("rejected", message, { message }))),
|
|
1188
|
+
list: ({ sessionID }, context) => services.monitor.view(sessionID).pipe(Effect11.map(viewOf), Effect11.mapError(failureMessage), Effect11.mapError((message) => context.error("rejected", message, { message }))),
|
|
1189
|
+
refresh: ({ sessionID }, context) => services.monitor.refresh(sessionID).pipe(Effect11.map(viewOf), Effect11.mapError(failureMessage), Effect11.mapError((message) => context.error("rejected", message, { message })))
|
|
2398
1190
|
};
|
|
2399
1191
|
}
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
1192
|
+
|
|
1193
|
+
// src/server/Tools.ts
|
|
1194
|
+
import { Tool } from "@opencode/schema/tool";
|
|
1195
|
+
import { Effect as Effect12, Schema as Schema14 } from "effect";
|
|
1196
|
+
|
|
1197
|
+
// src/domain/Appearance.ts
|
|
1198
|
+
import { Match as Match3 } from "effect";
|
|
2406
1199
|
var diagnosticLabels = {
|
|
1200
|
+
AuthenticationRequired: "authenticate",
|
|
2407
1201
|
GitHubCliMissing: "install gh",
|
|
2408
|
-
GitHubAuthenticationRequired: "run gh auth login",
|
|
2409
1202
|
GitHubUnavailable: "GitHub unavailable",
|
|
2410
|
-
|
|
2411
|
-
|
|
1203
|
+
InvalidResponse: "invalid response",
|
|
1204
|
+
NotFound: "inaccessible"
|
|
2412
1205
|
};
|
|
1206
|
+
var shown = (tone, label, strikethrough = false) => ({
|
|
1207
|
+
label,
|
|
1208
|
+
stale: false,
|
|
1209
|
+
strikethrough,
|
|
1210
|
+
tone
|
|
1211
|
+
});
|
|
1212
|
+
function openAppearance(open) {
|
|
1213
|
+
if (open.mergeability === "conflicting")
|
|
1214
|
+
return shown("red", "conflict");
|
|
1215
|
+
if (open.ci === "failed")
|
|
1216
|
+
return shown("red", "failed");
|
|
1217
|
+
if (open.draft)
|
|
1218
|
+
return shown("gray", "draft");
|
|
1219
|
+
if (open.ci === "pending")
|
|
1220
|
+
return shown("yellow", "pending");
|
|
1221
|
+
if (open.behind)
|
|
1222
|
+
return shown("yellow", "behind");
|
|
1223
|
+
return open.ci === "passed" ? shown("green", "passed") : shown("gray", "no checks");
|
|
1224
|
+
}
|
|
2413
1225
|
function stateAppearance(state) {
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
case "mergeable":
|
|
2420
|
-
case "unknown":
|
|
2421
|
-
switch (state.ci) {
|
|
2422
|
-
case "failed":
|
|
2423
|
-
case "pending":
|
|
2424
|
-
return openAppearances[state.ci];
|
|
2425
|
-
case "none":
|
|
2426
|
-
case "passed":
|
|
2427
|
-
switch (state.blocker) {
|
|
2428
|
-
case "behind":
|
|
2429
|
-
return { tone: "yellow", label: "branch behind", strikethrough: false };
|
|
2430
|
-
case "none":
|
|
2431
|
-
return openAppearances[state.ci];
|
|
2432
|
-
default:
|
|
2433
|
-
return casesHandled(state.blocker);
|
|
2434
|
-
}
|
|
2435
|
-
default:
|
|
2436
|
-
return casesHandled(state.ci);
|
|
2437
|
-
}
|
|
2438
|
-
default:
|
|
2439
|
-
return casesHandled(state.mergeability);
|
|
2440
|
-
}
|
|
2441
|
-
}
|
|
2442
|
-
case "Merged":
|
|
2443
|
-
return { tone: "purple", label: "merged", strikethrough: true };
|
|
2444
|
-
case "Closed":
|
|
2445
|
-
return { tone: "red", label: "closed", strikethrough: true };
|
|
2446
|
-
default:
|
|
2447
|
-
return casesHandled(state);
|
|
2448
|
-
}
|
|
1226
|
+
return Match3.valueTags(state, {
|
|
1227
|
+
Closed: () => shown("red", "closed", true),
|
|
1228
|
+
Merged: () => shown("purple", "merged", true),
|
|
1229
|
+
Open: openAppearance
|
|
1230
|
+
});
|
|
2449
1231
|
}
|
|
2450
|
-
function
|
|
2451
|
-
|
|
2452
|
-
return {
|
|
2453
|
-
tone: "gray",
|
|
2454
|
-
label: status.diagnostic === undefined ? "status unavailable" : diagnosticLabels[status.diagnostic],
|
|
2455
|
-
strikethrough: false
|
|
2456
|
-
};
|
|
2457
|
-
}
|
|
2458
|
-
const appearance = stateAppearance(status.state);
|
|
2459
|
-
return status.stale ? { ...appearance, label: `${appearance.label} (stale; ${diagnosticLabels[status.diagnostic]})` } : appearance;
|
|
1232
|
+
function markStale(fresh) {
|
|
1233
|
+
return { label: fresh.label, stale: true, strikethrough: fresh.strikethrough, tone: fresh.tone };
|
|
2460
1234
|
}
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
if (!batch.ok)
|
|
2468
|
-
return batch;
|
|
2469
|
-
const item = batch.value[0];
|
|
2470
|
-
if (item === undefined)
|
|
2471
|
-
throw new Error("GitHub client omitted the requested pull request");
|
|
2472
|
-
if (!item.ok)
|
|
2473
|
-
return item;
|
|
2474
|
-
return { ok: true, value: undefined };
|
|
2475
|
-
}
|
|
1235
|
+
function appearance(status) {
|
|
1236
|
+
return Match3.valueTags(status, {
|
|
1237
|
+
Fresh: ({ snapshot }) => stateAppearance(snapshot.state),
|
|
1238
|
+
Pending: () => shown("gray", "unavailable"),
|
|
1239
|
+
Stale: ({ snapshot }) => markStale(stateAppearance(snapshot.state)),
|
|
1240
|
+
Unavailable: ({ diagnostic }) => shown("gray", diagnosticLabels[diagnostic])
|
|
2476
1241
|
});
|
|
2477
1242
|
}
|
|
2478
|
-
var invalidPullRequestInput = {
|
|
2479
|
-
ok: false,
|
|
2480
|
-
error: {
|
|
2481
|
-
tag: "InvalidPullRequestInput",
|
|
2482
|
-
message: "Expected https://github.com/<owner>/<repository>/pull/<positive-integer>, github.com/<owner>/<repository>/pull/<positive-integer>, or a positive pull request number"
|
|
2483
|
-
}
|
|
2484
|
-
};
|
|
2485
|
-
var repositoryResolutionFailed = {
|
|
2486
|
-
tag: "RepositoryResolutionFailed",
|
|
2487
|
-
message: "Unable to resolve the current GitHub repository with gh; attach with a full URL instead"
|
|
2488
|
-
};
|
|
2489
|
-
var repositoryResolutionCancelled = {
|
|
2490
|
-
ok: false,
|
|
2491
|
-
error: { tag: "RepositoryResolutionCancelled" }
|
|
2492
|
-
};
|
|
2493
|
-
function isCancellation2(cause, signal) {
|
|
2494
|
-
if (signal?.aborted)
|
|
2495
|
-
return true;
|
|
2496
|
-
return cause instanceof Error && cause.name === "AbortError";
|
|
2497
|
-
}
|
|
2498
|
-
function parseRepositoryPullRequest(stdout, number) {
|
|
2499
|
-
let decoded;
|
|
2500
|
-
try {
|
|
2501
|
-
decoded = JSON.parse(stdout);
|
|
2502
|
-
} catch {
|
|
2503
|
-
return { ok: false, error: repositoryResolutionFailed };
|
|
2504
|
-
}
|
|
2505
|
-
if (decoded === null || typeof decoded !== "object" || !("url" in decoded) || typeof decoded.url !== "string" || decoded.url === "") {
|
|
2506
|
-
return { ok: false, error: repositoryResolutionFailed };
|
|
2507
|
-
}
|
|
2508
|
-
const repositoryUrl = decoded.url.endsWith("/") ? decoded.url.slice(0, -1) : decoded.url;
|
|
2509
|
-
const pullRequest = parsePullRequestUrl(`${repositoryUrl}/pull/${number}`);
|
|
2510
|
-
return pullRequest.ok ? pullRequest : { ok: false, error: repositoryResolutionFailed };
|
|
2511
|
-
}
|
|
2512
|
-
async function resolvePullRequestInput(input, options) {
|
|
2513
|
-
const direct = parsePullRequestUrl(input);
|
|
2514
|
-
if (direct.ok)
|
|
2515
|
-
return direct;
|
|
2516
|
-
if (input.trim() !== input || !/^\d+$/.test(input))
|
|
2517
|
-
return invalidPullRequestInput;
|
|
2518
|
-
const number = Number(input);
|
|
2519
|
-
if (!Number.isSafeInteger(number) || number <= 0)
|
|
2520
|
-
return invalidPullRequestInput;
|
|
2521
|
-
let stdout;
|
|
2522
|
-
try {
|
|
2523
|
-
const result = await (options.runner ?? execFileRunner)("gh", ["repo", "view", "--json", "url"], {
|
|
2524
|
-
cwd: options.directory,
|
|
2525
|
-
...options.signal ? { signal: options.signal } : {}
|
|
2526
|
-
});
|
|
2527
|
-
stdout = result.stdout;
|
|
2528
|
-
} catch (cause) {
|
|
2529
|
-
if (isCancellation2(cause, options.signal))
|
|
2530
|
-
return repositoryResolutionCancelled;
|
|
2531
|
-
return { ok: false, error: { ...repositoryResolutionFailed, cause } };
|
|
2532
|
-
}
|
|
2533
|
-
return parseRepositoryPullRequest(stdout, number);
|
|
2534
|
-
}
|
|
2535
1243
|
|
|
2536
|
-
// src/
|
|
2537
|
-
var
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
var
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
}
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
return invalidStateFile;
|
|
2569
|
-
const attachments = [];
|
|
2570
|
-
const seen = new Set;
|
|
2571
|
-
for (const item of input.pullRequests) {
|
|
2572
|
-
if (!isRecord2(item) || !hasExactKeys(item, ["url", "attachedAt"]))
|
|
2573
|
-
return invalidStateFile;
|
|
2574
|
-
if (typeof item.url !== "string" || typeof item.attachedAt !== "string")
|
|
2575
|
-
return invalidStateFile;
|
|
2576
|
-
const parsed = parsePullRequestUrl(item.url);
|
|
2577
|
-
if (!parsed.ok || parsed.value.url !== item.url || seen.has(item.url))
|
|
2578
|
-
return invalidStateFile;
|
|
2579
|
-
const attachedAt = new Date(item.attachedAt);
|
|
2580
|
-
if (Number.isNaN(attachedAt.valueOf()) || attachedAt.toISOString() !== item.attachedAt)
|
|
2581
|
-
return invalidStateFile;
|
|
2582
|
-
seen.add(item.url);
|
|
2583
|
-
attachments.push({ pullRequest: parsed.value, attachedAt: item.attachedAt });
|
|
2584
|
-
}
|
|
2585
|
-
return { ok: true, value: attachments };
|
|
2586
|
-
}
|
|
2587
|
-
function isMissingFile(cause) {
|
|
2588
|
-
return cause instanceof Error && "code" in cause && cause.code === "ENOENT";
|
|
2589
|
-
}
|
|
2590
|
-
function fileName(sessionID) {
|
|
2591
|
-
return `${createHash("sha256").update(sessionID).digest("hex")}.json`;
|
|
2592
|
-
}
|
|
2593
|
-
function defaultStateDirectory(environment = process.env, home = homedir()) {
|
|
2594
|
-
const dataHome = environment.XDG_DATA_HOME || join(home, ".local", "share");
|
|
2595
|
-
return join(dataHome, "opencode", "opencode-pr-tracker");
|
|
2596
|
-
}
|
|
2597
|
-
function createStateStore(options = {}) {
|
|
2598
|
-
const directory = options.directory ?? defaultStateDirectory();
|
|
2599
|
-
const now = options.now ?? (() => new Date);
|
|
2600
|
-
const lockStateFile = options.lock ?? import_proper_lockfile.lock;
|
|
2601
|
-
const attachTails = new Map;
|
|
2602
|
-
async function enqueueAttach(sessionID, operation) {
|
|
2603
|
-
const previous = attachTails.get(sessionID) ?? Promise.resolve();
|
|
2604
|
-
let release;
|
|
2605
|
-
const current = new Promise((resolve) => {
|
|
2606
|
-
release = resolve;
|
|
2607
|
-
});
|
|
2608
|
-
attachTails.set(sessionID, current);
|
|
2609
|
-
await previous;
|
|
2610
|
-
try {
|
|
2611
|
-
return await operation();
|
|
2612
|
-
} finally {
|
|
2613
|
-
release();
|
|
2614
|
-
if (attachTails.get(sessionID) === current)
|
|
2615
|
-
attachTails.delete(sessionID);
|
|
2616
|
-
}
|
|
2617
|
-
}
|
|
2618
|
-
async function acquireLock(sessionID) {
|
|
2619
|
-
const stateFile = join(directory, fileName(sessionID));
|
|
2620
|
-
let compromised;
|
|
2621
|
-
try {
|
|
2622
|
-
await mkdir(directory, { recursive: true });
|
|
2623
|
-
const release = await lockStateFile(stateFile, {
|
|
2624
|
-
realpath: false,
|
|
2625
|
-
stale: lockStaleMilliseconds,
|
|
2626
|
-
update: lockUpdateMilliseconds,
|
|
2627
|
-
retries: { retries: 50, factor: 1, minTimeout: 10, maxTimeout: 100 },
|
|
2628
|
-
onCompromised: (error) => {
|
|
2629
|
-
compromised = error;
|
|
2630
|
-
}
|
|
2631
|
-
});
|
|
2632
|
-
return { ok: true, value: { release, compromised: () => compromised } };
|
|
2633
|
-
} catch (cause) {
|
|
2634
|
-
return {
|
|
2635
|
-
ok: false,
|
|
2636
|
-
error: stateUnavailable("write", "Unable to lock the session pull request state", cause)
|
|
2637
|
-
};
|
|
2638
|
-
}
|
|
2639
|
-
}
|
|
2640
|
-
async function withLock(sessionID, operation) {
|
|
2641
|
-
const lock = await acquireLock(sessionID);
|
|
2642
|
-
if (!lock.ok)
|
|
2643
|
-
return lock;
|
|
2644
|
-
let result;
|
|
2645
|
-
try {
|
|
2646
|
-
result = await operation();
|
|
2647
|
-
} catch (cause) {
|
|
2648
|
-
await lock.value.release().catch(() => {
|
|
2649
|
-
return;
|
|
2650
|
-
});
|
|
2651
|
-
throw cause;
|
|
2652
|
-
}
|
|
2653
|
-
try {
|
|
2654
|
-
await lock.value.release();
|
|
2655
|
-
} catch (cause) {
|
|
2656
|
-
return {
|
|
2657
|
-
ok: false,
|
|
2658
|
-
error: stateUnavailable("write", "Unable to unlock the session pull request state", cause)
|
|
2659
|
-
};
|
|
2660
|
-
}
|
|
2661
|
-
const compromise = lock.value.compromised();
|
|
2662
|
-
if (compromise !== undefined) {
|
|
2663
|
-
return {
|
|
2664
|
-
ok: false,
|
|
2665
|
-
error: stateUnavailable("write", "The session pull request state lock was compromised", compromise)
|
|
2666
|
-
};
|
|
2667
|
-
}
|
|
2668
|
-
return result;
|
|
2669
|
-
}
|
|
2670
|
-
async function readExisting(sessionID) {
|
|
2671
|
-
const path = join(directory, fileName(sessionID));
|
|
2672
|
-
let content;
|
|
2673
|
-
try {
|
|
2674
|
-
content = await readFile(path, "utf8");
|
|
2675
|
-
} catch (cause) {
|
|
2676
|
-
if (isMissingFile(cause))
|
|
2677
|
-
return { ok: true, value: undefined };
|
|
2678
|
-
return {
|
|
2679
|
-
ok: false,
|
|
2680
|
-
error: stateUnavailable("read", "Unable to read the session pull request state", cause)
|
|
2681
|
-
};
|
|
2682
|
-
}
|
|
2683
|
-
let decoded;
|
|
2684
|
-
try {
|
|
2685
|
-
decoded = JSON.parse(content);
|
|
2686
|
-
} catch {
|
|
2687
|
-
return invalidStateFile;
|
|
2688
|
-
}
|
|
2689
|
-
return parseState(decoded);
|
|
2690
|
-
}
|
|
2691
|
-
async function read(sessionID) {
|
|
2692
|
-
const result = await readExisting(sessionID);
|
|
2693
|
-
if (!result.ok)
|
|
2694
|
-
return result;
|
|
2695
|
-
return { ok: true, value: result.value ?? [] };
|
|
2696
|
-
}
|
|
2697
|
-
async function write(sessionID, attachments) {
|
|
2698
|
-
const destination = join(directory, fileName(sessionID));
|
|
2699
|
-
const temporary = `${destination}.${process.pid}.${randomUUID()}.tmp`;
|
|
2700
|
-
const state = {
|
|
2701
|
-
version: 1,
|
|
2702
|
-
pullRequests: attachments.map((attachment) => ({
|
|
2703
|
-
url: attachment.pullRequest.url,
|
|
2704
|
-
attachedAt: attachment.attachedAt
|
|
2705
|
-
}))
|
|
2706
|
-
};
|
|
2707
|
-
try {
|
|
2708
|
-
await mkdir(directory, { recursive: true });
|
|
2709
|
-
await writeFile(temporary, `${JSON.stringify(state, null, 2)}
|
|
2710
|
-
`, { mode: 384 });
|
|
2711
|
-
await rename(temporary, destination);
|
|
2712
|
-
return { ok: true, value: undefined };
|
|
2713
|
-
} catch (cause) {
|
|
2714
|
-
await rm(temporary, { force: true }).catch(() => {
|
|
2715
|
-
return;
|
|
2716
|
-
});
|
|
2717
|
-
return {
|
|
2718
|
-
ok: false,
|
|
2719
|
-
error: stateUnavailable("write", "Unable to write the session pull request state", cause)
|
|
2720
|
-
};
|
|
2721
|
-
}
|
|
2722
|
-
}
|
|
2723
|
-
async function attach(sessionID, pullRequest, attachOptions = {}) {
|
|
2724
|
-
return enqueueAttach(sessionID, async () => {
|
|
2725
|
-
if (attachOptions.validate !== undefined) {
|
|
2726
|
-
const current = await read(sessionID);
|
|
2727
|
-
if (!current.ok)
|
|
2728
|
-
return current;
|
|
2729
|
-
if (current.value.some((attachment) => attachment.pullRequest.url === pullRequest.url)) {
|
|
2730
|
-
return { ok: true, value: "already_attached" };
|
|
2731
|
-
}
|
|
2732
|
-
if (current.value.length >= maximumPullRequestsPerSession) {
|
|
2733
|
-
return {
|
|
2734
|
-
ok: false,
|
|
2735
|
-
error: {
|
|
2736
|
-
tag: "AttachmentLimitReached",
|
|
2737
|
-
limit: maximumPullRequestsPerSession,
|
|
2738
|
-
message: "A session can track at most 20 pull requests"
|
|
2739
|
-
}
|
|
2740
|
-
};
|
|
2741
|
-
}
|
|
2742
|
-
const validation = await attachOptions.validate();
|
|
2743
|
-
if (!validation.ok)
|
|
2744
|
-
return validation;
|
|
2745
|
-
}
|
|
2746
|
-
return withLock(sessionID, async () => {
|
|
2747
|
-
const current = await read(sessionID);
|
|
2748
|
-
if (!current.ok)
|
|
2749
|
-
return current;
|
|
2750
|
-
if (current.value.some((attachment) => attachment.pullRequest.url === pullRequest.url)) {
|
|
2751
|
-
return { ok: true, value: "already_attached" };
|
|
2752
|
-
}
|
|
2753
|
-
if (current.value.length >= maximumPullRequestsPerSession) {
|
|
2754
|
-
return {
|
|
2755
|
-
ok: false,
|
|
2756
|
-
error: {
|
|
2757
|
-
tag: "AttachmentLimitReached",
|
|
2758
|
-
limit: maximumPullRequestsPerSession,
|
|
2759
|
-
message: "A session can track at most 20 pull requests"
|
|
2760
|
-
}
|
|
2761
|
-
};
|
|
2762
|
-
}
|
|
2763
|
-
const written = await write(sessionID, [...current.value, { pullRequest, attachedAt: now().toISOString() }]);
|
|
2764
|
-
if (!written.ok)
|
|
2765
|
-
return written;
|
|
2766
|
-
return { ok: true, value: "added" };
|
|
2767
|
-
});
|
|
1244
|
+
// src/server/Tools.ts
|
|
1245
|
+
var options = { codemode: true, namespace: "pr", pinned: true };
|
|
1246
|
+
var PullRequestArgument = Schema14.Struct({
|
|
1247
|
+
pull_request: Schema14.Union([Schema14.String, Schema14.Int]).annotate({
|
|
1248
|
+
description: "A pull request URL, such as github.com/owner/repository/pull/123, or a number in this repository"
|
|
1249
|
+
})
|
|
1250
|
+
});
|
|
1251
|
+
var targetOf = ({ pull_request }) => String(pull_request);
|
|
1252
|
+
var NoArguments = Schema14.Struct({});
|
|
1253
|
+
var asToolError = (failure) => new Tool.Error({ message: failureMessage(failure) });
|
|
1254
|
+
var listTool = ({ monitor }) => ({
|
|
1255
|
+
description: "List the pull requests attached to this session, with their status.",
|
|
1256
|
+
execute: (_input, context) => monitor.view(context.sessionID).pipe(Effect12.map((view) => ({
|
|
1257
|
+
content: view.entries.length === 0 ? "No pull requests are attached to this session." : view.entries.map((entry) => `- ${entry.ref.url} (${appearance(entry.status).label})`).join(`
|
|
1258
|
+
`)
|
|
1259
|
+
})), Effect12.mapError(asToolError)),
|
|
1260
|
+
input: NoArguments,
|
|
1261
|
+
name: "list",
|
|
1262
|
+
options
|
|
1263
|
+
});
|
|
1264
|
+
var changeTool = (name, description, change) => ({
|
|
1265
|
+
description,
|
|
1266
|
+
execute: (argument, context) => change(context.sessionID, targetOf(argument)).pipe(Effect12.map((changed) => ({ content: changed.message })), Effect12.mapError(asToolError)),
|
|
1267
|
+
input: PullRequestArgument,
|
|
1268
|
+
name,
|
|
1269
|
+
options
|
|
1270
|
+
});
|
|
1271
|
+
function registerTools(tools, services, settings) {
|
|
1272
|
+
return tools.transform((editor) => {
|
|
1273
|
+
editor.namespace({
|
|
1274
|
+
description: "Pull requests attached to this session and shown in its sidebar",
|
|
1275
|
+
name: "pr"
|
|
2768
1276
|
});
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
return withLock(sessionID, async () => {
|
|
2775
|
-
const current = await read(sessionID);
|
|
2776
|
-
if (!current.ok)
|
|
2777
|
-
return current;
|
|
2778
|
-
const next = current.value.filter((attachment) => attachment.pullRequest.url !== pullRequest.url);
|
|
2779
|
-
if (next.length === current.value.length)
|
|
2780
|
-
return { ok: true, value: "absent" };
|
|
2781
|
-
const written = await write(sessionID, next);
|
|
2782
|
-
if (!written.ok)
|
|
2783
|
-
return written;
|
|
2784
|
-
return { ok: true, value: "removed" };
|
|
2785
|
-
});
|
|
2786
|
-
},
|
|
2787
|
-
async detachByNumber(sessionID, number) {
|
|
2788
|
-
return withLock(sessionID, async () => {
|
|
2789
|
-
const current = await read(sessionID);
|
|
2790
|
-
if (!current.ok)
|
|
2791
|
-
return current;
|
|
2792
|
-
const matches = current.value.filter((attachment) => attachment.pullRequest.number === number);
|
|
2793
|
-
if (matches.length === 0)
|
|
2794
|
-
return { ok: true, value: { tag: "absent" } };
|
|
2795
|
-
if (matches.length > 1) {
|
|
2796
|
-
return {
|
|
2797
|
-
ok: true,
|
|
2798
|
-
value: { tag: "ambiguous", pullRequests: matches.map((attachment) => attachment.pullRequest) }
|
|
2799
|
-
};
|
|
2800
|
-
}
|
|
2801
|
-
const match = matches[0];
|
|
2802
|
-
if (match === undefined)
|
|
2803
|
-
return { ok: true, value: { tag: "absent" } };
|
|
2804
|
-
const next = current.value.filter((attachment) => attachment.pullRequest.url !== match.pullRequest.url);
|
|
2805
|
-
const written = await write(sessionID, next);
|
|
2806
|
-
if (!written.ok)
|
|
2807
|
-
return written;
|
|
2808
|
-
return { ok: true, value: { tag: "removed", pullRequest: match.pullRequest } };
|
|
2809
|
-
});
|
|
2810
|
-
},
|
|
2811
|
-
async removeSession(sessionID) {
|
|
2812
|
-
return withLock(sessionID, async () => {
|
|
2813
|
-
const current = await readExisting(sessionID);
|
|
2814
|
-
if (!current.ok)
|
|
2815
|
-
return current;
|
|
2816
|
-
if (current.value === undefined)
|
|
2817
|
-
return { ok: true, value: "absent" };
|
|
2818
|
-
try {
|
|
2819
|
-
await rm(join(directory, fileName(sessionID)), { force: true });
|
|
2820
|
-
return { ok: true, value: "removed" };
|
|
2821
|
-
} catch (cause) {
|
|
2822
|
-
return {
|
|
2823
|
-
ok: false,
|
|
2824
|
-
error: stateUnavailable("write", "Unable to remove the session pull request state", cause)
|
|
2825
|
-
};
|
|
2826
|
-
}
|
|
2827
|
-
});
|
|
2828
|
-
}
|
|
2829
|
-
};
|
|
1277
|
+
const changes = requests(services, settings);
|
|
1278
|
+
editor.add(listTool(services));
|
|
1279
|
+
editor.add(changeTool("attach", "Attach a pull request to this session. Attaching a GitHub Stack member attaches the whole Stack.", changes.attach));
|
|
1280
|
+
editor.add(changeTool("detach", "Detach a pull request from this session. Other members of its Stack stay attached.", changes.detach));
|
|
1281
|
+
});
|
|
2830
1282
|
}
|
|
2831
1283
|
|
|
2832
1284
|
// src/server.ts
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
function
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
tool: {
|
|
2861
|
-
pr_list: tool({
|
|
2862
|
-
description: "List pull requests attached to the current OpenCode session.",
|
|
2863
|
-
args: {},
|
|
2864
|
-
async execute(_args, context) {
|
|
2865
|
-
const result = await store.list(context.sessionID);
|
|
2866
|
-
if (!result.ok)
|
|
2867
|
-
throw toToolError(result.error);
|
|
2868
|
-
if (result.value.length === 0)
|
|
2869
|
-
return "No pull requests are attached to this session.";
|
|
2870
|
-
return `Attached pull requests:
|
|
2871
|
-
${result.value.map((attachment) => `- ${attachment.pullRequest.url}`).join(`
|
|
2872
|
-
`)}`;
|
|
2873
|
-
}
|
|
2874
|
-
}),
|
|
2875
|
-
pr_attach: tool({
|
|
2876
|
-
description: "Attach a GitHub pull request URL to the current OpenCode session.",
|
|
2877
|
-
args: {
|
|
2878
|
-
url: tool.schema.string().describe("A https://github.com/<owner>/<repository>/pull/<number> or github.com/<owner>/<repository>/pull/<number> URL")
|
|
2879
|
-
},
|
|
2880
|
-
async execute(args, context) {
|
|
2881
|
-
const pullRequest = parsePullRequestUrl(args.url);
|
|
2882
|
-
if (!pullRequest.ok)
|
|
2883
|
-
throw new PrToolError(pullRequest.error.tag, pullRequest.error.message);
|
|
2884
|
-
const result = await attachPullRequest({ store, github }, context.sessionID, pullRequest.value, {
|
|
2885
|
-
signal: context.abort
|
|
2886
|
-
});
|
|
2887
|
-
if (!result.ok)
|
|
2888
|
-
throw toToolError(result.error);
|
|
2889
|
-
const reference = formatPullRequestRef(pullRequest.value);
|
|
2890
|
-
return result.value === "added" ? `Attached ${reference} to this session.` : `${reference} is already attached to this session.`;
|
|
2891
|
-
}
|
|
2892
|
-
}),
|
|
2893
|
-
pr_detach: tool({
|
|
2894
|
-
description: "Detach a pull request from the current OpenCode session by positive number or GitHub URL.",
|
|
2895
|
-
args: {
|
|
2896
|
-
pull_request: tool.schema.union([tool.schema.number().int().positive().max(Number.MAX_SAFE_INTEGER), tool.schema.string()]).describe("https://github.com/owner/repository/pull/123, github.com/owner/repository/pull/123, or 123")
|
|
2897
|
-
},
|
|
2898
|
-
async execute(args, context) {
|
|
2899
|
-
if (typeof args.pull_request === "number") {
|
|
2900
|
-
if (!Number.isSafeInteger(args.pull_request) || args.pull_request <= 0) {
|
|
2901
|
-
throw new PrToolError("InvalidPullRequestNumber", "Expected 123, https://github.com/owner/repository/pull/123, or github.com/owner/repository/pull/123");
|
|
2902
|
-
}
|
|
2903
|
-
const result2 = await store.detachByNumber(context.sessionID, args.pull_request);
|
|
2904
|
-
if (!result2.ok)
|
|
2905
|
-
throw toToolError(result2.error);
|
|
2906
|
-
if (result2.value.tag === "absent") {
|
|
2907
|
-
return `Pull request #${args.pull_request} is not attached to this session.`;
|
|
2908
|
-
}
|
|
2909
|
-
if (result2.value.tag === "ambiguous") {
|
|
2910
|
-
const references = result2.value.pullRequests.map(formatPullRequestRef);
|
|
2911
|
-
throw new PrToolError("AmbiguousPullRequestNumber", `Pull request #${args.pull_request} matches ${formatReferenceList(references)}. Use a canonical GitHub pull request URL.`);
|
|
2912
|
-
}
|
|
2913
|
-
return `Detached ${formatPullRequestRef(result2.value.pullRequest)} from this session.`;
|
|
2914
|
-
}
|
|
2915
|
-
const pullRequest = parsePullRequestUrl(args.pull_request);
|
|
2916
|
-
if (!pullRequest.ok)
|
|
2917
|
-
throw new PrToolError(pullRequest.error.tag, pullRequest.error.message);
|
|
2918
|
-
const result = await store.detach(context.sessionID, pullRequest.value);
|
|
2919
|
-
if (!result.ok)
|
|
2920
|
-
throw toToolError(result.error);
|
|
2921
|
-
const reference = formatPullRequestRef(pullRequest.value);
|
|
2922
|
-
return result.value === "removed" ? `Detached ${reference} from this session.` : `${reference} is not attached to this session.`;
|
|
2923
|
-
}
|
|
2924
|
-
})
|
|
2925
|
-
}
|
|
2926
|
-
};
|
|
2927
|
-
}
|
|
2928
|
-
var plugin = {
|
|
2929
|
-
id: "opencode-pr-tracker",
|
|
2930
|
-
server: async () => createServerHooks(createStateStore(), createGitHubClient())
|
|
2931
|
-
};
|
|
2932
|
-
var server_default = plugin;
|
|
1285
|
+
var pollInterval = "1 second";
|
|
1286
|
+
var Options = Schema15.Struct({ layout: Layout });
|
|
1287
|
+
var layoutOf = (options) => Option17.match(Schema15.decodeUnknownOption(Options)(options), {
|
|
1288
|
+
onNone: () => "default",
|
|
1289
|
+
onSome: ({ layout }) => layout
|
|
1290
|
+
});
|
|
1291
|
+
var forgetDeletedSessions = (ctx, services) => ctx.event.subscribe().pipe(Stream2.runForEach((event) => event.type === "session.deleted" ? Effect13.andThen(services.tracker.forget(event.data.sessionID), services.monitor.forget(event.data.sessionID)) : Effect13.void), Effect13.ignore);
|
|
1292
|
+
var server_default = Plugin.define({
|
|
1293
|
+
effect: (ctx) => Effect13.gen(function* () {
|
|
1294
|
+
const application = layer6.pipe(Layer7.provideMerge(layer5), Layer7.provide([live, layer4(ctx.storage)]));
|
|
1295
|
+
const context = yield* Layer7.build(application);
|
|
1296
|
+
const services = {
|
|
1297
|
+
monitor: Context7.get(context, Monitor),
|
|
1298
|
+
tracker: Context7.get(context, Tracker)
|
|
1299
|
+
};
|
|
1300
|
+
const settings = {
|
|
1301
|
+
directory: ctx.location.directory,
|
|
1302
|
+
layout: layoutOf(ctx.options)
|
|
1303
|
+
};
|
|
1304
|
+
const registration = yield* ctx.rpc.register(PullRequestTracker, handlers(services, settings)).pipe(Effect13.orDie);
|
|
1305
|
+
yield* registerTools(ctx.tool, services, settings);
|
|
1306
|
+
yield* Effect13.forkScoped(forgetDeletedSessions(ctx, services));
|
|
1307
|
+
yield* Effect13.forkScoped(Effect13.repeat(services.monitor.poll, Schedule.spaced(pollInterval)));
|
|
1308
|
+
yield* services.monitor.changes.pipe(Stream2.runForEach((view) => registration.events.emit("updated", toView(view, settings.layout)).pipe(Effect13.ignore)), Effect13.forkScoped);
|
|
1309
|
+
}),
|
|
1310
|
+
id: "opencode-pr-tracker"
|
|
1311
|
+
});
|
|
2933
1312
|
export {
|
|
2934
|
-
server_default as default
|
|
2935
|
-
createServerHooks,
|
|
2936
|
-
PrToolError
|
|
1313
|
+
server_default as default
|
|
2937
1314
|
};
|
|
2938
1315
|
|
|
2939
|
-
//# debugId=
|
|
1316
|
+
//# debugId=2C26781E1C009A7864756E2164756E21
|