@hcrosse/opencode-pr-tracker 0.3.0 → 0.4.1

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