@hcrosse/opencode-pr-tracker 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -1,1970 +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
206
  }
758
207
  }
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
- }
774
- }
775
- retry();
776
- }
777
- function retry() {
778
- clearTimeout(retryTimer);
779
- retryTimer = undefined;
780
- if (fs[gracefulQueue].length === 0)
781
- return;
782
- var elem = fs[gracefulQueue].shift();
783
- var fn = elem[0];
784
- var args = elem[1];
785
- var err = elem[2];
786
- var startTime = elem[3];
787
- var lastTime = elem[4];
788
- if (startTime === undefined) {
789
- debug("RETRY", fn.name, args);
790
- fn.apply(null, args);
791
- } else if (Date.now() - startTime >= 60000) {
792
- debug("TIMEOUT", fn.name, args);
793
- var cb = args.pop();
794
- if (typeof cb === "function")
795
- cb.call(null, err);
796
- } else {
797
- var sinceAttempt = Date.now() - lastTime;
798
- var sinceStart = Math.max(lastTime - startTime, 1);
799
- var desiredDelay = Math.min(sinceStart * 1.2, 100);
800
- if (sinceAttempt >= desiredDelay) {
801
- debug("RETRY", fn.name, args);
802
- fn.apply(null, args.concat([startTime]));
803
- } else {
804
- fs[gracefulQueue].push(elem);
208
+ }`;
209
+ var pullRequest = (pageSize) => `
210
+ __typename
211
+ ... on PullRequest {
212
+ url title state isDraft mergeable mergeStateStatus
213
+ stack {
214
+ id size
215
+ entries(first: 100) {
216
+ pageInfo { hasNextPage }
217
+ nodes { position pullRequest { url } }
805
218
  }
806
219
  }
807
- 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
  };
436
+ }
437
+ function combined(outcomes) {
438
+ const failures = outcomes.flatMap((outcome) => Option4.toArray(outcome.failure));
439
+ const everyBatchFailed = failures.length === outcomes.length;
440
+ return Option4.match(Option4.filter(Arr2.head(failures), () => everyBatchFailed), {
441
+ onNone: () => Result3.succeed(new Map(outcomes.flatMap((outcome) => outcome.entries))),
442
+ onSome: (diagnostic) => Result3.fail(diagnostic)
443
+ });
444
+ }
445
+
446
+ // src/adapters/github/Token.ts
447
+ import { Config, Context as Context3, Effect as Effect3, Layer as Layer2, Option as Option5, Redacted, Ref } from "effect";
448
+ class Token extends Context3.Service()("opencode-pr-tracker/Token") {
449
+ }
450
+ var variable = (name) => Config.option(Config.redacted(name)).pipe(Config.map(Option5.filter((token) => Redacted.value(token).trim() !== "")));
451
+ var environmentToken = Config.all([variable("GH_TOKEN"), variable("GITHUB_TOKEN")]).pipe(Config.map(([ghToken, githubToken]) => Option5.orElse(ghToken, () => githubToken)));
452
+ var layer2 = Layer2.effect(Token, Effect3.gen(function* () {
453
+ const runner = yield* CommandRunner;
454
+ const cached = yield* Ref.make(Option5.none());
455
+ const fromGh = runner.run("gh", ["auth", "token"], process.cwd()).pipe(Effect3.map((output) => output.trim()), Effect3.filterOrFail((token) => token !== "", () => new GitHubFailure({ diagnostic: "AuthenticationRequired" })), Effect3.catchTags({
456
+ CommandFailed: () => Effect3.fail(new GitHubFailure({ diagnostic: "AuthenticationRequired" })),
457
+ CommandMissing: () => Effect3.fail(new GitHubFailure({ diagnostic: "GitHubCliMissing" }))
458
+ }));
459
+ const fromEnvironment = yield* environmentToken.pipe(Effect3.orElseSucceed(() => Option5.none()));
460
+ const load = Option5.match(fromEnvironment, {
461
+ onNone: () => Effect3.map(fromGh, Redacted.make),
462
+ onSome: Effect3.succeed
463
+ }).pipe(Effect3.tap((token) => Ref.set(cached, Option5.some(token))));
464
+ return Token.of({
465
+ get: Ref.get(cached).pipe(Effect3.flatMap(Option5.match({ onNone: () => load, onSome: Effect3.succeed }))),
466
+ invalidate: Ref.set(cached, Option5.none())
467
+ });
468
+ }));
469
+
470
+ // src/adapters/github/Client.ts
471
+ var endpoint = "https://api.github.com/graphql";
472
+ var GraphQlError = Schema7.Struct({
473
+ path: Schema7.optional(Schema7.Array(Schema7.Union([Schema7.String, Schema7.Number]))),
474
+ type: Schema7.optional(Schema7.String)
475
+ });
476
+ var Envelope = Schema7.Struct({
477
+ data: Schema7.optional(Schema7.NullOr(Schema7.Record(Schema7.String, Schema7.Unknown))),
478
+ errors: Schema7.optional(Schema7.Array(GraphQlError))
1024
479
  });
480
+ var ContinuationData = Schema7.Struct({
481
+ resource: Schema7.Struct({ statusCheckRollup: Schema7.Struct({ contexts: Contexts }) })
482
+ });
483
+ var failure = (diagnostic) => new GitHubFailure({ diagnostic });
1025
484
 
1026
- // 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");
485
+ class Unauthorized extends Schema7.TaggedError()("Unauthorized", {}) {
486
+ }
487
+ var failed2 = (diagnostic) => ({ _tag: "Failed", diagnostic });
488
+ var decodeEnvelope = Schema7.decodeUnknownEffect(Envelope);
489
+ var makePost = Effect4.fn("makePost")(function* () {
490
+ const http = yield* HttpClient.HttpClient;
491
+ const token = yield* Token;
492
+ const attempt = Effect4.fn("attempt")(function* (query, variables) {
493
+ const bearer = yield* token.get;
494
+ const request = HttpClientRequest.post(endpoint).pipe(HttpClientRequest.bearerToken(Redacted2.value(bearer)), HttpClientRequest.bodyJsonUnsafe({ query, variables }));
495
+ const response = yield* http.execute(request).pipe(Effect4.mapError(() => failure("GitHubUnavailable")));
496
+ if (response.status === 401)
497
+ return yield* new Unauthorized;
498
+ if (response.status < 200 || response.status >= 300)
499
+ return yield* failure("GitHubUnavailable");
500
+ const body = yield* response.json.pipe(Effect4.mapError(() => failure("InvalidResponse")));
501
+ return yield* decodeEnvelope(body).pipe(Effect4.mapError(() => failure("InvalidResponse")));
502
+ });
503
+ return (query, variables) => attempt(query, variables).pipe(Effect4.catchTag("Unauthorized", () => Effect4.andThen(token.invalidate, attempt(query, variables))), Effect4.catchTag("Unauthorized", () => Effect4.fail(failure("AuthenticationRequired"))));
504
+ });
505
+ var allContexts = Effect4.fn("allContexts")(function* (post, node) {
506
+ const collected = [];
507
+ const followed = new Set;
508
+ let page = Option6.map(Option6.fromNullishOr(node.statusCheckRollup), (rollup) => rollup.contexts);
509
+ while (Option6.isSome(page)) {
510
+ collected.push(...page.value.nodes);
511
+ if (!page.value.pageInfo.hasNextPage)
512
+ break;
513
+ const after = page.value.pageInfo.endCursor ?? "";
514
+ if (after === "" || followed.has(after))
515
+ return yield* failure("InvalidResponse");
516
+ followed.add(after);
517
+ const envelope = yield* post(continuation(), { cursor: after, url: node.url });
518
+ if ((envelope.errors ?? []).length > 0)
519
+ return yield* failure("InvalidResponse");
520
+ const data = yield* Schema7.decodeUnknownEffect(ContinuationData)(envelope.data).pipe(Effect4.mapError(() => failure("InvalidResponse")));
521
+ page = Option6.some(data.resource.statusCheckRollup.contexts);
1040
522
  }
523
+ return collected;
1041
524
  });
1042
-
1043
- // 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";
525
+ var inaccessible = new Set(["NOT_FOUND", "FORBIDDEN"]);
526
+ function aliasFailure(request, key) {
527
+ const diagnostics = (request.envelope.errors ?? []).flatMap((error) => {
528
+ const root = String((error.path ?? [])[0] ?? "");
529
+ if (root === key)
530
+ return [inaccessible.has(error.type ?? "") ? "NotFound" : "InvalidResponse"];
531
+ return request.aliases.has(root) ? [] : ["InvalidResponse"];
532
+ });
533
+ if (diagnostics.length === 0)
534
+ return Option6.none();
535
+ return Option6.some(diagnostics.includes("InvalidResponse") ? "InvalidResponse" : "NotFound");
536
+ }
537
+ var itemResult = Effect4.fn("itemResult")(function* (request, ref, key) {
538
+ const { envelope, post } = request;
539
+ const reported = aliasFailure(request, key);
540
+ const raw = Option6.fromNullishOr((envelope.data ?? {})[key]);
541
+ if (Option6.isSome(reported))
542
+ return failed2(reported.value);
543
+ if (Option6.isNone(raw))
544
+ return failed2("NotFound");
545
+ const node = Schema7.decodeUnknownOption(PullRequestNode)(raw.value);
546
+ if (Option6.isNone(node))
547
+ return failed2("InvalidResponse");
548
+ const contexts = yield* Effect4.result(allContexts(post, node.value));
549
+ if (Result4.isFailure(contexts))
550
+ return failed2(contexts.failure.diagnostic);
551
+ return {
552
+ _tag: "Reported",
553
+ report: toReport(ref, node.value, contexts.success)
1048
554
  };
1049
- if (!processOk(process2)) {
1050
- module.exports = function() {
1051
- return function() {};
1052
- };
1053
- } else {
1054
- assert = __require("assert");
1055
- signals = require_signals();
1056
- isWin = /^win/i.test(process2.platform);
1057
- EE = __require("events");
1058
- if (typeof EE !== "function") {
1059
- EE = EE.EventEmitter;
1060
- }
1061
- if (process2.__signal_exit_emitter__) {
1062
- emitter = process2.__signal_exit_emitter__;
1063
- } else {
1064
- emitter = process2.__signal_exit_emitter__ = new EE;
1065
- emitter.count = 0;
1066
- emitter.emitted = {};
1067
- }
1068
- if (!emitter.infinite) {
1069
- emitter.setMaxListeners(Infinity);
1070
- emitter.infinite = true;
1071
- }
1072
- module.exports = function(cb, opts) {
1073
- if (!processOk(global.process)) {
1074
- return function() {};
1075
- }
1076
- assert.equal(typeof cb, "function", "a callback must be provided for exit handler");
1077
- if (loaded === false) {
1078
- load();
1079
- }
1080
- var ev = "exit";
1081
- if (opts && opts.alwaysLast) {
1082
- ev = "afterexit";
1083
- }
1084
- var remove = function() {
1085
- emitter.removeListener(ev, cb);
1086
- if (emitter.listeners("exit").length === 0 && emitter.listeners("afterexit").length === 0) {
1087
- unload();
1088
- }
1089
- };
1090
- emitter.on(ev, cb);
1091
- return remove;
1092
- };
1093
- unload = function unload2() {
1094
- if (!loaded || !processOk(global.process)) {
1095
- return;
1096
- }
1097
- loaded = false;
1098
- signals.forEach(function(sig) {
1099
- try {
1100
- process2.removeListener(sig, sigListeners[sig]);
1101
- } catch (er) {}
1102
- });
1103
- process2.emit = originalProcessEmit;
1104
- process2.reallyExit = originalProcessReallyExit;
1105
- emitter.count -= 1;
1106
- };
1107
- module.exports.unload = unload;
1108
- emit = function emit2(event, code, signal) {
1109
- if (emitter.emitted[event]) {
1110
- return;
1111
- }
1112
- emitter.emitted[event] = true;
1113
- emitter.emit(event, code, signal);
1114
- };
1115
- sigListeners = {};
1116
- signals.forEach(function(sig) {
1117
- sigListeners[sig] = function listener() {
1118
- if (!processOk(global.process)) {
1119
- return;
1120
- }
1121
- var listeners = process2.listeners(sig);
1122
- if (listeners.length === emitter.count) {
1123
- unload();
1124
- emit("exit", null, sig);
1125
- emit("afterexit", null, sig);
1126
- if (isWin && sig === "SIGHUP") {
1127
- sig = "SIGINT";
1128
- }
1129
- process2.kill(process2.pid, sig);
1130
- }
1131
- };
1132
- });
1133
- module.exports.signals = function() {
1134
- return signals;
1135
- };
1136
- loaded = false;
1137
- load = function load2() {
1138
- if (loaded || !processOk(global.process)) {
1139
- return;
1140
- }
1141
- loaded = true;
1142
- emitter.count += 1;
1143
- signals = signals.filter(function(sig) {
1144
- try {
1145
- process2.on(sig, sigListeners[sig]);
1146
- return true;
1147
- } catch (er) {
1148
- return false;
1149
- }
1150
- });
1151
- process2.emit = processEmit;
1152
- process2.reallyExit = processReallyExit;
1153
- };
1154
- module.exports.load = load;
1155
- originalProcessReallyExit = process2.reallyExit;
1156
- processReallyExit = function processReallyExit2(code) {
1157
- if (!processOk(global.process)) {
1158
- return;
1159
- }
1160
- process2.exitCode = code || 0;
1161
- emit("exit", process2.exitCode, null);
1162
- emit("afterexit", process2.exitCode, null);
1163
- originalProcessReallyExit.call(process2, process2.exitCode);
1164
- };
1165
- originalProcessEmit = process2.emit;
1166
- processEmit = function processEmit2(ev, arg) {
1167
- if (ev === "exit" && processOk(global.process)) {
1168
- if (arg !== undefined) {
1169
- process2.exitCode = arg;
1170
- }
1171
- var ret = originalProcessEmit.apply(this, arguments);
1172
- emit("exit", process2.exitCode, null);
1173
- emit("afterexit", process2.exitCode, null);
1174
- return ret;
1175
- } else {
1176
- return originalProcessEmit.apply(this, arguments);
1177
- }
1178
- };
1179
- }
1180
- var assert;
1181
- var signals;
1182
- var isWin;
1183
- var EE;
1184
- var emitter;
1185
- var unload;
1186
- var emit;
1187
- var sigListeners;
1188
- var loaded;
1189
- var load;
1190
- var originalProcessReallyExit;
1191
- var processReallyExit;
1192
- var originalProcessEmit;
1193
- var processEmit;
1194
555
  });
556
+ var fetchBatch = Effect4.fn("fetchBatch")(function* (post, refs) {
557
+ const variables = Object.fromEntries(refs.map((ref, index) => [alias(index), ref.url]));
558
+ const envelope = yield* post(batch(refs.length), variables);
559
+ if (Option6.isNone(Option6.fromNullishOr(envelope.data)))
560
+ return yield* failure("GitHubUnavailable");
561
+ const results = yield* Effect4.forEach(refs, (ref, index) => itemResult({ aliases: new Set(Object.keys(variables)), envelope, post }, ref, alias(index)), {
562
+ concurrency: 4
563
+ });
564
+ return Arr3.zip(refs.map((ref) => ref.url), results);
565
+ });
566
+ var outcomeOf = (post, refs) => Effect4.gen(function* () {
567
+ const result = yield* Effect4.result(fetchBatch(post, refs));
568
+ if (Result4.isSuccess(result))
569
+ return { entries: result.success, failure: Option6.none() };
570
+ const { diagnostic } = result.failure;
571
+ return {
572
+ entries: refs.map((ref) => [ref.url, { _tag: "Failed", diagnostic }]),
573
+ failure: Option6.some(diagnostic)
574
+ };
575
+ });
576
+ var layer3 = Layer3.effect(GitHub, Effect4.gen(function* () {
577
+ const post = yield* makePost();
578
+ const runner = yield* CommandRunner;
579
+ return GitHub.of({
580
+ fetch: (refs) => {
581
+ const batches = Arr3.chunksOf(Arr3.dedupeWith(refs, (left, right) => left.url === right.url), maximumBatch);
582
+ return Effect4.forEach(batches, (refsInBatch) => outcomeOf(post, refsInBatch)).pipe(Effect4.flatMap((outcomes) => Effect4.fromResult(combined(outcomes))), Effect4.mapError(failure));
583
+ },
584
+ pullRequestInRepository: (directory, number) => resolveInRepository(directory, number).pipe(Effect4.provideService(CommandRunner, runner))
585
+ });
586
+ }));
587
+ var live = layer3.pipe(Layer3.provide(layer2), Layer3.provide([layer, FetchHttpClient.layer]));
1195
588
 
1196
- // 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
- });
589
+ // src/adapters/Storage.ts
590
+ import { Array as Arr5, Effect as Effect5, Layer as Layer4, Option as Option8, Result as Result6, Schema as Schema10 } from "effect";
591
+
592
+ // src/domain/Tracking.ts
593
+ import { Array as Arr4, Option as Option7, Result as Result5, Schema as Schema8 } from "effect";
594
+ var maximumAttachments = 20;
595
+
596
+ class Attachment extends Schema8.Class("Attachment")({
597
+ attachedAt: Schema8.Int,
598
+ ref: PullRequestRef
599
+ }) {
600
+ }
601
+
602
+ class AttachmentLimitReached extends Schema8.TaggedError()("AttachmentLimitReached", { limit: Schema8.Int, requested: Schema8.Int }) {
603
+ }
604
+
605
+ class AmbiguousPullRequestNumber extends Schema8.TaggedError()("AmbiguousPullRequestNumber", { matches: Schema8.Array(PullRequestRef), number: Schema8.Int }) {
606
+ }
607
+ function isMember(stack, attachment) {
608
+ return stack.some((ref) => samePullRequest(ref, attachment.ref));
609
+ }
610
+ function attach(tracking, stack, now) {
611
+ const members = Arr4.dedupeWith(stack, samePullRequest);
612
+ const others = tracking.filter((attachment) => !isMember(members, attachment));
613
+ const requested = others.length + members.length;
614
+ if (requested > maximumAttachments) {
615
+ return Result5.fail(new AttachmentLimitReached({ limit: maximumAttachments, requested }));
1223
616
  }
1224
- 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);
617
+ const position = tracking.findIndex((attachment) => isMember(members, attachment));
618
+ const insertAt = position === -1 ? others.length : position;
619
+ const placed = members.map((ref) => Option7.getOrElse(Arr4.findFirst(tracking, (attachment) => samePullRequest(attachment.ref, ref)), () => new Attachment({ attachedAt: now, ref })));
620
+ const next = [...others.slice(0, insertAt), ...placed, ...others.slice(insertAt)];
621
+ const changed = next.length !== tracking.length || next.some((attachment, index) => attachment !== tracking[index]);
622
+ return Result5.succeed({ changed, tracking: next });
623
+ }
624
+ function detach(tracking, ref) {
625
+ const next = tracking.filter((attachment) => !samePullRequest(attachment.ref, ref));
626
+ const removed = next.length === tracking.length ? Option7.none() : Option7.some(ref);
627
+ return { removed, tracking: next };
628
+ }
629
+ function detachNumber(tracking, number) {
630
+ const matches = tracking.filter((attachment) => attachment.ref.number === number);
631
+ if (matches.length > 1) {
632
+ const refs = matches.map((attachment) => attachment.ref);
633
+ return Result5.fail(new AmbiguousPullRequestNumber({ matches: refs, number }));
1230
634
  }
1231
- exports.probe = probe;
1232
- exports.getMtime = getMtime;
635
+ return Result5.succeed(Option7.match(Arr4.head(matches), {
636
+ onNone: () => ({ removed: Option7.none(), tracking }),
637
+ onSome: (attachment) => detach(tracking, attachment.ref)
638
+ }));
639
+ }
640
+
641
+ // src/ports/TrackingRepository.ts
642
+ import { Context as Context4, Schema as Schema9 } from "effect";
643
+
644
+ class StoredStateInvalid extends Schema9.TaggedError()("StoredStateInvalid", {
645
+ sessionID: Schema9.String
646
+ }) {
647
+ }
648
+
649
+ class TrackingRepository extends Context4.Service()("opencode-pr-tracker/TrackingRepository") {
650
+ }
651
+
652
+ // src/adapters/Storage.ts
653
+ var Stored = Schema10.Struct({
654
+ pullRequests: Schema10.Array(Schema10.Struct({ attachedAt: Schema10.Int, url: Schema10.String })).check(Schema10.isMaxLength(maximumAttachments)),
655
+ version: Schema10.Literal(1)
1233
656
  });
