opencode-forgekeeper 2.1.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/index.js +179 -152
- package/dist/tui.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -148,6 +148,11 @@ reuses a branch's PR/MR number for up to 10 minutes.
|
|
|
148
148
|
Explicit targets that aren't PRs/MRs, such as issues, show no status. Unresolved explicit
|
|
149
149
|
PRs/MRs never fall back to the branch.
|
|
150
150
|
|
|
151
|
+
Without a PR/MR to show, the footer stays empty, even when a lookup fails, such as in a new
|
|
152
|
+
session before the server answers. It shows `PR/MR status unavailable` only when none of
|
|
153
|
+
the session's explicit PR/MR targets can be looked up or found. `/forge-status` explains
|
|
154
|
+
why.
|
|
155
|
+
|
|
151
156
|
The server runs at most four `glab` or `gh` commands at once for status and review
|
|
152
157
|
feedback, across all sessions. When some target lookups fail, the footer shows the others
|
|
153
158
|
and counts the failures as `N unavailable`. A target whose lookup failed with an error
|
package/dist/index.js
CHANGED
|
@@ -1255,6 +1255,175 @@ function createFeedbackWatcher(options) {
|
|
|
1255
1255
|
};
|
|
1256
1256
|
}
|
|
1257
1257
|
|
|
1258
|
+
// packages/forgekeeper/src/store.ts
|
|
1259
|
+
class TargetLookupError extends Error {
|
|
1260
|
+
cause;
|
|
1261
|
+
constructor(cause) {
|
|
1262
|
+
super(cause instanceof Error ? cause.message : String(cause));
|
|
1263
|
+
this.cause = cause;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
function createStatusStore(options) {
|
|
1267
|
+
const now = options.now ?? Date.now;
|
|
1268
|
+
const setTimer = options.setTimer ?? ((callback, delay) => setTimeout(callback, delay));
|
|
1269
|
+
const clearTimer = options.clearTimer ?? ((timer) => clearTimeout(timer));
|
|
1270
|
+
const interval = options.interval ?? 120000;
|
|
1271
|
+
const maxBackoff = options.maxBackoff ?? 900000;
|
|
1272
|
+
const minEventInterval = options.minEventInterval ?? 15000;
|
|
1273
|
+
const entries = new Map;
|
|
1274
|
+
let disposed = false;
|
|
1275
|
+
let revision = now() * 1000;
|
|
1276
|
+
const entry = (key2) => {
|
|
1277
|
+
let value = entries.get(key2);
|
|
1278
|
+
if (!value) {
|
|
1279
|
+
value = { snapshot: { loading: false }, refs: 0, failures: 0 };
|
|
1280
|
+
entries.set(key2, value);
|
|
1281
|
+
}
|
|
1282
|
+
return value;
|
|
1283
|
+
};
|
|
1284
|
+
const update = (key2, value, patch) => {
|
|
1285
|
+
value.snapshot = { ...value.snapshot, ...patch, revision: ++revision };
|
|
1286
|
+
if (!disposed)
|
|
1287
|
+
options.onChange(key2);
|
|
1288
|
+
};
|
|
1289
|
+
const cancelTimer = (value) => {
|
|
1290
|
+
if (value.timer === undefined)
|
|
1291
|
+
return;
|
|
1292
|
+
clearTimer(value.timer);
|
|
1293
|
+
value.timer = undefined;
|
|
1294
|
+
};
|
|
1295
|
+
const baseInterval = (value) => options.active?.(value.snapshot.lookup) ? options.activeInterval ?? interval : interval;
|
|
1296
|
+
const pollDelay = (value) => Math.min(baseInterval(value) * 2 ** value.failures, maxBackoff);
|
|
1297
|
+
const lastAttempt = (value) => Math.max(value.snapshot.fetchedAt ?? -Infinity, value.snapshot.error?.at ?? -Infinity);
|
|
1298
|
+
const schedule = (key2, value, delay = pollDelay(value)) => {
|
|
1299
|
+
cancelTimer(value);
|
|
1300
|
+
if (disposed || value.refs === 0)
|
|
1301
|
+
return;
|
|
1302
|
+
const lookup = value.snapshot.lookup;
|
|
1303
|
+
if (!value.snapshot.error && (lookup?.kind !== "found" || lookup.requests.length === 0))
|
|
1304
|
+
return;
|
|
1305
|
+
value.timer = setTimer(() => {
|
|
1306
|
+
value.timer = undefined;
|
|
1307
|
+
refresh(key2);
|
|
1308
|
+
}, delay);
|
|
1309
|
+
};
|
|
1310
|
+
function refresh(key2) {
|
|
1311
|
+
if (disposed)
|
|
1312
|
+
return Promise.resolve({ loading: false });
|
|
1313
|
+
const value = entry(key2);
|
|
1314
|
+
if (value.inflight)
|
|
1315
|
+
return value.inflight;
|
|
1316
|
+
value.stale = false;
|
|
1317
|
+
update(key2, value, { loading: true });
|
|
1318
|
+
value.inflight = options.load(key2).then((lookup) => {
|
|
1319
|
+
if (disposed || value.invalidated)
|
|
1320
|
+
return;
|
|
1321
|
+
const previous = value.snapshot.lookup;
|
|
1322
|
+
value.failures = 0;
|
|
1323
|
+
update(key2, value, {
|
|
1324
|
+
lookup,
|
|
1325
|
+
fetchedAt: now(),
|
|
1326
|
+
error: undefined,
|
|
1327
|
+
targeted: lookup.explicitTarget === true ? true : undefined,
|
|
1328
|
+
loading: false
|
|
1329
|
+
});
|
|
1330
|
+
try {
|
|
1331
|
+
if (!disposed)
|
|
1332
|
+
options.onLoad?.(key2, previous, lookup);
|
|
1333
|
+
} catch (error) {
|
|
1334
|
+
try {
|
|
1335
|
+
options.onLoadError?.(error);
|
|
1336
|
+
} catch {}
|
|
1337
|
+
}
|
|
1338
|
+
}, (error) => {
|
|
1339
|
+
if (disposed || value.invalidated)
|
|
1340
|
+
return;
|
|
1341
|
+
value.failures++;
|
|
1342
|
+
const target2 = error instanceof TargetLookupError;
|
|
1343
|
+
const cause = target2 ? error.cause : error;
|
|
1344
|
+
update(key2, value, {
|
|
1345
|
+
loading: false,
|
|
1346
|
+
error: {
|
|
1347
|
+
kind: cause instanceof ForgeError ? cause.kind : "request",
|
|
1348
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
1349
|
+
at: now()
|
|
1350
|
+
},
|
|
1351
|
+
...target2 ? { targeted: true } : {}
|
|
1352
|
+
});
|
|
1353
|
+
}).then(() => {
|
|
1354
|
+
value.inflight = undefined;
|
|
1355
|
+
if (value.invalidated && !disposed) {
|
|
1356
|
+
value.invalidated = false;
|
|
1357
|
+
return refresh(key2);
|
|
1358
|
+
}
|
|
1359
|
+
schedule(key2, value);
|
|
1360
|
+
return value.snapshot;
|
|
1361
|
+
});
|
|
1362
|
+
return value.inflight;
|
|
1363
|
+
}
|
|
1364
|
+
return {
|
|
1365
|
+
get(key2) {
|
|
1366
|
+
return entries.get(key2)?.snapshot ?? { loading: false };
|
|
1367
|
+
},
|
|
1368
|
+
acquire(key2) {
|
|
1369
|
+
const value = entry(key2);
|
|
1370
|
+
value.refs++;
|
|
1371
|
+
if (value.refs === 1 && !value.inflight) {
|
|
1372
|
+
const remaining = lastAttempt(value) + pollDelay(value) - now();
|
|
1373
|
+
if (value.stale || remaining <= 0)
|
|
1374
|
+
refresh(key2);
|
|
1375
|
+
else
|
|
1376
|
+
schedule(key2, value, remaining);
|
|
1377
|
+
}
|
|
1378
|
+
let released = false;
|
|
1379
|
+
return () => {
|
|
1380
|
+
if (released)
|
|
1381
|
+
return;
|
|
1382
|
+
released = true;
|
|
1383
|
+
value.refs--;
|
|
1384
|
+
if (value.refs === 0)
|
|
1385
|
+
cancelTimer(value);
|
|
1386
|
+
};
|
|
1387
|
+
},
|
|
1388
|
+
refresh,
|
|
1389
|
+
keys() {
|
|
1390
|
+
return [...entries.keys()];
|
|
1391
|
+
},
|
|
1392
|
+
notify(key2) {
|
|
1393
|
+
const value = entries.get(key2);
|
|
1394
|
+
if (!value || value.refs === 0 || value.inflight)
|
|
1395
|
+
return;
|
|
1396
|
+
const last = Math.max(value.snapshot.fetchedAt ?? 0, value.snapshot.error?.at ?? 0);
|
|
1397
|
+
if (now() - last < minEventInterval)
|
|
1398
|
+
return;
|
|
1399
|
+
refresh(key2);
|
|
1400
|
+
},
|
|
1401
|
+
invalidate(key2) {
|
|
1402
|
+
const value = entries.get(key2);
|
|
1403
|
+
if (!value)
|
|
1404
|
+
return;
|
|
1405
|
+
if (value.inflight)
|
|
1406
|
+
value.invalidated = true;
|
|
1407
|
+
else if (value.refs > 0)
|
|
1408
|
+
refresh(key2);
|
|
1409
|
+
else
|
|
1410
|
+
value.stale = true;
|
|
1411
|
+
},
|
|
1412
|
+
setTargeted(key2, targeted) {
|
|
1413
|
+
const value = entries.get(key2);
|
|
1414
|
+
if (!value || disposed || value.snapshot.targeted === true === targeted)
|
|
1415
|
+
return;
|
|
1416
|
+
update(key2, value, { targeted: targeted ? true : undefined });
|
|
1417
|
+
},
|
|
1418
|
+
dispose() {
|
|
1419
|
+
disposed = true;
|
|
1420
|
+
for (const value of entries.values())
|
|
1421
|
+
cancelTimer(value);
|
|
1422
|
+
entries.clear();
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1258
1427
|
// packages/forgekeeper/src/target.ts
|
|
1259
1428
|
var MAX_TARGETS = 50;
|
|
1260
1429
|
var PREFIX_REFERENCES = 4;
|
|
@@ -1466,11 +1635,12 @@ async function locateTargets(forges, directory, targets, previous) {
|
|
|
1466
1635
|
});
|
|
1467
1636
|
});
|
|
1468
1637
|
if (error !== undefined && !outcomes.some((outcome) => ("request" in outcome)))
|
|
1469
|
-
throw error;
|
|
1638
|
+
throw new TargetLookupError(error);
|
|
1470
1639
|
if (found.length === 0)
|
|
1471
1640
|
return {
|
|
1472
1641
|
kind: "none",
|
|
1473
|
-
reason: requests2.length === 1 ? failed[0].reason : "No session target PR/MR could be resolved"
|
|
1642
|
+
reason: requests2.length === 1 ? failed[0].reason : "No session target PR/MR could be resolved",
|
|
1643
|
+
explicitTarget: true
|
|
1474
1644
|
};
|
|
1475
1645
|
return {
|
|
1476
1646
|
kind: "found",
|
|
@@ -1516,154 +1686,6 @@ async function locate(forges, hints) {
|
|
|
1516
1686
|
};
|
|
1517
1687
|
}
|
|
1518
1688
|
|
|
1519
|
-
// packages/forgekeeper/src/store.ts
|
|
1520
|
-
function createStatusStore(options) {
|
|
1521
|
-
const now = options.now ?? Date.now;
|
|
1522
|
-
const setTimer = options.setTimer ?? ((callback, delay) => setTimeout(callback, delay));
|
|
1523
|
-
const clearTimer = options.clearTimer ?? ((timer) => clearTimeout(timer));
|
|
1524
|
-
const interval = options.interval ?? 120000;
|
|
1525
|
-
const maxBackoff = options.maxBackoff ?? 900000;
|
|
1526
|
-
const minEventInterval = options.minEventInterval ?? 15000;
|
|
1527
|
-
const entries = new Map;
|
|
1528
|
-
let disposed = false;
|
|
1529
|
-
let revision = now() * 1000;
|
|
1530
|
-
const entry = (key2) => {
|
|
1531
|
-
let value = entries.get(key2);
|
|
1532
|
-
if (!value) {
|
|
1533
|
-
value = { snapshot: { loading: false }, refs: 0, failures: 0 };
|
|
1534
|
-
entries.set(key2, value);
|
|
1535
|
-
}
|
|
1536
|
-
return value;
|
|
1537
|
-
};
|
|
1538
|
-
const update = (key2, value, patch) => {
|
|
1539
|
-
value.snapshot = { ...value.snapshot, ...patch, revision: ++revision };
|
|
1540
|
-
if (!disposed)
|
|
1541
|
-
options.onChange(key2);
|
|
1542
|
-
};
|
|
1543
|
-
const cancelTimer = (value) => {
|
|
1544
|
-
if (value.timer === undefined)
|
|
1545
|
-
return;
|
|
1546
|
-
clearTimer(value.timer);
|
|
1547
|
-
value.timer = undefined;
|
|
1548
|
-
};
|
|
1549
|
-
const baseInterval = (value) => options.active?.(value.snapshot.lookup) ? options.activeInterval ?? interval : interval;
|
|
1550
|
-
const pollDelay = (value) => Math.min(baseInterval(value) * 2 ** value.failures, maxBackoff);
|
|
1551
|
-
const lastAttempt = (value) => Math.max(value.snapshot.fetchedAt ?? -Infinity, value.snapshot.error?.at ?? -Infinity);
|
|
1552
|
-
const schedule = (key2, value, delay = pollDelay(value)) => {
|
|
1553
|
-
cancelTimer(value);
|
|
1554
|
-
if (disposed || value.refs === 0)
|
|
1555
|
-
return;
|
|
1556
|
-
const lookup = value.snapshot.lookup;
|
|
1557
|
-
if (!value.snapshot.error && (lookup?.kind !== "found" || lookup.requests.length === 0))
|
|
1558
|
-
return;
|
|
1559
|
-
value.timer = setTimer(() => {
|
|
1560
|
-
value.timer = undefined;
|
|
1561
|
-
refresh(key2);
|
|
1562
|
-
}, delay);
|
|
1563
|
-
};
|
|
1564
|
-
function refresh(key2) {
|
|
1565
|
-
if (disposed)
|
|
1566
|
-
return Promise.resolve({ loading: false });
|
|
1567
|
-
const value = entry(key2);
|
|
1568
|
-
if (value.inflight)
|
|
1569
|
-
return value.inflight;
|
|
1570
|
-
value.stale = false;
|
|
1571
|
-
update(key2, value, { loading: true });
|
|
1572
|
-
value.inflight = options.load(key2).then((lookup) => {
|
|
1573
|
-
if (disposed || value.invalidated)
|
|
1574
|
-
return;
|
|
1575
|
-
const previous = value.snapshot.lookup;
|
|
1576
|
-
value.failures = 0;
|
|
1577
|
-
update(key2, value, { lookup, fetchedAt: now(), error: undefined, loading: false });
|
|
1578
|
-
try {
|
|
1579
|
-
if (!disposed)
|
|
1580
|
-
options.onLoad?.(key2, previous, lookup);
|
|
1581
|
-
} catch (error) {
|
|
1582
|
-
try {
|
|
1583
|
-
options.onLoadError?.(error);
|
|
1584
|
-
} catch {}
|
|
1585
|
-
}
|
|
1586
|
-
}, (error) => {
|
|
1587
|
-
if (disposed || value.invalidated)
|
|
1588
|
-
return;
|
|
1589
|
-
value.failures++;
|
|
1590
|
-
const known = error instanceof ForgeError;
|
|
1591
|
-
update(key2, value, {
|
|
1592
|
-
loading: false,
|
|
1593
|
-
error: {
|
|
1594
|
-
kind: known ? error.kind : "request",
|
|
1595
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1596
|
-
at: now()
|
|
1597
|
-
}
|
|
1598
|
-
});
|
|
1599
|
-
}).then(() => {
|
|
1600
|
-
value.inflight = undefined;
|
|
1601
|
-
if (value.invalidated && !disposed) {
|
|
1602
|
-
value.invalidated = false;
|
|
1603
|
-
return refresh(key2);
|
|
1604
|
-
}
|
|
1605
|
-
schedule(key2, value);
|
|
1606
|
-
return value.snapshot;
|
|
1607
|
-
});
|
|
1608
|
-
return value.inflight;
|
|
1609
|
-
}
|
|
1610
|
-
return {
|
|
1611
|
-
get(key2) {
|
|
1612
|
-
return entries.get(key2)?.snapshot ?? { loading: false };
|
|
1613
|
-
},
|
|
1614
|
-
acquire(key2) {
|
|
1615
|
-
const value = entry(key2);
|
|
1616
|
-
value.refs++;
|
|
1617
|
-
if (value.refs === 1 && !value.inflight) {
|
|
1618
|
-
const remaining = lastAttempt(value) + pollDelay(value) - now();
|
|
1619
|
-
if (value.stale || remaining <= 0)
|
|
1620
|
-
refresh(key2);
|
|
1621
|
-
else
|
|
1622
|
-
schedule(key2, value, remaining);
|
|
1623
|
-
}
|
|
1624
|
-
let released = false;
|
|
1625
|
-
return () => {
|
|
1626
|
-
if (released)
|
|
1627
|
-
return;
|
|
1628
|
-
released = true;
|
|
1629
|
-
value.refs--;
|
|
1630
|
-
if (value.refs === 0)
|
|
1631
|
-
cancelTimer(value);
|
|
1632
|
-
};
|
|
1633
|
-
},
|
|
1634
|
-
refresh,
|
|
1635
|
-
keys() {
|
|
1636
|
-
return [...entries.keys()];
|
|
1637
|
-
},
|
|
1638
|
-
notify(key2) {
|
|
1639
|
-
const value = entries.get(key2);
|
|
1640
|
-
if (!value || value.refs === 0 || value.inflight)
|
|
1641
|
-
return;
|
|
1642
|
-
const last = Math.max(value.snapshot.fetchedAt ?? 0, value.snapshot.error?.at ?? 0);
|
|
1643
|
-
if (now() - last < minEventInterval)
|
|
1644
|
-
return;
|
|
1645
|
-
refresh(key2);
|
|
1646
|
-
},
|
|
1647
|
-
invalidate(key2) {
|
|
1648
|
-
const value = entries.get(key2);
|
|
1649
|
-
if (!value)
|
|
1650
|
-
return;
|
|
1651
|
-
if (value.inflight)
|
|
1652
|
-
value.invalidated = true;
|
|
1653
|
-
else if (value.refs > 0)
|
|
1654
|
-
refresh(key2);
|
|
1655
|
-
else
|
|
1656
|
-
value.stale = true;
|
|
1657
|
-
},
|
|
1658
|
-
dispose() {
|
|
1659
|
-
disposed = true;
|
|
1660
|
-
for (const value of entries.values())
|
|
1661
|
-
cancelTimer(value);
|
|
1662
|
-
entries.clear();
|
|
1663
|
-
}
|
|
1664
|
-
};
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
1689
|
// packages/forgekeeper/src/status-service.ts
|
|
1668
1690
|
function createStatusService(options) {
|
|
1669
1691
|
const now = options.now ?? Date.now;
|
|
@@ -1791,6 +1813,7 @@ function createStatusService(options) {
|
|
|
1791
1813
|
if (!disposed && !holds.has(key2) && liveLeases(key2).length > 0)
|
|
1792
1814
|
store.refresh(key2);
|
|
1793
1815
|
},
|
|
1816
|
+
setTargeted: (key2, targeted) => store.setTargeted(key2, targeted),
|
|
1794
1817
|
notify: (key2) => store.notify(key2),
|
|
1795
1818
|
keys: () => store.keys(),
|
|
1796
1819
|
polling: (key2) => holds.has(key2),
|
|
@@ -1995,7 +2018,11 @@ async function setupStatus(ctx, settings, deps, host) {
|
|
|
1995
2018
|
});
|
|
1996
2019
|
return {
|
|
1997
2020
|
service,
|
|
1998
|
-
targetsChanged
|
|
2021
|
+
targetsChanged(sessionID, targets) {
|
|
2022
|
+
const classified = classifyTargets(targetOutput(targets)) ?? [];
|
|
2023
|
+
service.setTargeted(sessionID, classified.some((target2) => target2.kind === "merge-request"));
|
|
2024
|
+
service.invalidate(sessionID);
|
|
2025
|
+
},
|
|
1999
2026
|
async titleChanged(sessionID) {
|
|
2000
2027
|
if (!service.keys().includes(sessionID))
|
|
2001
2028
|
return;
|
|
@@ -2243,7 +2270,7 @@ async function setupServer(ctx, lookups, status2) {
|
|
|
2243
2270
|
targets: result.targets,
|
|
2244
2271
|
...state.prefix ? { prefix: state.prefix } : {}
|
|
2245
2272
|
});
|
|
2246
|
-
watcher?.targetsChanged(sessionID);
|
|
2273
|
+
watcher?.targetsChanged(sessionID, result.targets);
|
|
2247
2274
|
await rpc.events.emit("targetChanged", { sessionID, ...targetOutput(result.targets) });
|
|
2248
2275
|
await update(sessionID);
|
|
2249
2276
|
});
|
package/dist/tui.js
CHANGED
|
@@ -731,7 +731,7 @@ function footerSegments(snapshot) {
|
|
|
731
731
|
const { lookup, error } = snapshot;
|
|
732
732
|
const stale = error ? [{ text: "stale", tone: "warning", essential: true }] : [];
|
|
733
733
|
if (!lookup || lookup.kind === "none" || lookup.requests.length === 0) {
|
|
734
|
-
return error ? [{ text: "PR/MR status unavailable", tone: "warning" }] : [];
|
|
734
|
+
return snapshot.targeted && (error || lookup?.explicitTarget) ? [{ text: "PR/MR status unavailable", tone: "warning" }] : [];
|
|
735
735
|
}
|
|
736
736
|
if (lookup.failed?.length) {
|
|
737
737
|
stale.unshift({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-forgekeeper",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "OpenCode plugin that tracks each session's GitHub and GitLab issues and PRs/MRs: title prefixes, a PR/MR status footer, and review notifications",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Pedro Pombeiro",
|