@hcrosse/opencode-pr-tracker 0.2.0 → 0.4.0
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 +68 -53
- package/dist/server.js +1242 -1896
- package/dist/server.js.map +30 -18
- package/dist/tui.js +1122 -2917
- package/dist/tui.js.map +20 -21
- package/package.json +35 -26
- package/server.js +1 -0
- package/tui.js +1 -0
package/dist/server.js
CHANGED
|
@@ -1,1970 +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
206
|
}
|
|
758
207
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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
|
-
}
|
|
774
|
-
}
|
|
775
|
-
retry();
|
|
776
|
-
}
|
|
777
|
-
function retry() {
|
|
778
|
-
clearTimeout(retryTimer);
|
|
779
|
-
retryTimer = undefined;
|
|
780
|
-
if (fs[gracefulQueue].length === 0)
|
|
781
|
-
return;
|
|
782
|
-
var elem = fs[gracefulQueue].shift();
|
|
783
|
-
var fn = elem[0];
|
|
784
|
-
var args = elem[1];
|
|
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
|
};
|
|
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
|
+
}
|
|
445
|
+
|
|
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))
|
|
1024
479
|
});
|
|
480
|
+
var ContinuationData = Schema7.Struct({
|
|
481
|
+
resource: Schema7.Struct({ statusCheckRollup: Schema7.Struct({ contexts: Contexts }) })
|
|
482
|
+
});
|
|
483
|
+
var failure = (diagnostic) => new GitHubFailure({ diagnostic });
|
|
1025
484
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
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);
|
|
1040
522
|
}
|
|
523
|
+
return collected;
|
|
1041
524
|
});
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
});
|
|
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);
|
|
565
|
+
});
|
|
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))
|
|
585
|
+
});
|
|
586
|
+
}));
|
|
587
|
+
var live = layer3.pipe(Layer3.provide(layer2), Layer3.provide([layer, FetchHttpClient.layer]));
|
|
1195
588
|
|
|
1196
|
-
//
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
});
|
|
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 }));
|
|
1223
616
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
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 }));
|
|
1230
634
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
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)
|
|
1233
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
|
+
}
|
|
1234
686
|
|
|
1235
|
-
//
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
}
|
|
1250
|
-
options.fs.realpath(file, callback);
|
|
1251
|
-
}
|
|
1252
|
-
function acquireLock(file, options, callback) {
|
|
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;
|
|
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;
|
|
1291
701
|
}
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
if (
|
|
1295
|
-
return
|
|
1296
|
-
|
|
1297
|
-
|
|
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));
|
|
1298
711
|
});
|
|
1299
712
|
}
|
|
1300
|
-
|
|
1301
|
-
const
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
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);
|
|
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;
|
|
1354
722
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
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
|
-
});
|
|
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))));
|
|
1402
725
|
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
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" }));
|
|
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);
|
|
1416
734
|
}
|
|
1417
|
-
lock2.updateTimeout && clearTimeout(lock2.updateTimeout);
|
|
1418
|
-
lock2.released = true;
|
|
1419
|
-
delete locks[file2];
|
|
1420
|
-
removeLock(file2, options, callback);
|
|
1421
735
|
});
|
|
1422
736
|
}
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
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
|
-
});
|
|
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
|
|
1441
743
|
});
|
|
1442
744
|
}
|
|
1443
|
-
|
|
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
|
-
}
|
|
1453
|
-
});
|
|
1454
|
-
exports.lock = lock;
|
|
1455
|
-
exports.unlock = unlock;
|
|
1456
|
-
exports.check = check;
|
|
1457
|
-
exports.getLocks = getLocks;
|
|
1458
|
-
});
|
|
745
|
+
}
|
|
1459
746
|
|
|
1460
|
-
//
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
function toPromise(method) {
|
|
1481
|
-
return (...args) => new Promise((resolve, reject) => {
|
|
1482
|
-
args.push((err, result) => {
|
|
1483
|
-
if (err) {
|
|
1484
|
-
reject(err);
|
|
1485
|
-
} else {
|
|
1486
|
-
resolve(result);
|
|
1487
|
-
}
|
|
1488
|
-
});
|
|
1489
|
-
method(...args);
|
|
1490
|
-
});
|
|
1491
|
-
}
|
|
1492
|
-
function toSync(method) {
|
|
1493
|
-
return (...args) => {
|
|
1494
|
-
let err;
|
|
1495
|
-
let result;
|
|
1496
|
-
args.push((_err, _result) => {
|
|
1497
|
-
err = _err;
|
|
1498
|
-
result = _result;
|
|
1499
|
-
});
|
|
1500
|
-
method(...args);
|
|
1501
|
-
if (err) {
|
|
1502
|
-
throw err;
|
|
1503
|
-
}
|
|
1504
|
-
return result;
|
|
1505
|
-
};
|
|
1506
|
-
}
|
|
1507
|
-
function toSyncOptions(options) {
|
|
1508
|
-
options = { ...options };
|
|
1509
|
-
options.fs = createSyncFs(options.fs || fs);
|
|
1510
|
-
if (typeof options.retries === "number" && options.retries > 0 || options.retries && typeof options.retries.retries === "number" && options.retries.retries > 0) {
|
|
1511
|
-
throw Object.assign(new Error("Cannot use retries with the sync api"), { code: "ESYNC" });
|
|
1512
|
-
}
|
|
1513
|
-
return options;
|
|
1514
|
-
}
|
|
1515
|
-
module.exports = {
|
|
1516
|
-
toPromise,
|
|
1517
|
-
toSync,
|
|
1518
|
-
toSyncOptions
|
|
747
|
+
// src/application/Known.ts
|
|
748
|
+
import { Duration as Duration3, Option as Option11 } from "effect";
|
|
749
|
+
|
|
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
|
+
}
|
|
757
|
+
|
|
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
|
|
1519
767
|
};
|
|
768
|
+
}
|
|
769
|
+
var isDue = (known, now, ref) => Option11.match((known.get(ref.url) ?? unknown).dueAt, {
|
|
770
|
+
onNone: () => false,
|
|
771
|
+
onSome: (at) => at <= now
|
|
1520
772
|
});
|
|
773
|
+
function withoutUnattached(entries, before, attached) {
|
|
774
|
+
return new Map([...entries].filter(([url, entry]) => attached.has(url) || entry !== before.get(url)));
|
|
775
|
+
}
|
|
1521
776
|
|
|
1522
|
-
//
|
|
1523
|
-
|
|
1524
|
-
|
|
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
|
-
});
|
|
777
|
+
// src/application/Time.ts
|
|
778
|
+
import { Clock, Effect as Effect7 } from "effect";
|
|
779
|
+
var currentMillis = Effect7.map(Clock.currentTimeMillis, Math.floor);
|
|
1554
780
|
|
|
1555
|
-
// src/
|
|
1556
|
-
import {
|
|
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 }) {
|
|
784
|
+
}
|
|
1557
785
|
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
import { homedir } from "os";
|
|
1563
|
-
import { join } from "path";
|
|
786
|
+
class StackIncomplete extends Schema11.TaggedError()("StackIncomplete", {
|
|
787
|
+
url: Schema11.String
|
|
788
|
+
}) {
|
|
789
|
+
}
|
|
1564
790
|
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
}
|
|
1587
|
-
const rawPath = input.slice(authorityEnd).split(/[?#]/, 1).join("");
|
|
1588
|
-
for (const segment of rawPath.split("/")) {
|
|
1589
|
-
let decoded;
|
|
1590
|
-
try {
|
|
1591
|
-
decoded = decodeURIComponent(segment);
|
|
1592
|
-
} catch {
|
|
1593
|
-
return invalidPullRequestUrl;
|
|
1594
|
-
}
|
|
1595
|
-
if (decoded === "." || decoded === "..")
|
|
1596
|
-
return invalidPullRequestUrl;
|
|
1597
|
-
}
|
|
1598
|
-
let parsed;
|
|
1599
|
-
try {
|
|
1600
|
-
parsed = new URL(input);
|
|
1601
|
-
} catch {
|
|
1602
|
-
return invalidPullRequestUrl;
|
|
1603
|
-
}
|
|
1604
|
-
if (parsed.protocol !== "https:" || parsed.hostname !== "github.com" || parsed.username !== "" || parsed.password !== "" || parsed.port !== "" || parsed.search !== "" || parsed.hash !== "") {
|
|
1605
|
-
return invalidPullRequestUrl;
|
|
1606
|
-
}
|
|
1607
|
-
const segments = parsed.pathname.split("/");
|
|
1608
|
-
if (segments.length !== 5 || segments[0] !== "" || segments[3] !== "pull") {
|
|
1609
|
-
return invalidPullRequestUrl;
|
|
1610
|
-
}
|
|
1611
|
-
const rawOwner = segments[1];
|
|
1612
|
-
const rawRepository = segments[2];
|
|
1613
|
-
const numberText = segments[4];
|
|
1614
|
-
if (rawOwner === undefined || rawRepository === undefined || numberText === undefined || !segmentPattern.test(rawOwner) || !segmentPattern.test(rawRepository) || !/^\d+$/.test(numberText)) {
|
|
1615
|
-
return invalidPullRequestUrl;
|
|
1616
|
-
}
|
|
1617
|
-
const number = Number(numberText);
|
|
1618
|
-
if (!Number.isSafeInteger(number) || number <= 0)
|
|
1619
|
-
return invalidPullRequestUrl;
|
|
1620
|
-
const owner = rawOwner.toLowerCase();
|
|
1621
|
-
const repository = rawRepository.toLowerCase();
|
|
1622
|
-
const url = `https://github.com/${owner}/${repository}/pull/${number}`;
|
|
1623
|
-
const value = { url, owner, repository, number };
|
|
1624
|
-
return { ok: true, value };
|
|
1625
|
-
}
|
|
1626
|
-
function formatPullRequestRef(pullRequest) {
|
|
1627
|
-
return `${pullRequest.owner}/${pullRequest.repository}#${pullRequest.number}`;
|
|
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
|
+
});
|
|
1628
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);
|
|
825
|
+
return {
|
|
826
|
+
changed: change.changed,
|
|
827
|
+
ref,
|
|
828
|
+
report,
|
|
829
|
+
stackSize: stack.length,
|
|
830
|
+
tracking: change.tracking
|
|
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
|
+
}));
|
|
1629
850
|
|
|
1630
|
-
// src/
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
function
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
}
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
851
|
+
// src/application/Monitor.ts
|
|
852
|
+
class Monitor extends Context6.Service()("opencode-pr-tracker/Monitor") {
|
|
853
|
+
}
|
|
854
|
+
function entryOf(known, attachment) {
|
|
855
|
+
const current = known.get(attachment.ref.url) ?? unknown;
|
|
856
|
+
return {
|
|
857
|
+
attachedAt: attachment.attachedAt,
|
|
858
|
+
membership: current.membership,
|
|
859
|
+
ref: attachment.ref,
|
|
860
|
+
status: current.status
|
|
861
|
+
};
|
|
862
|
+
}
|
|
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
|
+
});
|
|
874
|
+
}
|
|
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);
|
|
891
|
+
}
|
|
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)
|
|
904
|
+
return;
|
|
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
|
+
});
|
|
912
|
+
}
|
|
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)
|
|
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 }]);
|
|
1738
971
|
}
|
|
1739
|
-
return result;
|
|
1740
972
|
}
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
return { ok: true, value: undefined };
|
|
1749
|
-
return {
|
|
1750
|
-
ok: false,
|
|
1751
|
-
error: stateUnavailable("read", "Unable to read the session pull request state", cause)
|
|
1752
|
-
};
|
|
1753
|
-
}
|
|
1754
|
-
let decoded;
|
|
1755
|
-
try {
|
|
1756
|
-
decoded = JSON.parse(content);
|
|
1757
|
-
} catch {
|
|
1758
|
-
return invalidStateFile;
|
|
973
|
+
return byId;
|
|
974
|
+
}
|
|
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]));
|
|
1759
980
|
}
|
|
1760
|
-
return parseState(decoded);
|
|
1761
|
-
}
|
|
1762
|
-
async function read(sessionID) {
|
|
1763
|
-
const result = await readExisting(sessionID);
|
|
1764
|
-
if (!result.ok)
|
|
1765
|
-
return result;
|
|
1766
|
-
return { ok: true, value: result.value ?? [] };
|
|
1767
981
|
}
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
982
|
+
return claimed;
|
|
983
|
+
}
|
|
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;
|
|
994
|
+
}
|
|
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();
|
|
1004
|
+
}
|
|
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)
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1792
1021
|
}
|
|
1793
1022
|
}
|
|
1794
|
-
return
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1023
|
+
return places;
|
|
1024
|
+
}
|
|
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";
|
|
1034
|
+
}
|
|
1035
|
+
function connector(place) {
|
|
1036
|
+
if (!place.last)
|
|
1037
|
+
return "continues";
|
|
1038
|
+
return place.current.position < place.current.size - 1 ? "open" : "none";
|
|
1039
|
+
}
|
|
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];
|
|
1052
|
+
}
|
|
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
|
+
}));
|
|
1059
|
+
}
|
|
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)
|
|
1819
1091
|
},
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
return current;
|
|
1825
|
-
const next = current.value.filter((attachment) => attachment.pullRequest.url !== pullRequest.url);
|
|
1826
|
-
if (next.length === current.value.length)
|
|
1827
|
-
return { ok: true, value: "absent" };
|
|
1828
|
-
const written = await write(sessionID, next);
|
|
1829
|
-
if (!written.ok)
|
|
1830
|
-
return written;
|
|
1831
|
-
return { ok: true, value: "removed" };
|
|
1832
|
-
});
|
|
1092
|
+
detach: {
|
|
1093
|
+
errors: { rejected: portable(Rejected) },
|
|
1094
|
+
input: portable(Target),
|
|
1095
|
+
output: portable(Changed)
|
|
1833
1096
|
},
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
return current;
|
|
1839
|
-
const matches = current.value.filter((attachment) => attachment.pullRequest.number === number);
|
|
1840
|
-
if (matches.length === 0)
|
|
1841
|
-
return { ok: true, value: { tag: "absent" } };
|
|
1842
|
-
if (matches.length > 1) {
|
|
1843
|
-
return {
|
|
1844
|
-
ok: true,
|
|
1845
|
-
value: { tag: "ambiguous", pullRequests: matches.map((attachment) => attachment.pullRequest) }
|
|
1846
|
-
};
|
|
1847
|
-
}
|
|
1848
|
-
const match = matches[0];
|
|
1849
|
-
if (match === undefined)
|
|
1850
|
-
return { ok: true, value: { tag: "absent" } };
|
|
1851
|
-
const next = current.value.filter((attachment) => attachment.pullRequest.url !== match.pullRequest.url);
|
|
1852
|
-
const written = await write(sessionID, next);
|
|
1853
|
-
if (!written.ok)
|
|
1854
|
-
return written;
|
|
1855
|
-
return { ok: true, value: { tag: "removed", pullRequest: match.pullRequest } };
|
|
1856
|
-
});
|
|
1097
|
+
list: {
|
|
1098
|
+
errors: { rejected: portable(Rejected) },
|
|
1099
|
+
input: portable(Session),
|
|
1100
|
+
output: portable(View)
|
|
1857
1101
|
},
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
return current;
|
|
1863
|
-
if (current.value === undefined)
|
|
1864
|
-
return { ok: true, value: "absent" };
|
|
1865
|
-
try {
|
|
1866
|
-
await rm(join(directory, fileName(sessionID)), { force: true });
|
|
1867
|
-
return { ok: true, value: "removed" };
|
|
1868
|
-
} catch (cause) {
|
|
1869
|
-
return {
|
|
1870
|
-
ok: false,
|
|
1871
|
-
error: stateUnavailable("write", "Unable to remove the session pull request state", cause)
|
|
1872
|
-
};
|
|
1873
|
-
}
|
|
1874
|
-
});
|
|
1102
|
+
refresh: {
|
|
1103
|
+
errors: { rejected: portable(Rejected) },
|
|
1104
|
+
input: portable(Session),
|
|
1105
|
+
output: portable(View)
|
|
1875
1106
|
}
|
|
1876
|
-
};
|
|
1877
|
-
}
|
|
1878
|
-
|
|
1879
|
-
// src/server.ts
|
|
1880
|
-
class PrToolError extends Error {
|
|
1881
|
-
code;
|
|
1882
|
-
name = "PrToolError";
|
|
1883
|
-
constructor(code, message) {
|
|
1884
|
-
super(message);
|
|
1885
|
-
this.code = code;
|
|
1886
1107
|
}
|
|
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
|
+
});
|
|
1887
1137
|
}
|
|
1888
|
-
function
|
|
1889
|
-
|
|
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}.`;
|
|
1890
1143
|
}
|
|
1891
|
-
function
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
return `${references.slice(0, -1).join(", ")}, and ${references.at(-1)}`;
|
|
1144
|
+
function detachedMessage(removal, target) {
|
|
1145
|
+
return Option15.match(removal.removed, {
|
|
1146
|
+
onNone: () => `${target} is not attached.`,
|
|
1147
|
+
onSome: (ref) => `Detached ${ref.label}.`
|
|
1148
|
+
});
|
|
1897
1149
|
}
|
|
1898
|
-
|
|
1150
|
+
|
|
1151
|
+
// src/server/Requests.ts
|
|
1152
|
+
function toView(view, layout) {
|
|
1899
1153
|
return {
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
pr_attach: tool({
|
|
1909
|
-
description: "Attach a canonical GitHub pull request URL to the current OpenCode session.",
|
|
1910
|
-
args: {
|
|
1911
|
-
url: tool.schema.string().describe("A https://github.com/<owner>/<repository>/pull/<number> URL")
|
|
1912
|
-
},
|
|
1913
|
-
async execute(args, context) {
|
|
1914
|
-
const pullRequest = parsePullRequestUrl(args.url);
|
|
1915
|
-
if (!pullRequest.ok)
|
|
1916
|
-
throw new PrToolError(pullRequest.error.tag, pullRequest.error.message);
|
|
1917
|
-
const result = await store.attach(context.sessionID, pullRequest.value);
|
|
1918
|
-
if (!result.ok)
|
|
1919
|
-
throw toToolError(result.error);
|
|
1920
|
-
const reference = formatPullRequestRef(pullRequest.value);
|
|
1921
|
-
return result.value === "added" ? `Attached ${reference} to this session.` : `${reference} is already attached to this session.`;
|
|
1922
|
-
}
|
|
1923
|
-
}),
|
|
1924
|
-
pr_detach: tool({
|
|
1925
|
-
description: "Detach a pull request from the current OpenCode session by positive number or canonical URL.",
|
|
1926
|
-
args: {
|
|
1927
|
-
pull_request: tool.schema.union([tool.schema.number().int().positive().max(Number.MAX_SAFE_INTEGER), tool.schema.string()]).describe("A positive pull request number or https://github.com/<owner>/<repository>/pull/<number> URL")
|
|
1928
|
-
},
|
|
1929
|
-
async execute(args, context) {
|
|
1930
|
-
if (typeof args.pull_request === "number") {
|
|
1931
|
-
if (!Number.isSafeInteger(args.pull_request) || args.pull_request <= 0) {
|
|
1932
|
-
throw new PrToolError("InvalidPullRequestNumber", "Expected a positive pull request number or canonical GitHub URL");
|
|
1933
|
-
}
|
|
1934
|
-
const result2 = await store.detachByNumber(context.sessionID, args.pull_request);
|
|
1935
|
-
if (!result2.ok)
|
|
1936
|
-
throw toToolError(result2.error);
|
|
1937
|
-
if (result2.value.tag === "absent") {
|
|
1938
|
-
return `Pull request #${args.pull_request} is not attached to this session.`;
|
|
1939
|
-
}
|
|
1940
|
-
if (result2.value.tag === "ambiguous") {
|
|
1941
|
-
const references = result2.value.pullRequests.map(formatPullRequestRef);
|
|
1942
|
-
throw new PrToolError("AmbiguousPullRequestNumber", `Pull request #${args.pull_request} matches ${formatReferenceList(references)}. Use a canonical GitHub pull request URL.`);
|
|
1943
|
-
}
|
|
1944
|
-
return `Detached ${formatPullRequestRef(result2.value.pullRequest)} from this session.`;
|
|
1945
|
-
}
|
|
1946
|
-
const pullRequest = parsePullRequestUrl(args.pull_request);
|
|
1947
|
-
if (!pullRequest.ok)
|
|
1948
|
-
throw new PrToolError(pullRequest.error.tag, pullRequest.error.message);
|
|
1949
|
-
const result = await store.detach(context.sessionID, pullRequest.value);
|
|
1950
|
-
if (!result.ok)
|
|
1951
|
-
throw toToolError(result.error);
|
|
1952
|
-
const reference = formatPullRequestRef(pullRequest.value);
|
|
1953
|
-
return result.value === "removed" ? `Detached ${reference} from this session.` : `${reference} is not attached to this session.`;
|
|
1954
|
-
}
|
|
1955
|
-
})
|
|
1956
|
-
}
|
|
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
|
|
1957
1162
|
};
|
|
1958
1163
|
}
|
|
1959
|
-
var
|
|
1960
|
-
|
|
1961
|
-
|
|
1164
|
+
var parseTarget = (target) => Effect10.fromResult(parsePullRequestInput(target));
|
|
1165
|
+
function requests({ monitor, tracker }, settings) {
|
|
1166
|
+
return {
|
|
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
|
+
})
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
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);
|
|
1185
|
+
return {
|
|
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 })))
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
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";
|
|
1199
|
+
var diagnosticLabels = {
|
|
1200
|
+
AuthenticationRequired: "authenticate",
|
|
1201
|
+
GitHubCliMissing: "install gh",
|
|
1202
|
+
GitHubUnavailable: "GitHub unavailable",
|
|
1203
|
+
InvalidResponse: "invalid response",
|
|
1204
|
+
NotFound: "inaccessible"
|
|
1962
1205
|
};
|
|
1963
|
-
var
|
|
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
|
+
}
|
|
1225
|
+
function stateAppearance(state) {
|
|
1226
|
+
return Match3.valueTags(state, {
|
|
1227
|
+
Closed: () => shown("red", "closed", true),
|
|
1228
|
+
Merged: () => shown("purple", "merged", true),
|
|
1229
|
+
Open: openAppearance
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
function markStale(fresh) {
|
|
1233
|
+
return { label: fresh.label, stale: true, strikethrough: fresh.strikethrough, tone: fresh.tone };
|
|
1234
|
+
}
|
|
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])
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
|
|
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"
|
|
1276
|
+
});
|
|
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
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// src/server.ts
|
|
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
|
+
});
|
|
1964
1312
|
export {
|
|
1965
|
-
server_default as default
|
|
1966
|
-
createServerHooks,
|
|
1967
|
-
PrToolError
|
|
1313
|
+
server_default as default
|
|
1968
1314
|
};
|
|
1969
1315
|
|
|
1970
|
-
//# debugId=
|
|
1316
|
+
//# debugId=2C26781E1C009A7864756E2164756E21
|