657
+ var keyOf = (sessionID) => `session/${sessionID}`;
658
+ function toTracking(stored) {
659
+ const attachments = stored.pullRequests.map((entry) => Result6.map(parsePullRequestUrl(entry.url), (ref) => new Attachment({ attachedAt: entry.attachedAt, ref })));
660
+ const valid = attachments.flatMap((attachment) => Option8.toArray(Result6.getSuccess(attachment)));
661
+ const unique = Arr5.dedupeWith(valid, (left, right) => left.ref.url === right.ref.url);
662
+ return unique.length === stored.pullRequests.length ? Option8.some(unique) : Option8.none();
663
+ }
664
+ function toStored(tracking) {
665
+ return {
666
+ pullRequests: tracking.map((attachment) => ({
667
+ attachedAt: attachment.attachedAt,
668
+ url: attachment.ref.url
669
+ })),
670
+ version: 1
671
+ };
672
+ }
673
+ function layer4(storage) {
674
+ return Layer4.succeed(TrackingRepository, TrackingRepository.of({
675
+ load: (sessionID) => storage.get(keyOf(sessionID)).pipe(Effect5.flatMap((value) => Option8.match(Option8.fromNullishOr(value), {
676
+ onNone: () => Effect5.succeed([]),
677
+ onSome: (json) => Schema10.decodeUnknownOption(Stored)(json).pipe(Option8.flatMap(toTracking), Option8.match({
678
+ onNone: () => Effect5.fail(new StoredStateInvalid({ sessionID })),
679
+ onSome: (tracking) => Effect5.succeed(tracking)
680
+ }))
681
+ }))),
682
+ remove: (sessionID) => storage.remove(keyOf(sessionID)),
683
+ save: (sessionID, tracking) => storage.set(keyOf(sessionID), toStored(tracking))
684
+ }));
685
+ }
1234
686
 
1235
- // 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;
687
+ // src/application/Monitor.ts
688
+ import { Context as Context6, Effect as Effect9, Layer as Layer6, Option as Option13, PubSub, Ref as Ref2, Result as Result7, Stream } from "effect";
689
+
690
+ // src/application/FetchQueue.ts
691
+ import { Deferred, Effect as Effect6, Exit, Option as Option9 } from "effect";
692
+
693
+ class FetchQueue {
694
+ running = false;
695
+ queued = Option9.none();
696
+ fetchNow;
697
+ scope;
698
+ constructor(fetchNow, scope) {
699
+ this.fetchNow = fetchNow;
700
+ this.scope = scope;
1291
701
  }
1292
- 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();
702
+ fetch(refs) {
703
+ return Effect6.suspend(() => {
704
+ if (refs.length === 0)
705
+ return Effect6.void;
706
+ if (this.running)
707
+ return Effect6.asVoid(Deferred.await(this.join(refs)));
708
+ this.running = true;
709
+ const done = Deferred.makeUnsafe();
710
+ return Effect6.andThen(Effect6.forkIn(this.drain(refs, done), this.scope), Deferred.await(done));
1298
711
  });
1299
712
  }
1300
- 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);
713
+ join(refs) {
714
+ const next = Option9.getOrElse(this.queued, () => ({
715
+ done: Deferred.makeUnsafe(),
716
+ refs: new Map
717
+ }));
718
+ for (const ref of refs)
719
+ next.refs.set(ref.url, ref);
720
+ this.queued = Option9.some(next);
721
+ return next.done;
1354
722
  }
1355
- 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
- });
723
+ drain(refs, done) {
724
+ return this.fetchNow(refs).pipe(Effect6.andThen(Deferred.succeed(done, true)), Effect6.andThen(Effect6.suspend(() => this.next())), Effect6.onExit((exit) => Exit.isSuccess(exit) ? Effect6.void : Effect6.suspend(() => this.reset(done, exit.cause))));
1402
725
  }
1403
- 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" }));
726
+ next() {
727
+ return Option9.match(this.queued, {
728
+ onNone: () => Effect6.sync(() => {
729
+ this.running = false;
730
+ }),
731
+ onSome: (queued) => {
732
+ this.queued = Option9.none();
733
+ return this.drain([...queued.refs.values()], queued.done);
1416
734
  }
1417
- lock2.updateTimeout && clearTimeout(lock2.updateTimeout);
1418
- lock2.released = true;
1419
- delete locks[file2];
1420
- removeLock(file2, options, callback);
1421
735
  });
1422
736
  }
1423
- 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
- });
737
+ reset(done, cause) {
738
+ const waiting = [done, ...Option9.toArray(Option9.map(this.queued, (queued) => queued.done))];
739
+ this.running = false;
740
+ this.queued = Option9.none();
741
+ return Effect6.forEach(waiting, (deferred) => Deferred.failCause(deferred, cause), {
742
+ discard: true
1441
743
  });
1442
744
  }
1443
- 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
- }
1453
- });
1454
- exports.lock = lock;
1455
- exports.unlock = unlock;
1456
- exports.check = check;
1457
- exports.getLocks = getLocks;
1458
- });
745
+ }
1459
746
 
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);
1490
- });
1491
- }
1492
- function toSync(method) {
1493
- return (...args) => {
1494
- let err;
1495
- let result;
1496
- args.push((_err, _result) => {
1497
- err = _err;
1498
- result = _result;
1499
- });
1500
- method(...args);
1501
- if (err) {
1502
- throw err;
1503
- }
1504
- return result;
1505
- };
1506
- }
1507
- function toSyncOptions(options) {
1508
- options = { ...options };
1509
- options.fs = createSyncFs(options.fs || fs);
1510
- if (typeof options.retries === "number" && options.retries > 0 || options.retries && typeof options.retries.retries === "number" && options.retries.retries > 0) {
1511
- throw Object.assign(new Error("Cannot use retries with the sync api"), { code: "ESYNC" });
1512
- }
1513
- return options;
1514
- }
1515
- module.exports = {
1516
- toPromise,
1517
- toSync,
1518
- toSyncOptions
747
+ // src/application/Known.ts
748
+ import { Duration as Duration3, Option as Option11 } from "effect";
749
+
750
+ // src/domain/RefreshPolicy.ts
751
+ import { Duration as Duration2, Option as Option10 } from "effect";
752
+ var refreshInterval = Duration2.seconds(15);
753
+ function nextRefresh(status) {
754
+ const merged = (status._tag === "Fresh" || status._tag === "Stale") && status.snapshot.state._tag === "Merged";
755
+ return merged ? Option10.none() : Option10.some(refreshInterval);
756
+ }
757
+
758
+ // src/application/Known.ts
759
+ var unknown = { dueAt: Option11.some(0), membership: Option11.none(), status: pending };
760
+ function afterRefresh(previous, result, now) {
761
+ const status = result._tag === "Reported" ? succeeded(result.report.snapshot) : failed(previous.status, result.diagnostic, now);
762
+ const membership = result._tag === "Reported" ? result.report.membership : previous.membership;
763
+ return {
764
+ dueAt: Option11.map(nextRefresh(status), (delay) => now + Duration3.toMillis(delay)),
765
+ membership,
766
+ status
1519
767
  };
768
+ }
769
+ var isDue = (known, now, ref) => Option11.match((known.get(ref.url) ?? unknown).dueAt, {
770
+ onNone: () => false,
771
+ onSome: (at) => at <= now
1520
772
  });
773
+ function withoutUnattached(entries, before, attached) {
774
+ return new Map([...entries].filter(([url, entry]) => attached.has(url) || entry !== before.get(url)));
775
+ }
1521
776
 
1522
- // 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
- });
777
+ // src/application/Time.ts
778
+ import { Clock, Effect as Effect7 } from "effect";
779
+ var currentMillis = Effect7.map(Clock.currentTimeMillis, Math.floor);
1554
780
 
1555
- // src/server.ts
1556
- import { tool } from "@opencode-ai/plugin";
781
+ // src/application/Tracker.ts
782
+ import { Array as Arr6, Context as Context5, Effect as Effect8, Layer as Layer5, Option as Option12, Schema as Schema11, Semaphore } from "effect";
783
+ class PullRequestUnavailable extends Schema11.TaggedError()("PullRequestUnavailable", { diagnostic: Diagnostic, url: Schema11.String }) {
784
+ }
1557
785
 
1558
- // src/state.ts
1559
- var import_proper_lockfile = __toESM(require_proper_lockfile(), 1);
1560
- import { createHash, randomUUID } from "crypto";
1561
- import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
1562
- import { homedir } from "os";
1563
- import { join } from "path";
786
+ class StackIncomplete extends Schema11.TaggedError()("StackIncomplete", {
787
+ url: Schema11.String
788
+ }) {
789
+ }
1564
790
 
1565
- // src/url.ts
1566
- var invalidPullRequestUrl = {
1567
- ok: false,
1568
- error: {
1569
- tag: "InvalidPullRequestUrl",
1570
- message: "Expected https://github.com/<owner>/<repository>/pull/<positive-integer>"
1571
- }
1572
- };
1573
- var segmentPattern = /^[A-Za-z0-9._-]+$/;
1574
- function parsePullRequestUrl(input) {
1575
- if (input.trim() !== input)
1576
- return invalidPullRequestUrl;
1577
- if (input.includes("\\"))
1578
- return invalidPullRequestUrl;
1579
- if (!input.startsWith("https://"))
1580
- return invalidPullRequestUrl;
1581
- const authorityEnd = input.indexOf("/", "https://".length);
1582
- if (authorityEnd === -1)
1583
- return invalidPullRequestUrl;
1584
- if (input.slice("https://".length, authorityEnd).toLowerCase() !== "github.com") {
1585
- return invalidPullRequestUrl;
1586
- }
1587
- const rawPath = input.slice(authorityEnd).split(/[?#]/, 1).join("");
1588
- for (const segment of rawPath.split("/")) {
1589
- let decoded;
1590
- try {
1591
- decoded = decodeURIComponent(segment);
1592
- } catch {
1593
- return invalidPullRequestUrl;
1594
- }
1595
- if (decoded === "." || decoded === "..")
1596
- return invalidPullRequestUrl;
1597
- }
1598
- let parsed;
1599
- try {
1600
- parsed = new URL(input);
1601
- } catch {
1602
- return invalidPullRequestUrl;
1603
- }
1604
- if (parsed.protocol !== "https:" || parsed.hostname !== "github.com" || parsed.username !== "" || parsed.password !== "" || parsed.port !== "" || parsed.search !== "" || parsed.hash !== "") {
1605
- return invalidPullRequestUrl;
1606
- }
1607
- const segments = parsed.pathname.split("/");
1608
- if (segments.length !== 5 || segments[0] !== "" || segments[3] !== "pull") {
1609
- return invalidPullRequestUrl;
1610
- }
1611
- const rawOwner = segments[1];
1612
- const rawRepository = segments[2];
1613
- const numberText = segments[4];
1614
- if (rawOwner === undefined || rawRepository === undefined || numberText === undefined || !segmentPattern.test(rawOwner) || !segmentPattern.test(rawRepository) || !/^\d+$/.test(numberText)) {
1615
- return invalidPullRequestUrl;
1616
- }
1617
- const number = Number(numberText);
1618
- if (!Number.isSafeInteger(number) || number <= 0)
1619
- return invalidPullRequestUrl;
1620
- const owner = rawOwner.toLowerCase();
1621
- const repository = rawRepository.toLowerCase();
1622
- const url = `https://github.com/${owner}/${repository}/pull/${number}`;
1623
- const value = { url, owner, repository, number };
1624
- return { ok: true, value };
1625
- }
1626
- function formatPullRequestRef(pullRequest) {
1627
- return `${pullRequest.owner}/${pullRequest.repository}#${pullRequest.number}`;
791
+ class Tracker extends Context5.Service()("opencode-pr-tracker/Tracker") {
792
+ }
793
+ function discovered(ref, result) {
794
+ if (result._tag === "Failed")
795
+ return Effect8.fail(new PullRequestUnavailable({ diagnostic: result.diagnostic, url: ref.url }));
796
+ const { report } = result;
797
+ return Option12.match(report.membership, {
798
+ onNone: () => Effect8.fail(new StackIncomplete({ url: ref.url })),
799
+ onSome: (membership) => Effect8.succeed({
800
+ report,
801
+ stack: membership._tag === "Stack" ? membership.members : Arr6.of(ref)
802
+ })
803
+ });
804
+ }
805
+ function sessionLocks() {
806
+ const locks = new Map;
807
+ return (sessionID) => (effect) => Effect8.suspend(() => {
808
+ const lock = locks.get(sessionID) ?? Semaphore.makeUnsafe(1);
809
+ locks.set(sessionID, lock);
810
+ return lock.withPermit(effect);
811
+ });
1628
812
  }
813
+ function resolve({ github }, input, directory) {
814
+ return input._tag === "Reference" ? Effect8.succeed(input.ref) : github.pullRequestInRepository(directory, input.number);
815
+ }
816
+ var attachTo = Effect8.fn("Tracker.attach")(function* (services, sessionID, target) {
817
+ const ref = yield* resolve(services, target.input, target.directory);
818
+ const reports = yield* services.github.fetch([ref]);
819
+ const missing = { _tag: "Failed", diagnostic: "NotFound" };
820
+ const { report, stack } = yield* discovered(ref, reports.get(ref.url) ?? missing);
821
+ const current = yield* services.repository.load(sessionID);
822
+ const change = yield* Effect8.fromResult(attach(current, stack, yield* currentMillis));
823
+ if (change.changed)
824
+ yield* services.repository.save(sessionID, change.tracking);
825
+ return {
826
+ changed: change.changed,
827
+ ref,
828
+ report,
829
+ stackSize: stack.length,
830
+ tracking: change.tracking
831
+ };
832
+ });
833
+ var detachFrom = Effect8.fn("Tracker.detach")(function* ({ repository }, sessionID, input) {
834
+ const current = yield* repository.load(sessionID);
835
+ const removal = input._tag === "Reference" ? detach(current, input.ref) : yield* Effect8.fromResult(detachNumber(current, input.number));
836
+ if (Option12.isSome(removal.removed))
837
+ yield* repository.save(sessionID, removal.tracking);
838
+ return removal;
839
+ });
840
+ var layer5 = Layer5.effect(Tracker, Effect8.gen(function* () {
841
+ const services = { github: yield* GitHub, repository: yield* TrackingRepository };
842
+ const locked = sessionLocks();
843
+ return Tracker.of({
844
+ attach: (sessionID, input, directory) => locked(sessionID)(attachTo(services, sessionID, { directory, input })),
845
+ detach: (sessionID, input) => locked(sessionID)(detachFrom(services, sessionID, input)),
846
+ forget: (sessionID) => locked(sessionID)(services.repository.remove(sessionID)),
847
+ list: (sessionID) => services.repository.load(sessionID)
848
+ });
849
+ }));
1629
850
 
1630
- // src/state.ts
1631
- var maximumPullRequestsPerSession = 20;
1632
- var invalidStateFile = {
1633
- ok: false,
1634
- error: {
1635
- tag: "InvalidStateFile",
1636
- message: "The session pull request state file is invalid"
1637
- }
1638
- };
1639
- var lockStaleMilliseconds = 1e4;
1640
- var lockUpdateMilliseconds = 2000;
1641
- function stateUnavailable(operation, message, cause) {
1642
- return { tag: "StateUnavailable", operation, message, cause };
1643
- }
1644
- function isRecord(value) {
1645
- return value !== null && typeof value === "object" && !Array.isArray(value);
1646
- }
1647
- function hasExactKeys(value, keys) {
1648
- const actual = Object.keys(value);
1649
- return actual.length === keys.length && keys.every((key) => Object.hasOwn(value, key));
1650
- }
1651
- function parseState(input) {
1652
- if (!isRecord(input) || !hasExactKeys(input, ["version", "pullRequests"]))
1653
- return invalidStateFile;
1654
- if (input.version !== 1 || !Array.isArray(input.pullRequests))
1655
- return invalidStateFile;
1656
- if (input.pullRequests.length > maximumPullRequestsPerSession)
1657
- return invalidStateFile;
1658
- const attachments = [];
1659
- const seen = new Set;
1660
- for (const item of input.pullRequests) {
1661
- if (!isRecord(item) || !hasExactKeys(item, ["url", "attachedAt"]))
1662
- return invalidStateFile;
1663
- if (typeof item.url !== "string" || typeof item.attachedAt !== "string")
1664
- return invalidStateFile;
1665
- const parsed = parsePullRequestUrl(item.url);
1666
- if (!parsed.ok || parsed.value.url !== item.url || seen.has(item.url))
1667
- return invalidStateFile;
1668
- const attachedAt = new Date(item.attachedAt);
1669
- if (Number.isNaN(attachedAt.valueOf()) || attachedAt.toISOString() !== item.attachedAt)
1670
- return invalidStateFile;
1671
- seen.add(item.url);
1672
- attachments.push({ pullRequest: parsed.value, attachedAt: item.attachedAt });
1673
- }
1674
- return { ok: true, value: attachments };
1675
- }
1676
- function isMissingFile(cause) {
1677
- return cause instanceof Error && "code" in cause && cause.code === "ENOENT";
1678
- }
1679
- function fileName(sessionID) {
1680
- return `${createHash("sha256").update(sessionID).digest("hex")}.json`;
1681
- }
1682
- function defaultStateDirectory(environment = process.env, home = homedir()) {
1683
- const dataHome = environment.XDG_DATA_HOME || join(home, ".local", "share");
1684
- return join(dataHome, "opencode", "opencode-pr-tracker");
1685
- }
1686
- function createStateStore(options = {}) {
1687
- const directory = options.directory ?? defaultStateDirectory();
1688
- const now = options.now ?? (() => new Date);
1689
- async function acquireLock(sessionID) {
1690
- const stateFile = join(directory, fileName(sessionID));
1691
- let compromised;
1692
- try {
1693
- await mkdir(directory, { recursive: true });
1694
- const release = await import_proper_lockfile.lock(stateFile, {
1695
- realpath: false,
1696
- stale: lockStaleMilliseconds,
1697
- update: lockUpdateMilliseconds,
1698
- retries: { retries: 50, factor: 1, minTimeout: 10, maxTimeout: 100 },
1699
- onCompromised: (error) => {
1700
- compromised = error;
1701
- }
1702
- });
1703
- return { ok: true, value: { release, compromised: () => compromised } };
1704
- } catch (cause) {
1705
- return {
1706
- ok: false,
1707
- error: stateUnavailable("write", "Unable to lock the session pull request state", cause)
1708
- };
1709
- }
1710
- }
1711
- async function withLock(sessionID, operation) {
1712
- const lock = await acquireLock(sessionID);
1713
- if (!lock.ok)
1714
- return lock;
1715
- let result;
1716
- try {
1717
- result = await operation();
1718
- } catch (cause) {
1719
- await lock.value.release().catch(() => {
1720
- return;
1721
- });
1722
- throw cause;
1723
- }
1724
- try {
1725
- await lock.value.release();
1726
- } catch (cause) {
1727
- return {
1728
- ok: false,
1729
- error: stateUnavailable("write", "Unable to unlock the session pull request state", cause)
1730
- };
1731
- }
1732
- const compromise = lock.value.compromised();
1733
- if (compromise !== undefined) {
1734
- return {
1735
- ok: false,
1736
- error: stateUnavailable("write", "The session pull request state lock was compromised", compromise)
1737
- };
851
+ // src/application/Monitor.ts
852
+ class Monitor extends Context6.Service()("opencode-pr-tracker/Monitor") {
853
+ }
854
+ function entryOf(known, attachment) {
855
+ const current = known.get(attachment.ref.url) ?? unknown;
856
+ return {
857
+ attachedAt: attachment.attachedAt,
858
+ membership: current.membership,
859
+ ref: attachment.ref,
860
+ status: current.status
861
+ };
862
+ }
863
+ var urlsOf = (tracking) => tracking.map((attachment) => attachment.ref.url);
864
+ function remember(cache, results) {
865
+ return Effect9.gen(function* () {
866
+ const now = yield* currentMillis;
867
+ yield* Ref2.update(cache.known, (current) => {
868
+ const next = new Map(current);
869
+ for (const [url, result] of results)
870
+ next.set(url, afterRefresh(current.get(url) ?? unknown, result, now));
871
+ return next;
872
+ });
873
+ });
874
+ }
875
+ function update(cache, refs) {
876
+ return Effect9.gen(function* () {
877
+ const outcome = yield* Effect9.result(cache.github.fetch(refs));
878
+ const results = Result7.isSuccess(outcome) ? outcome.success : new Map(refs.map((ref) => [ref.url, { _tag: "Failed", diagnostic: outcome.failure.diagnostic }]));
879
+ yield* remember(cache, results);
880
+ });
881
+ }
882
+ function viewOf(state, sessionID) {
883
+ return Effect9.gen(function* () {
884
+ const tracking = yield* state.tracker.list(sessionID);
885
+ const current = yield* Ref2.get(state.known);
886
+ return { entries: tracking.map((attachment) => entryOf(current, attachment)), sessionID };
887
+ });
888
+ }
889
+ function publish(state, sessionID) {
890
+ return viewOf(state, sessionID).pipe(Effect9.flatMap((view) => PubSub.publish(state.published, view)), Effect9.ignore);
891
+ }
892
+ var use = (state, sessionID) => Ref2.update(state.working, (sessions) => new Set(sessions).add(sessionID));
893
+ function poll(state) {
894
+ return Effect9.gen(function* () {
895
+ const sessions = [...yield* Ref2.get(state.working)];
896
+ const known = yield* Ref2.get(state.known);
897
+ const trackings = yield* Effect9.forEach(sessions, (sessionID) => state.tracker.list(sessionID).pipe(Effect9.orElseSucceed(() => [])));
898
+ const attached = new Map(trackings.flat().map((attachment) => [attachment.ref.url, attachment.ref]));
899
+ const now = yield* currentMillis;
900
+ const current = yield* Ref2.get(state.known);
901
+ const due = [...attached.values()].filter((ref) => isDue(current, now, ref));
902
+ yield* Ref2.update(state.known, (entries) => withoutUnattached(entries, known, attached));
903
+ if (due.length === 0)
904
+ return;
905
+ yield* state.fetch(due);
906
+ const dueUrls = new Set(due.map((ref) => ref.url));
907
+ const affected = sessions.filter((_, index) => urlsOf(trackings[index] ?? []).some((url) => dueUrls.has(url)));
908
+ yield* Effect9.forEach(affected, (sessionID) => publish(state, sessionID), {
909
+ discard: true
910
+ });
911
+ });
912
+ }
913
+ function fetchAndShow(state, sessionID, select) {
914
+ return Effect9.gen(function* () {
915
+ yield* use(state, sessionID);
916
+ const tracking = yield* state.tracker.list(sessionID);
917
+ const known = yield* Ref2.get(state.known);
918
+ yield* state.fetch(tracking.map((attachment) => attachment.ref).filter((ref) => select(Option13.fromNullishOr(known.get(ref.url)))));
919
+ const view = yield* viewOf(state, sessionID);
920
+ yield* PubSub.publish(state.published, view);
921
+ return view;
922
+ });
923
+ }
924
+ var refreshable = (known) => Option13.isSome(Option13.getOrElse(known, () => unknown).dueAt);
925
+ var notYetKnown = (known) => Option13.isNone(known);
926
+ var layer6 = Layer6.effect(Monitor, Effect9.gen(function* () {
927
+ const cache = {
928
+ github: yield* GitHub,
929
+ known: yield* Ref2.make(new Map)
930
+ };
931
+ const queue = new FetchQueue((refs) => update(cache, refs), yield* Effect9.scope);
932
+ const state = {
933
+ fetch: (refs) => queue.fetch(refs),
934
+ github: cache.github,
935
+ known: cache.known,
936
+ published: yield* PubSub.unbounded(),
937
+ tracker: yield* Tracker,
938
+ working: yield* Ref2.make(new Set)
939
+ };
940
+ return Monitor.of({
941
+ changes: Stream.fromPubSub(state.published),
942
+ forget: (sessionID) => Ref2.update(state.working, (sessions) => new Set([...sessions].filter((id) => id !== sessionID))),
943
+ poll: poll(state),
944
+ attached: (sessionID, ref, report) => Effect9.andThen(remember(cache, new Map([[ref.url, { _tag: "Reported", report }]])), fetchAndShow(state, sessionID, notYetKnown)),
945
+ refresh: (sessionID) => fetchAndShow(state, sessionID, refreshable),
946
+ show: (sessionID) => fetchAndShow(state, sessionID, notYetKnown),
947
+ view: (sessionID) => Effect9.andThen(use(state, sessionID), viewOf(state, sessionID))
948
+ });
949
+ }));
950
+
951
+ // src/rpc.ts
952
+ import { Rpc } from "@opencode/plugin/rpc";
953
+ import { Schema as Schema13 } from "effect";
954
+
955
+ // src/domain/StackLayout.ts
956
+ import { Array as Arr7, Option as Option14, Schema as Schema12 } from "effect";
957
+ var Membership = Schema12.Union([
958
+ Schema12.TaggedStruct("Standalone", {}),
959
+ Schema12.TaggedStruct("Stack", {
960
+ id: Schema12.String,
961
+ members: Schema12.NonEmptyArray(PullRequestRef)
962
+ })
963
+ ]);
964
+ var stackOf = (entry) => Option14.filter(entry.membership, (membership) => membership._tag === "Stack");
965
+ var urlsOf2 = (stack) => stack.members.map((member) => member.url);
966
+ function reportsById(entries) {
967
+ const byId = new Map;
968
+ for (const [index, entry] of entries.entries()) {
969
+ for (const stack of Option14.toArray(stackOf(entry))) {
970
+ byId.set(stack.id, [...byId.get(stack.id) ?? [], { index, stack, url: entry.ref.url }]);
1738
971
  }
1739
- return result;
1740
972
  }
1741
- async function readExisting(sessionID) {
1742
- const path = join(directory, fileName(sessionID));
1743
- let content;
1744
- try {
1745
- content = await readFile(path, "utf8");
1746
- } catch (cause) {
1747
- if (isMissingFile(cause))
1748
- return { ok: true, value: undefined };
1749
- return {
1750
- ok: false,
1751
- error: stateUnavailable("read", "Unable to read the session pull request state", cause)
1752
- };
1753
- }
1754
- let decoded;
1755
- try {
1756
- decoded = JSON.parse(content);
1757
- } catch {
1758
- return invalidStateFile;
973
+ return byId;
974
+ }
975
+ function claims(byId) {
976
+ const claimed = new Map;
977
+ for (const [id, reports] of byId) {
978
+ for (const url of reports.flatMap((report) => urlsOf2(report.stack))) {
979
+ claimed.set(url, new Set([...claimed.get(url) ?? [], id]));
1759
980
  }
1760
- return parseState(decoded);
1761
- }
1762
- async function read(sessionID) {
1763
- const result = await readExisting(sessionID);
1764
- if (!result.ok)
1765
- return result;
1766
- return { ok: true, value: result.value ?? [] };
1767
981
  }
1768
- async function write(sessionID, attachments) {
1769
- const destination = join(directory, fileName(sessionID));
1770
- const temporary = `${destination}.${process.pid}.${randomUUID()}.tmp`;
1771
- const state = {
1772
- version: 1,
1773
- pullRequests: attachments.map((attachment) => ({
1774
- url: attachment.pullRequest.url,
1775
- attachedAt: attachment.attachedAt
1776
- }))
1777
- };
1778
- try {
1779
- await mkdir(directory, { recursive: true });
1780
- await writeFile(temporary, `${JSON.stringify(state, null, 2)}
1781
- `, { mode: 384 });
1782
- await rename(temporary, destination);
1783
- return { ok: true, value: undefined };
1784
- } catch (cause) {
1785
- await rm(temporary, { force: true }).catch(() => {
1786
- return;
1787
- });
1788
- return {
1789
- ok: false,
1790
- error: stateUnavailable("write", "Unable to write the session pull request state", cause)
1791
- };
982
+ return claimed;
983
+ }
984
+ function agrees(reports, entries, claimed) {
985
+ const members = Option14.match(Arr7.head(reports), {
986
+ onNone: () => [],
987
+ onSome: (report) => urlsOf2(report.stack)
988
+ });
989
+ const listed = new Set(members);
990
+ const attachedMembers = entries.filter((entry) => listed.has(entry.ref.url));
991
+ return listed.size === members.length && reports.every((report) => urlsOf2(report.stack).join(`
992
+ `) === members.join(`
993
+ `)) && members.every((url) => (claimed.get(url) ?? new Set).size === 1) && attachedMembers.length === reports.length;
994
+ }
995
+ function placements(reports) {
996
+ const placed = reports.map((report) => ({
997
+ index: report.index,
998
+ position: urlsOf2(report.stack).indexOf(report.url),
999
+ size: report.stack.members.length
1000
+ }));
1001
+ const adjacent = Arr7.zipWith(placed, placed.slice(1), (before, after) => after.index === before.index + 1 && after.position > before.position);
1002
+ const ordered = placed.every((member) => member.position >= 0) && adjacent.every(Boolean);
1003
+ return ordered ? Option14.some(placed) : Option14.none();
1004
+ }
1005
+ function consistentStacks(entries) {
1006
+ const byId = reportsById(entries);
1007
+ const claimed = claims(byId);
1008
+ const places = new Map;
1009
+ for (const reports of byId.values()) {
1010
+ const placed = agrees(reports, entries, claimed) ? placements(reports) : Option14.none();
1011
+ for (const group of Option14.toArray(placed)) {
1012
+ for (const [step, current] of group.entries()) {
1013
+ places.set(current.index, {
1014
+ attached: group.length,
1015
+ current,
1016
+ first: step === 0,
1017
+ last: step === group.length - 1,
1018
+ previous: Arr7.get(group, step - 1)
1019
+ });
1020
+ }
1792
1021
  }
1793
1022
  }
1794
- return {
1795
- list: read,
1796
- async attach(sessionID, pullRequest) {
1797
- return withLock(sessionID, async () => {
1798
- const current = await read(sessionID);
1799
- if (!current.ok)
1800
- return current;
1801
- if (current.value.some((attachment) => attachment.pullRequest.url === pullRequest.url)) {
1802
- return { ok: true, value: "already_attached" };
1803
- }
1804
- if (current.value.length >= maximumPullRequestsPerSession) {
1805
- return {
1806
- ok: false,
1807
- error: {
1808
- tag: "AttachmentLimitReached",
1809
- limit: maximumPullRequestsPerSession,
1810
- message: "A session can track at most 20 pull requests"
1811
- }
1812
- };
1813
- }
1814
- const written = await write(sessionID, [...current.value, { pullRequest, attachedAt: now().toISOString() }]);
1815
- if (!written.ok)
1816
- return written;
1817
- return { ok: true, value: "added" };
1818
- });
1023
+ return places;
1024
+ }
1025
+ function marker(place) {
1026
+ const { current } = place;
1027
+ if (place.attached === 1 && current.size > 1)
1028
+ return "middle";
1029
+ if (place.first && current.position === 0)
1030
+ return "first";
1031
+ if (place.last && current.position === current.size - 1)
1032
+ return "last";
1033
+ return "middle";
1034
+ }
1035
+ function connector(place) {
1036
+ if (!place.last)
1037
+ return "continues";
1038
+ return place.current.position < place.current.size - 1 ? "open" : "none";
1039
+ }
1040
+ function stackRows(entry, place) {
1041
+ const skipped = Option14.match(place.previous, {
1042
+ onNone: () => 0,
1043
+ onSome: (before) => place.current.position - before.position - 1
1044
+ });
1045
+ const row = {
1046
+ _tag: "PullRequest",
1047
+ connector: connector(place),
1048
+ entry,
1049
+ marker: marker(place)
1050
+ };
1051
+ return skipped > 0 ? [{ _tag: "Gap", count: skipped }, row] : [row];
1052
+ }
1053
+ function layout(entries) {
1054
+ const places = consistentStacks(entries);
1055
+ return entries.flatMap((entry, index) => Option14.match(Option14.fromNullishOr(places.get(index)), {
1056
+ onNone: () => [{ _tag: "PullRequest", connector: "none", entry, marker: "bullet" }],
1057
+ onSome: (place) => stackRows(entry, place)
1058
+ }));
1059
+ }
1060
+
1061
+ // src/rpc.ts
1062
+ var Layout = Schema13.Literals(["default", "compact"]);
1063
+ var EntryView = Schema13.Struct({
1064
+ attachedAt: Schema13.Int,
1065
+ membership: Schema13.NullOr(Membership),
1066
+ ref: PullRequestRef,
1067
+ status: Status
1068
+ });
1069
+ var View = Schema13.Struct({
1070
+ entries: Schema13.Array(EntryView),
1071
+ layout: Layout,
1072
+ sessionID: Schema13.String
1073
+ });
1074
+ var Session = Schema13.Struct({ sessionID: Schema13.String });
1075
+ var Target = Schema13.Struct({ sessionID: Schema13.String, target: Schema13.String });
1076
+ var Changed = Schema13.Struct({ message: Schema13.String, view: View });
1077
+ var Rejected = Schema13.Struct({ message: Schema13.String });
1078
+ function portable(schema) {
1079
+ return { "~standard": Schema13.toStandardSchemaV1(Schema13.toEncoded(schema))["~standard"] };
1080
+ }
1081
+ var PullRequestTracker = Rpc.define({
1082
+ events: {
1083
+ updated: { schema: portable(View) }
1084
+ },
1085
+ id: "opencode-pr-tracker",
1086
+ methods: {
1087
+ attach: {
1088
+ errors: { rejected: portable(Rejected) },
1089
+ input: portable(Target),
1090
+ output: portable(Changed)
1819
1091
  },
1820
- async detach(sessionID, pullRequest) {
1821
- return withLock(sessionID, async () => {
1822
- const current = await read(sessionID);
1823
- if (!current.ok)
1824
- return current;
1825
- const next = current.value.filter((attachment) => attachment.pullRequest.url !== pullRequest.url);
1826
- if (next.length === current.value.length)
1827
- return { ok: true, value: "absent" };
1828
- const written = await write(sessionID, next);
1829
- if (!written.ok)
1830
- return written;
1831
- return { ok: true, value: "removed" };
1832
- });
1092
+ detach: {
1093
+ errors: { rejected: portable(Rejected) },
1094
+ input: portable(Target),
1095
+ output: portable(Changed)
1833
1096
  },
1834
- async detachByNumber(sessionID, number) {
1835
- return withLock(sessionID, async () => {
1836
- const current = await read(sessionID);
1837
- if (!current.ok)
1838
- return current;
1839
- const matches = current.value.filter((attachment) => attachment.pullRequest.number === number);
1840
- if (matches.length === 0)
1841
- return { ok: true, value: { tag: "absent" } };
1842
- if (matches.length > 1) {
1843
- return {
1844
- ok: true,
1845
- value: { tag: "ambiguous", pullRequests: matches.map((attachment) => attachment.pullRequest) }
1846
- };
1847
- }
1848
- const match = matches[0];
1849
- if (match === undefined)
1850
- return { ok: true, value: { tag: "absent" } };
1851
- const next = current.value.filter((attachment) => attachment.pullRequest.url !== match.pullRequest.url);
1852
- const written = await write(sessionID, next);
1853
- if (!written.ok)
1854
- return written;
1855
- return { ok: true, value: { tag: "removed", pullRequest: match.pullRequest } };
1856
- });
1097
+ list: {
1098
+ errors: { rejected: portable(Rejected) },
1099
+ input: portable(Session),
1100
+ output: portable(View)
1857
1101
  },
1858
- async removeSession(sessionID) {
1859
- return withLock(sessionID, async () => {
1860
- const current = await readExisting(sessionID);
1861
- if (!current.ok)
1862
- return current;
1863
- if (current.value === undefined)
1864
- return { ok: true, value: "absent" };
1865
- try {
1866
- await rm(join(directory, fileName(sessionID)), { force: true });
1867
- return { ok: true, value: "removed" };
1868
- } catch (cause) {
1869
- return {
1870
- ok: false,
1871
- error: stateUnavailable("write", "Unable to remove the session pull request state", cause)
1872
- };
1873
- }
1874
- });
1102
+ refresh: {
1103
+ errors: { rejected: portable(Rejected) },
1104
+ input: portable(Session),
1105
+ output: portable(View)
1875
1106
  }
1876
- };
1877
- }
1878
-
1879
- // src/server.ts
1880
- class PrToolError extends Error {
1881
- code;
1882
- name = "PrToolError";
1883
- constructor(code, message) {
1884
- super(message);
1885
- this.code = code;
1886
1107
  }
1108
+ });
1109
+
1110
+ // src/server/Requests.ts
1111
+ import { Effect as Effect10, Option as Option16 } from "effect";
1112
+
1113
+ // src/messages.ts
1114
+ import { Match as Match2, Option as Option15 } from "effect";
1115
+ var diagnosticMessages = {
1116
+ AuthenticationRequired: "GitHub needs you to sign in: run `gh auth login`, or set GH_TOKEN.",
1117
+ GitHubCliMissing: "Install the GitHub CLI (`gh`), or set GH_TOKEN.",
1118
+ GitHubUnavailable: "GitHub is not responding right now. Try again shortly.",
1119
+ InvalidResponse: "GitHub returned a response the tracker could not read.",
1120
+ NotFound: "The pull request does not exist, or your GitHub account cannot see it."
1121
+ };
1122
+ var list = (refs) => {
1123
+ const labels = refs.map((ref) => ref.label);
1124
+ return labels.length <= 2 ? labels.join(" and ") : `${labels.slice(0, -1).join(", ")}, and ${labels.at(-1) ?? ""}`;
1125
+ };
1126
+ function failureMessage(failure) {
1127
+ return Match2.valueTags(failure, {
1128
+ AmbiguousPullRequestNumber: ({ matches, number }) => `#${String(number)} matches ${list(matches)}. Use the pull request URL instead.`,
1129
+ AttachmentLimitReached: ({ limit, requested }) => `A session can track at most ${String(limit)} pull requests; this would make ${String(requested)}.`,
1130
+ GitHubFailure: ({ diagnostic }) => diagnosticMessages[diagnostic],
1131
+ InvalidPullRequestInput: () => "Expected a pull request URL, such as github.com/owner/repository/pull/123, or a pull request number.",
1132
+ PullRequestUnavailable: ({ diagnostic, url }) => `${url}: ${diagnosticMessages[diagnostic]}`,
1133
+ RepositoryUnavailable: () => "This session's directory is not a GitHub repository that `gh` can see. Use the pull request URL instead.",
1134
+ StackIncomplete: ({ url }) => `GitHub returned only part of the Stack of ${url}, so nothing was attached. Try again later.`,
1135
+ StoredStateInvalid: () => "The tracker's saved state for this session cannot be read. It has been left unchanged."
1136
+ });
1887
1137
  }
1888
- function toToolError(failure) {
1889
- return new PrToolError(failure.tag, failure.message);
1138
+ function attachedMessage(attached) {
1139
+ const label = attached.ref.label;
1140
+ if (!attached.changed)
1141
+ return `${label} is already attached.`;
1142
+ return attached.stackSize > 1 ? `Attached ${label} with the rest of its Stack (${String(attached.stackSize)} pull requests).` : `Attached ${label}.`;
1890
1143
  }
1891
- function formatReferenceList(references) {
1892
- if (references.length < 2)
1893
- return references.join("");
1894
- if (references.length === 2)
1895
- return references.join(" and ");
1896
- return `${references.slice(0, -1).join(", ")}, and ${references.at(-1)}`;
1144
+ function detachedMessage(removal, target) {
1145
+ return Option15.match(removal.removed, {
1146
+ onNone: () => `${target} is not attached.`,
1147
+ onSome: (ref) => `Detached ${ref.label}.`
1148
+ });
1897
1149
  }
1898
- function createServerHooks(store) {
1150
+
1151
+ // src/server/Requests.ts
1152
+ function toView(view, layout) {
1899
1153
  return {
1900
- async event({ event }) {
1901
- if (event.type !== "session.deleted")
1902
- return;
1903
- const result = await store.removeSession(event.properties.info.id);
1904
- if (!result.ok)
1905
- throw toToolError(result.error);
1906
- },
1907
- tool: {
1908
- pr_attach: tool({
1909
- description: "Attach a canonical GitHub pull request URL to the current OpenCode session.",
1910
- args: {
1911
- url: tool.schema.string().describe("A https://github.com/<owner>/<repository>/pull/<number> URL")
1912
- },
1913
- async execute(args, context) {
1914
- const pullRequest = parsePullRequestUrl(args.url);
1915
- if (!pullRequest.ok)
1916
- throw new PrToolError(pullRequest.error.tag, pullRequest.error.message);
1917
- const result = await store.attach(context.sessionID, pullRequest.value);
1918
- if (!result.ok)
1919
- throw toToolError(result.error);
1920
- const reference = formatPullRequestRef(pullRequest.value);
1921
- return result.value === "added" ? `Attached ${reference} to this session.` : `${reference} is already attached to this session.`;
1922
- }
1923
- }),
1924
- pr_detach: tool({
1925
- description: "Detach a pull request from the current OpenCode session by positive number or canonical URL.",
1926
- args: {
1927
- pull_request: tool.schema.union([tool.schema.number().int().positive().max(Number.MAX_SAFE_INTEGER), tool.schema.string()]).describe("A positive pull request number or https://github.com/<owner>/<repository>/pull/<number> URL")
1928
- },
1929
- async execute(args, context) {
1930
- if (typeof args.pull_request === "number") {
1931
- if (!Number.isSafeInteger(args.pull_request) || args.pull_request <= 0) {
1932
- throw new PrToolError("InvalidPullRequestNumber", "Expected a positive pull request number or canonical GitHub URL");
1933
- }
1934
- const result2 = await store.detachByNumber(context.sessionID, args.pull_request);
1935
- if (!result2.ok)
1936
- throw toToolError(result2.error);
1937
- if (result2.value.tag === "absent") {
1938
- return `Pull request #${args.pull_request} is not attached to this session.`;
1939
- }
1940
- if (result2.value.tag === "ambiguous") {
1941
- const references = result2.value.pullRequests.map(formatPullRequestRef);
1942
- throw new PrToolError("AmbiguousPullRequestNumber", `Pull request #${args.pull_request} matches ${formatReferenceList(references)}. Use a canonical GitHub pull request URL.`);
1943
- }
1944
- return `Detached ${formatPullRequestRef(result2.value.pullRequest)} from this session.`;
1945
- }
1946
- const pullRequest = parsePullRequestUrl(args.pull_request);
1947
- if (!pullRequest.ok)
1948
- throw new PrToolError(pullRequest.error.tag, pullRequest.error.message);
1949
- const result = await store.detach(context.sessionID, pullRequest.value);
1950
- if (!result.ok)
1951
- throw toToolError(result.error);
1952
- const reference = formatPullRequestRef(pullRequest.value);
1953
- return result.value === "removed" ? `Detached ${reference} from this session.` : `${reference} is not attached to this session.`;
1954
- }
1955
- })
1956
- }
1154
+ entries: view.entries.map((entry) => ({
1155
+ attachedAt: entry.attachedAt,
1156
+ membership: Option16.getOrNull(entry.membership),
1157
+ ref: entry.ref,
1158
+ status: entry.status
1159
+ })),
1160
+ layout,
1161
+ sessionID: view.sessionID
1957
1162
  };
1958
1163
  }
1959
- var plugin = {
1960
- id: "opencode-pr-tracker",
1961
- server: async () => createServerHooks(createStateStore())
1164
+ var parseTarget = (target) => Effect10.fromResult(parsePullRequestInput(target));
1165
+ function requests({ monitor, tracker }, settings) {
1166
+ return {
1167
+ attach: (sessionID, target) => Effect10.gen(function* () {
1168
+ const attached = yield* tracker.attach(sessionID, yield* parseTarget(target), settings.directory);
1169
+ const view = yield* monitor.attached(sessionID, attached.ref, attached.report);
1170
+ return { message: attachedMessage(attached), view: toView(view, settings.layout) };
1171
+ }),
1172
+ detach: (sessionID, target) => Effect10.gen(function* () {
1173
+ const removal = yield* tracker.detach(sessionID, yield* parseTarget(target));
1174
+ const view = yield* monitor.show(sessionID);
1175
+ return { message: detachedMessage(removal, target), view: toView(view, settings.layout) };
1176
+ })
1177
+ };
1178
+ }
1179
+
1180
+ // src/server/Rpc.ts
1181
+ import { Effect as Effect11 } from "effect";
1182
+ function handlers(services, settings) {
1183
+ const viewOf = (view) => toView(view, settings.layout);
1184
+ const { attach, detach } = requests(services, settings);
1185
+ return {
1186
+ attach: ({ sessionID, target }, context) => attach(sessionID, target).pipe(Effect11.mapError(failureMessage), Effect11.mapError((message) => context.error("rejected", message, { message }))),
1187
+ detach: ({ sessionID, target }, context) => detach(sessionID, target).pipe(Effect11.mapError(failureMessage), Effect11.mapError((message) => context.error("rejected", message, { message }))),
1188
+ list: ({ sessionID }, context) => services.monitor.view(sessionID).pipe(Effect11.map(viewOf), Effect11.mapError(failureMessage), Effect11.mapError((message) => context.error("rejected", message, { message }))),
1189
+ refresh: ({ sessionID }, context) => services.monitor.refresh(sessionID).pipe(Effect11.map(viewOf), Effect11.mapError(failureMessage), Effect11.mapError((message) => context.error("rejected", message, { message })))
1190
+ };
1191
+ }
1192
+
1193
+ // src/server/Tools.ts
1194
+ import { Tool } from "@opencode/schema/tool";
1195
+ import { Effect as Effect12, Schema as Schema14 } from "effect";
1196
+
1197
+ // src/domain/Appearance.ts
1198
+ import { Match as Match3 } from "effect";
1199
+ var diagnosticLabels = {
1200
+ AuthenticationRequired: "authenticate",
1201
+ GitHubCliMissing: "install gh",
1202
+ GitHubUnavailable: "GitHub unavailable",
1203
+ InvalidResponse: "invalid response",
1204
+ NotFound: "inaccessible"
1962
1205
  };
1963
- var server_default = plugin;
1206
+ var shown = (tone, label, strikethrough = false) => ({
1207
+ label,
1208
+ stale: false,
1209
+ strikethrough,
1210
+ tone
1211
+ });
1212
+ function openAppearance(open) {
1213
+ if (open.mergeability === "conflicting")
1214
+ return shown("red", "conflict");
1215
+ if (open.ci === "failed")
1216
+ return shown("red", "failed");
1217
+ if (open.draft)
1218
+ return shown("gray", "draft");
1219
+ if (open.ci === "pending")
1220
+ return shown("yellow", "pending");
1221
+ if (open.behind)
1222
+ return shown("yellow", "behind");
1223
+ return open.ci === "passed" ? shown("green", "passed") : shown("gray", "no checks");
1224
+ }
1225
+ function stateAppearance(state) {
1226
+ return Match3.valueTags(state, {
1227
+ Closed: () => shown("red", "closed", true),
1228
+ Merged: () => shown("purple", "merged", true),
1229
+ Open: openAppearance
1230
+ });
1231
+ }
1232
+ function markStale(fresh) {
1233
+ return { label: fresh.label, stale: true, strikethrough: fresh.strikethrough, tone: fresh.tone };
1234
+ }
1235
+ function appearance(status) {
1236
+ return Match3.valueTags(status, {
1237
+ Fresh: ({ snapshot }) => stateAppearance(snapshot.state),
1238
+ Pending: () => shown("gray", "unavailable"),
1239
+ Stale: ({ snapshot }) => markStale(stateAppearance(snapshot.state)),
1240
+ Unavailable: ({ diagnostic }) => shown("gray", diagnosticLabels[diagnostic])
1241
+ });
1242
+ }
1243
+
1244
+ // src/server/Tools.ts
1245
+ var options = { codemode: true, namespace: "pr", pinned: true };
1246
+ var PullRequestArgument = Schema14.Struct({
1247
+ pull_request: Schema14.Union([Schema14.String, Schema14.Int]).annotate({
1248
+ description: "A pull request URL, such as github.com/owner/repository/pull/123, or a number in this repository"
1249
+ })
1250
+ });
1251
+ var targetOf = ({ pull_request }) => String(pull_request);
1252
+ var NoArguments = Schema14.Struct({});
1253
+ var asToolError = (failure) => new Tool.Error({ message: failureMessage(failure) });
1254
+ var listTool = ({ monitor }) => ({
1255
+ description: "List the pull requests attached to this session, with their status.",
1256
+ execute: (_input, context) => monitor.view(context.sessionID).pipe(Effect12.map((view) => ({
1257
+ content: view.entries.length === 0 ? "No pull requests are attached to this session." : view.entries.map((entry) => `- ${entry.ref.url} (${appearance(entry.status).label})`).join(`
1258
+ `)
1259
+ })), Effect12.mapError(asToolError)),
1260
+ input: NoArguments,
1261
+ name: "list",
1262
+ options
1263
+ });
1264
+ var changeTool = (name, description, change) => ({
1265
+ description,
1266
+ execute: (argument, context) => change(context.sessionID, targetOf(argument)).pipe(Effect12.map((changed) => ({ content: changed.message })), Effect12.mapError(asToolError)),
1267
+ input: PullRequestArgument,
1268
+ name,
1269
+ options
1270
+ });
1271
+ function registerTools(tools, services, settings) {
1272
+ return tools.transform((editor) => {
1273
+ editor.namespace({
1274
+ description: "Pull requests attached to this session and shown in its sidebar",
1275
+ name: "pr"
1276
+ });
1277
+ const changes = requests(services, settings);
1278
+ editor.add(listTool(services));
1279
+ editor.add(changeTool("attach", "Attach a pull request to this session. Attaching a GitHub Stack member attaches the whole Stack.", changes.attach));
1280
+ editor.add(changeTool("detach", "Detach a pull request from this session. Other members of its Stack stay attached.", changes.detach));
1281
+ });
1282
+ }
1283
+
1284
+ // src/server.ts
1285
+ var pollInterval = "1 second";
1286
+ var Options = Schema15.Struct({ layout: Layout });
1287
+ var layoutOf = (options) => Option17.match(Schema15.decodeUnknownOption(Options)(options), {
1288
+ onNone: () => "default",
1289
+ onSome: ({ layout }) => layout
1290
+ });
1291
+ var forgetDeletedSessions = (ctx, services) => ctx.event.subscribe().pipe(Stream2.runForEach((event) => event.type === "session.deleted" ? Effect13.andThen(services.tracker.forget(event.data.sessionID), services.monitor.forget(event.data.sessionID)) : Effect13.void), Effect13.ignore);
1292
+ var server_default = Plugin.define({
1293
+ effect: (ctx) => Effect13.gen(function* () {
1294
+ const application = layer6.pipe(Layer7.provideMerge(layer5), Layer7.provide([live, layer4(ctx.storage)]));
1295
+ const context = yield* Layer7.build(application);
1296
+ const services = {
1297
+ monitor: Context7.get(context, Monitor),
1298
+ tracker: Context7.get(context, Tracker)
1299
+ };
1300
+ const settings = {
1301
+ directory: ctx.location.directory,
1302
+ layout: layoutOf(ctx.options)
1303
+ };
1304
+ const registration = yield* ctx.rpc.register(PullRequestTracker, handlers(services, settings)).pipe(Effect13.orDie);
1305
+ yield* registerTools(ctx.tool, services, settings);
1306
+ yield* Effect13.forkScoped(forgetDeletedSessions(ctx, services));
1307
+ yield* Effect13.forkScoped(Effect13.repeat(services.monitor.poll, Schedule.spaced(pollInterval)));
1308
+ yield* services.monitor.changes.pipe(Stream2.runForEach((view) => registration.events.emit("updated", toView(view, settings.layout)).pipe(Effect13.ignore)), Effect13.forkScoped);
1309
+ }),
1310
+ id: "opencode-pr-tracker"
1311
+ });
1964
1312
  export {
1965
- server_default as default,
1966
- createServerHooks,
1967
- PrToolError
1313
+ server_default as default
1968
1314
  };
1969
1315
 
1970
- //# debugId=8B547DD45DB1BAE364756E2164756E21
1316
+ //# debugId=2C26781E1C009A7864756E2164756E21