@bensandee/forgejo-cli 0.5.6 → 0.6.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.
Files changed (2) hide show
  1. package/dist/bin.mjs +347 -369
  2. package/package.json +1 -1
package/dist/bin.mjs CHANGED
@@ -6,7 +6,7 @@ import { FatalError, TransientError, UnexpectedError, describeError, redact, red
6
6
  import { z } from "zod";
7
7
  import { defineCommand, renderUsage, runCommand, runMain } from "citty";
8
8
  import { execFileSync, spawn } from "node:child_process";
9
- //#region ../cli-kit/src/json.ts
9
+ // ../cli-kit/src/json.ts
10
10
  function resolveJsonMode(rawArgs) {
11
11
  if (!rawArgs.some(requestsJson)) return false;
12
12
  if (rawArgs.includes("--help") || rawArgs.includes("-h")) return false;
@@ -100,8 +100,7 @@ function classifyThrown(error, verbose) {
100
100
  detail
101
101
  };
102
102
  }
103
- //#endregion
104
- //#region ../cli-kit/src/env-file.ts
103
+ // ../cli-kit/src/env-file.ts
105
104
  const ENV_FILE_VAR = "BST_ENV_FILE";
106
105
  function loadEnvFileFromEnv() {
107
106
  const raw = process.env[ENV_FILE_VAR]?.trim();
@@ -158,11 +157,10 @@ function loadEnvFileForBin(rawArgs, bin, io = processIo) {
158
157
  return io.exit(1);
159
158
  }
160
159
  }
161
- //#endregion
162
- //#region src/version.ts
160
+ // src/version.ts
163
161
  const parsed = z.object({ version: z.string() }).safeParse({
164
162
  name: "@bensandee/forgejo-cli",
165
- version: "0.5.6",
163
+ version: "0.6.1",
166
164
  description: "bst-fj: Forgejo API commands without the rest of bst",
167
165
  license: "MIT",
168
166
  repository: {
@@ -203,14 +201,12 @@ const parsed = z.object({ version: z.string() }).safeParse({
203
201
  });
204
202
  if (!parsed.success) throw new UnexpectedError(`@bensandee/forgejo-cli's package.json has no string version.`);
205
203
  const BST_FJ_VERSION = parsed.data.version;
206
- //#endregion
207
- //#region src/env-file-preload.ts
204
+ // src/env-file-preload.ts
208
205
  const envFileLoad = loadEnvFileForBin(process.argv.slice(2), {
209
206
  name: "bst-fj",
210
207
  version: BST_FJ_VERSION
211
208
  });
212
- //#endregion
213
- //#region ../cli-kit/src/logger.ts
209
+ // ../cli-kit/src/logger.ts
214
210
  const RANK = {
215
211
  error: 0,
216
212
  warn: 1,
@@ -310,8 +306,7 @@ function createLogger(options = {}) {
310
306
  });
311
307
  }
312
308
  const rootLogger = createLogger();
313
- //#endregion
314
- //#region ../cli-kit/src/flags.ts
309
+ // ../cli-kit/src/flags.ts
315
310
  const UNIVERSAL_FLAGS = [
316
311
  "--help",
317
312
  "-h",
@@ -643,8 +638,12 @@ function listFlagPresent(rawArgs, flag) {
643
638
  const long = `--${flag}`;
644
639
  return rawArgs.some((token) => token === long || token.startsWith(`${long}=`));
645
640
  }
646
- //#endregion
647
- //#region ../cli-kit/src/output.ts
641
+ function parseLimitFlag(raw) {
642
+ const value = Number(raw);
643
+ if (!Number.isInteger(value) || value < 1) throw new FatalError(`--limit must be a positive integer, got "${raw}"`);
644
+ return value;
645
+ }
646
+ // ../cli-kit/src/output.ts
648
647
  const stdout = (msg) => console.log(msg);
649
648
  function initialState() {
650
649
  return {
@@ -707,8 +706,7 @@ function flushJsonOutput() {
707
706
  function jsonDocumentWritten() {
708
707
  return state.wrote;
709
708
  }
710
- //#endregion
711
- //#region ../cli-kit/src/path-probe.ts
709
+ // ../cli-kit/src/path-probe.ts
712
710
  function probeCliPath(value) {
713
711
  try {
714
712
  const stat = statSync(value, { throwIfNoEntry: false });
@@ -718,8 +716,7 @@ function probeCliPath(value) {
718
716
  return "missing";
719
717
  }
720
718
  }
721
- //#endregion
722
- //#region ../cli-kit/src/run.ts
719
+ // ../cli-kit/src/run.ts
723
720
  function classifyWithHook(error, verbose, hook) {
724
721
  const classified = classifyThrown(error, verbose);
725
722
  const worded = hook?.(error);
@@ -873,8 +870,7 @@ const log = {
873
870
  current.debug(message);
874
871
  }
875
872
  };
876
- //#endregion
877
- //#region ../forgejo/src/connection.ts
873
+ // ../forgejo/src/connection.ts
878
874
  function parseGitUrl(urlStr) {
879
875
  try {
880
876
  const url = new URL(urlStr);
@@ -1001,14 +997,12 @@ function normalizeGitUrl(raw) {
1001
997
  return null;
1002
998
  }
1003
999
  }
1004
- //#endregion
1005
- //#region src/describe-forge-error.ts
1000
+ // src/describe-forge-error.ts
1006
1001
  function describeForgeError(error) {
1007
1002
  if (error instanceof RepoTargetEmptyError) return "--repo was given with no repository URL (expected e.g. https://host/owner/repo).";
1008
1003
  if (error instanceof RepoTargetMismatchError) return `--repo disagrees with the issue URL: --repo names ${error.given}, the issue URL names ${error.fromReference}.`;
1009
1004
  }
1010
- //#endregion
1011
- //#region ../forgejo/src/fetch-retry.ts
1005
+ // ../forgejo/src/fetch-retry.ts
1012
1006
  const DEFAULT_RETRY_POLICY = {
1013
1007
  attempts: 5,
1014
1008
  baseDelayMs: 250,
@@ -1065,28 +1059,29 @@ function fetchWithRetry(fetchImpl, policy = DEFAULT_RETRY_POLICY, defaultTimeout
1065
1059
  function createForgeFetcher(policy = DEFAULT_RETRY_POLICY) {
1066
1060
  return { fetch: fetchWithRetry((input, init) => globalThis.fetch(input, init), policy) };
1067
1061
  }
1068
- //#endregion
1069
- //#region src/fetcher.ts
1062
+ // src/fetcher.ts
1070
1063
  function cliFetcher() {
1071
1064
  return createForgeFetcher();
1072
1065
  }
1073
- //#endregion
1074
- //#region ../forgejo/src/errors.ts
1066
+ // ../forgejo/src/errors.ts
1075
1067
  var ForgeAccessError = class extends FatalError {};
1076
- function authHeaders$2(conn) {
1068
+ function authHeaders(conn) {
1077
1069
  return { Authorization: `token ${conn.token}` };
1078
1070
  }
1079
1071
  function jsonHeaders(conn) {
1080
1072
  return {
1081
- ...authHeaders$2(conn),
1073
+ ...authHeaders(conn),
1082
1074
  "Content-Type": "application/json"
1083
1075
  };
1084
1076
  }
1085
1077
  function repoNotFoundMessage(conn) {
1086
1078
  return `Repository ${conn.repository} not found (or the token cannot see it)`;
1087
1079
  }
1080
+ function repoBase(conn) {
1081
+ return `${conn.serverUrl}/api/v1/repos/${conn.repository}`;
1082
+ }
1088
1083
  function issuesBase(conn) {
1089
- return `${conn.serverUrl}/api/v1/repos/${conn.repository}/issues`;
1084
+ return `${repoBase(conn)}/issues`;
1090
1085
  }
1091
1086
  function issueNotFoundMessage(conn, number) {
1092
1087
  return `Issue #${String(number)} not found in ${conn.repository}`;
@@ -1165,8 +1160,7 @@ async function throwForgejoFailure(res, operation, notFoundMessage) {
1165
1160
  if (res.status === 410) throw new FatalError(`${message}\nForgejo returned 410 Gone -- issues are likely disabled on this repository; retrying will not help.`);
1166
1161
  throw new TransientError(message);
1167
1162
  }
1168
- //#endregion
1169
- //#region ../forgejo/src/commit-status.ts
1163
+ // ../forgejo/src/commit-status.ts
1170
1164
  const CombinedStatusSchema = z.object({
1171
1165
  state: z.string(),
1172
1166
  statuses: z.array(z.unknown())
@@ -1286,18 +1280,12 @@ z.object({
1286
1280
  created: z.string()
1287
1281
  });
1288
1282
  z.array(z.unknown());
1289
- function repoBase$1(conn) {
1290
- return `${conn.serverUrl}/api/v1/repos/${conn.repository}`;
1291
- }
1292
- function authHeaders$1(conn) {
1293
- return { Authorization: `token ${conn.token}` };
1294
- }
1295
1283
  function branchNotFoundMessage(conn, branch) {
1296
1284
  return `Branch ${branch} not found in ${conn.repository}`;
1297
1285
  }
1298
1286
  async function getBranchHeadSha(executor, conn, branch) {
1299
- const url = `${repoBase$1(conn)}/branches/${encodeURIComponent(branch)}`;
1300
- const res = await executor.fetch(url, { headers: authHeaders$1(conn) });
1287
+ const url = `${repoBase(conn)}/branches/${encodeURIComponent(branch)}`;
1288
+ const res = await executor.fetch(url, { headers: authHeaders(conn) });
1301
1289
  if (!res.ok) await throwForgejoFailure(res, {
1302
1290
  verb: "read branch",
1303
1291
  resource: branch
@@ -1307,8 +1295,8 @@ async function getBranchHeadSha(executor, conn, branch) {
1307
1295
  return parsed.data.commit.id;
1308
1296
  }
1309
1297
  async function branchExists(executor, conn, branch) {
1310
- const url = `${repoBase$1(conn)}/branches/${encodeURIComponent(branch)}`;
1311
- const res = await executor.fetch(url, { headers: authHeaders$1(conn) });
1298
+ const url = `${repoBase(conn)}/branches/${encodeURIComponent(branch)}`;
1299
+ const res = await executor.fetch(url, { headers: authHeaders(conn) });
1312
1300
  if (res.status === 404) return false;
1313
1301
  if (!res.ok) await throwForgejoFailure(res, {
1314
1302
  verb: "check branch",
@@ -1316,8 +1304,7 @@ async function branchExists(executor, conn, branch) {
1316
1304
  }, branchNotFoundMessage(conn, branch));
1317
1305
  return true;
1318
1306
  }
1319
- //#endregion
1320
- //#region ../cli-kit/src/git.ts
1307
+ // ../cli-kit/src/git.ts
1321
1308
  const BRANCH_REF_PREFIX = "refs/heads/";
1322
1309
  const ORIGIN_READ_TIMEOUT_MS = 5e3;
1323
1310
  function defaultGitRunner(args, cwd, options) {
@@ -1380,8 +1367,7 @@ function readPackageRepositoryUrl(cwd) {
1380
1367
  if (!repo) return null;
1381
1368
  return typeof repo === "string" ? repo : repo.url;
1382
1369
  }
1383
- //#endregion
1384
- //#region src/connection.ts
1370
+ // src/connection.ts
1385
1371
  function resolveRepoUrl(cwd) {
1386
1372
  return resolveRepoUrlFrom(() => readOriginUrl(cwd), () => readPackageRepositoryUrl(cwd));
1387
1373
  }
@@ -1403,8 +1389,7 @@ function requireForgejoTarget(cwd, target, githubRefusal, env = process.env) {
1403
1389
  if (conn === null) throw new FatalError(githubRefusal);
1404
1390
  return conn;
1405
1391
  }
1406
- //#endregion
1407
- //#region ../forgejo/src/check-poll.ts
1392
+ // ../forgejo/src/check-poll.ts
1408
1393
  function classifyBaseStatus(checks) {
1409
1394
  if (!checks.parsed) return "unparseable";
1410
1395
  if (checks.contexts.length === 0) return "no-status";
@@ -1441,17 +1426,67 @@ async function readPollChecks(fetcher, conn, sha) {
1441
1426
  return { failureDetail: describeError(error) };
1442
1427
  }
1443
1428
  }
1444
- //#endregion
1445
- //#region src/poll-timings.ts
1446
- const DEFAULT_TIMEOUT_MS = 12e5;
1447
- const DEFAULT_INTERVAL_MS = 15e3;
1448
- const MIN_INTERVAL_MS = 5e3;
1429
+ async function settleChecks(fetcher, conn, poll, round) {
1430
+ const { timeoutMs, intervalMs, sleep, now } = poll;
1431
+ const startedAt = now();
1432
+ const elapsed = () => now() - startedAt;
1433
+ let consecutiveFailures = 0;
1434
+ let lastFailureDetail;
1435
+ let lastGoodChecks;
1436
+ let previousSettledContexts = null;
1437
+ for (;;) {
1438
+ const sha = round.sha();
1439
+ const { checks, failureDetail } = await readPollChecks(fetcher, conn, sha);
1440
+ if (failureDetail !== void 0) lastFailureDetail = failureDetail;
1441
+ if (checks === void 0) {
1442
+ consecutiveFailures += 1;
1443
+ if (consecutiveFailures >= 3) return round.report(EMPTY_CHECKS, {
1444
+ exit: "retry-budget",
1445
+ elapsed,
1446
+ failureDetail: lastFailureDetail
1447
+ });
1448
+ } else {
1449
+ consecutiveFailures = 0;
1450
+ lastGoodChecks = checks;
1451
+ pushDisagreementNotice(round.notices, sha, checks);
1452
+ if (isSettled(checks)) {
1453
+ const contexts = new Set(checks.contexts.map((check) => check.context));
1454
+ if (previousSettledContexts !== null && previousSettledContexts.size === contexts.size && [...previousSettledContexts].every((context) => contexts.has(context))) return round.report(checks, {
1455
+ exit: "settled",
1456
+ elapsed
1457
+ });
1458
+ previousSettledContexts = contexts;
1459
+ } else previousSettledContexts = null;
1460
+ }
1461
+ if (elapsed() >= timeoutMs) return round.report(lastGoodChecks ?? EMPTY_CHECKS, {
1462
+ exit: "timeout",
1463
+ elapsed
1464
+ });
1465
+ await sleep(intervalMs);
1466
+ if (round.afterSleep !== void 0) {
1467
+ const after = await round.afterSleep(sha, elapsed);
1468
+ if (after !== "retry" && after !== "next") return after.done;
1469
+ }
1470
+ }
1471
+ }
1472
+ function renderCheckLine(check) {
1473
+ const parts = [check.state, check.context];
1474
+ if (check.description !== null) parts.push(check.description);
1475
+ let line = parts.join(" -- ");
1476
+ if (check.verdict === "red" && check.targetUrl !== null) line += ` (${check.targetUrl})`;
1477
+ return line;
1478
+ }
1479
+ // ../cli-kit/src/env-override.ts
1449
1480
  function envMsOverride(raw, productionMs) {
1450
1481
  if (raw === void 0) return productionMs;
1451
1482
  const parsed = Number(raw.trim());
1452
1483
  if (!Number.isSafeInteger(parsed) || parsed <= 0) return productionMs;
1453
1484
  return Math.min(parsed, productionMs);
1454
1485
  }
1486
+ // src/poll-timings.ts
1487
+ const DEFAULT_TIMEOUT_MS = 12e5;
1488
+ const DEFAULT_INTERVAL_MS = 15e3;
1489
+ const MIN_INTERVAL_MS = 5e3;
1455
1490
  function resolvePollTimings(options) {
1456
1491
  const timeoutMs = options.timeout !== void 0 ? parseDuration(options.timeout, "--timeout") : DEFAULT_TIMEOUT_MS;
1457
1492
  const requestedIntervalMs = options.interval !== void 0 ? parseDuration(options.interval, "--interval") : DEFAULT_INTERVAL_MS;
@@ -1460,15 +1495,13 @@ function resolvePollTimings(options) {
1460
1495
  intervalMs: envMsOverride(process.env["BST_TEST_CHECK_POLL_INTERVAL_MS"], Math.max(requestedIntervalMs, MIN_INTERVAL_MS))
1461
1496
  };
1462
1497
  }
1463
- //#endregion
1464
- //#region src/sleep.ts
1498
+ // src/sleep.ts
1465
1499
  function sleep(ms) {
1466
1500
  return new Promise((resolve) => {
1467
1501
  setTimeout(resolve, ms);
1468
1502
  });
1469
1503
  }
1470
- //#endregion
1471
- //#region src/commit-status.ts
1504
+ // src/commit-status.ts
1472
1505
  async function resolveCommitStatusTarget(fetcher, options) {
1473
1506
  const targetRepo = parseRepoTarget(options.repo);
1474
1507
  const conn = requireForgejoTarget(options.cwd, targetRepo, "commit:status is Forgejo-only: GitHub Actions publishes check runs, not commit statuses. Use `gh run list` / `gh run view` instead.");
@@ -1528,47 +1561,23 @@ function buildReport$1(target, checks, ctx, extras = {}) {
1528
1561
  return report;
1529
1562
  }
1530
1563
  async function waitForSettledChecks$1(fetcher, target, notices, poll) {
1531
- const { timeoutMs, intervalMs, sleep, now } = poll;
1532
- const startedAt = now();
1533
- let consecutiveFailures = 0;
1534
- let lastFailureDetail;
1535
- let lastGoodChecks;
1536
- let previousSettledContexts = null;
1537
- for (;;) {
1538
- const sha = target.sha;
1539
- const { checks, failureDetail } = await readPollChecks(fetcher, target.conn, sha);
1540
- if (failureDetail !== void 0) lastFailureDetail = failureDetail;
1541
- if (checks === void 0) {
1542
- consecutiveFailures += 1;
1543
- if (consecutiveFailures >= 3) return buildReport$1(target, EMPTY_CHECKS, {
1564
+ return settleChecks(fetcher, target.conn, poll, {
1565
+ sha: () => target.sha,
1566
+ notices,
1567
+ report: (checks, extras) => {
1568
+ const extrasForReport = {};
1569
+ if (extras.exit === "timeout") extrasForReport.statusOverride = "timeout";
1570
+ else if (extras.exit === "retry-budget") {
1571
+ extrasForReport.statusOverride = "unparseable";
1572
+ if (extras.failureDetail !== void 0) extrasForReport.detailOverride = extras.failureDetail;
1573
+ }
1574
+ return buildReport$1(target, checks, {
1544
1575
  notices,
1545
1576
  waited: true,
1546
- elapsedMs: now() - startedAt
1547
- }, {
1548
- statusOverride: "unparseable",
1549
- detailOverride: lastFailureDetail
1550
- });
1551
- } else {
1552
- consecutiveFailures = 0;
1553
- lastGoodChecks = checks;
1554
- pushDisagreementNotice(notices, sha, checks);
1555
- if (isSettled(checks)) {
1556
- const contexts = new Set(checks.contexts.map((check) => check.context));
1557
- if (previousSettledContexts !== null && previousSettledContexts.size === contexts.size && [...previousSettledContexts].every((context) => contexts.has(context))) return buildReport$1(target, checks, {
1558
- notices,
1559
- waited: true,
1560
- elapsedMs: now() - startedAt
1561
- });
1562
- previousSettledContexts = contexts;
1563
- } else previousSettledContexts = null;
1577
+ elapsedMs: extras.elapsed()
1578
+ }, extrasForReport);
1564
1579
  }
1565
- if (now() - startedAt >= timeoutMs) return buildReport$1(target, lastGoodChecks ?? EMPTY_CHECKS, {
1566
- notices,
1567
- waited: true,
1568
- elapsedMs: now() - startedAt
1569
- }, { statusOverride: "timeout" });
1570
- await sleep(intervalMs);
1571
- }
1580
+ });
1572
1581
  }
1573
1582
  async function runCommitStatus(fetcher, options) {
1574
1583
  const timings = options.wait === true ? resolvePollTimings(options) : void 0;
@@ -1588,13 +1597,7 @@ async function runCommitStatus(fetcher, options) {
1588
1597
  });
1589
1598
  }
1590
1599
  function renderCommitStatus(report) {
1591
- const lines = report.checks.map((check) => {
1592
- const parts = [check.state, check.context];
1593
- if (check.description !== null) parts.push(check.description);
1594
- let line = parts.join(" -- ");
1595
- if (check.verdict === "red" && check.targetUrl !== null) line += ` (${check.targetUrl})`;
1596
- return line;
1597
- });
1600
+ const lines = report.checks.map(renderCheckLine);
1598
1601
  const shortSha = FULL_SHA.test(report.sha) ? report.sha.slice(0, 12) : report.sha;
1599
1602
  const redCount = report.checks.filter((c) => c.verdict === "red").length;
1600
1603
  const pendingCount = report.checks.filter((c) => c.verdict === "pending").length;
@@ -1608,8 +1611,7 @@ function renderCommitStatus(report) {
1608
1611
  lines.push(summaryParts.join(" -- "));
1609
1612
  return lines.join("\n");
1610
1613
  }
1611
- //#endregion
1612
- //#region src/commands/commit-status.ts
1614
+ // src/commands/commit-status.ts
1613
1615
  const commitStatusCommand = defineCommand({
1614
1616
  meta: {
1615
1617
  name: "commit:status",
@@ -1663,8 +1665,7 @@ const commitStatusCommand = defineCommand({
1663
1665
  process.exitCode = report.status === "green" ? 0 : 1;
1664
1666
  }
1665
1667
  });
1666
- //#endregion
1667
- //#region ../forgejo/src/fetcher.ts
1668
+ // ../forgejo/src/fetcher.ts
1668
1669
  const LONG_FETCH_TIMEOUT_MS = 6e5;
1669
1670
  function morePagesFollow(res, pageLength, received, pageSize) {
1670
1671
  if (pageLength === 0) return false;
@@ -1673,8 +1674,7 @@ function morePagesFollow(res, pageLength, received, pageSize) {
1673
1674
  if (!Number.isSafeInteger(total) || total < 0) return pageLength >= pageSize;
1674
1675
  return received < total;
1675
1676
  }
1676
- //#endregion
1677
- //#region ../forgejo/src/label-provisioning.ts
1677
+ // ../forgejo/src/label-provisioning.ts
1678
1678
  const LabelSchema = z.object({
1679
1679
  id: z.number(),
1680
1680
  name: z.string(),
@@ -1688,7 +1688,7 @@ const LABELS_MAX_PAGES = 20;
1688
1688
  async function fetchAllForgejoLabels(executor, conn) {
1689
1689
  const labels = [];
1690
1690
  for (let page = 1; page <= LABELS_MAX_PAGES; page++) {
1691
- const res = await executor.fetch(`${conn.serverUrl}/api/v1/repos/${conn.repository}/labels?limit=${String(LABELS_PAGE_SIZE)}&page=${String(page)}`, { headers: authHeaders$2(conn) });
1691
+ const res = await executor.fetch(`${conn.serverUrl}/api/v1/repos/${conn.repository}/labels?limit=${String(LABELS_PAGE_SIZE)}&page=${String(page)}`, { headers: authHeaders(conn) });
1692
1692
  if (!res.ok) await throwForgejoFailure(res, {
1693
1693
  verb: "list labels",
1694
1694
  resource: conn.repository
@@ -1712,6 +1712,17 @@ async function resolveLabelIds(executor, conn, names) {
1712
1712
  return match.id;
1713
1713
  });
1714
1714
  }
1715
+ function isRetiringExpired(spec, today) {
1716
+ return today > spec.until;
1717
+ }
1718
+ function retiringAsLabelSpec(spec) {
1719
+ return {
1720
+ name: spec.name,
1721
+ color: spec.color,
1722
+ description: spec.description,
1723
+ exclusive: false
1724
+ };
1725
+ }
1715
1726
  const LabelSpecSchema = z.strictObject({
1716
1727
  name: z.string().min(1),
1717
1728
  color: z.string().regex(/^#[0-9a-fA-F]{6}$/),
@@ -1761,13 +1772,14 @@ function labelScope(name) {
1761
1772
  const slash = name.indexOf("/");
1762
1773
  return slash === -1 ? null : name.slice(0, slash + 1);
1763
1774
  }
1764
- function staleLabelNames(specs, live) {
1775
+ function staleLabelNames(specs, live, retiring = []) {
1765
1776
  const owned = /* @__PURE__ */ new Set();
1766
1777
  for (const spec of specs) {
1767
1778
  const scope = labelScope(spec.name);
1768
1779
  if (scope !== null) owned.add(scope);
1769
1780
  }
1770
1781
  const wanted = new Set(specs.map((spec) => spec.name));
1782
+ for (const spec of retiring) wanted.add(spec.name);
1771
1783
  const stale = [];
1772
1784
  for (const name of live) {
1773
1785
  if (wanted.has(name)) continue;
@@ -1828,15 +1840,14 @@ async function ensureForgejoLabels(executor, conn, specs) {
1828
1840
  async function deleteForgejoLabel(executor, conn, id) {
1829
1841
  const res = await executor.fetch(`${conn.serverUrl}/api/v1/repos/${conn.repository}/labels/${String(id)}`, {
1830
1842
  method: "DELETE",
1831
- headers: authHeaders$2(conn)
1843
+ headers: authHeaders(conn)
1832
1844
  }, { idempotent: true });
1833
1845
  if (!res.ok) await throwForgejoFailure(res, {
1834
1846
  verb: "delete label",
1835
1847
  resource: String(id)
1836
1848
  }, repoNotFoundMessage(conn));
1837
1849
  }
1838
- //#endregion
1839
- //#region ../forgejo/src/issues.ts
1850
+ // ../forgejo/src/issues.ts
1840
1851
  const IssueResponseSchema = z.object({
1841
1852
  number: z.number(),
1842
1853
  title: z.string(),
@@ -1881,7 +1892,7 @@ function normalizeIssue(raw) {
1881
1892
  };
1882
1893
  }
1883
1894
  async function getForgejoIssue(executor, conn, number) {
1884
- const res = await executor.fetch(`${issuesBase(conn)}/${String(number)}`, { headers: authHeaders$2(conn) });
1895
+ const res = await executor.fetch(`${issuesBase(conn)}/${String(number)}`, { headers: authHeaders(conn) });
1885
1896
  if (!res.ok) await throwForgejoFailure(res, {
1886
1897
  verb: "get issue",
1887
1898
  resource: `#${String(number)}`
@@ -1895,17 +1906,19 @@ const DependencyResponseSchema = z.object({
1895
1906
  });
1896
1907
  z.array(DependencyResponseSchema);
1897
1908
  function issueListParams(type, query) {
1898
- const params = new URLSearchParams(type === "all" ? {} : { type });
1909
+ const kind = query.type ?? type;
1910
+ const params = new URLSearchParams(kind === "all" ? {} : { type: kind });
1899
1911
  params.set("state", query.state);
1900
1912
  params.set("limit", String(query.limit));
1901
1913
  if (query.label !== void 0) params.set("labels", query.label);
1902
1914
  if (query.page !== void 0) params.set("page", String(query.page));
1903
1915
  if (query.since !== void 0) params.set("since", query.since);
1904
1916
  if (query.sort !== void 0) params.set("sort", query.sort);
1917
+ if (query.q !== void 0) params.set("q", query.q);
1905
1918
  return params;
1906
1919
  }
1907
1920
  async function fetchIssueListPage(executor, conn, params) {
1908
- const res = await executor.fetch(`${issuesBase(conn)}?${params.toString()}`, { headers: authHeaders$2(conn) });
1921
+ const res = await executor.fetch(`${issuesBase(conn)}?${params.toString()}`, { headers: authHeaders(conn) });
1909
1922
  if (!res.ok) await throwForgejoFailure(res, {
1910
1923
  verb: "list issues",
1911
1924
  resource: conn.repository
@@ -1939,6 +1952,29 @@ async function listForgejoIssuesDetailedPaged(executor, conn, query, options) {
1939
1952
  }
1940
1953
  return issues;
1941
1954
  }
1955
+ async function listOpenObjectsWithLabel(executor, conn, label) {
1956
+ const objects = [];
1957
+ for (let page = 1; page <= DETAILED_LIST_MAX_PAGES; page++) {
1958
+ const { raw } = await fetchIssueListPage(executor, conn, issueListParams("all", {
1959
+ state: "open",
1960
+ label,
1961
+ limit: DETAILED_LIST_PAGE_SIZE,
1962
+ page
1963
+ }));
1964
+ for (const issue of raw.map(normalizeIssue)) {
1965
+ if (!issue.labels.includes(label)) continue;
1966
+ objects.push({
1967
+ number: issue.number,
1968
+ kind: issue.isPullRequest ? "pull" : "issue",
1969
+ title: issue.title,
1970
+ url: issue.url
1971
+ });
1972
+ }
1973
+ if (raw.length === 0) break;
1974
+ if (page === DETAILED_LIST_MAX_PAGES) log.warn(`Open-object listing on ${conn.repository} hit the ${String(DETAILED_LIST_MAX_PAGES)}-page cap without an empty page; results may be truncated`);
1975
+ }
1976
+ return objects;
1977
+ }
1942
1978
  const USER_REPOS_PAGE_SIZE = 50;
1943
1979
  const USER_REPOS_MAX_PAGES = 20;
1944
1980
  const UserRepoSchema = z.object({
@@ -2030,8 +2066,7 @@ async function setForgejoIssueState(executor, conn, number, state) {
2030
2066
  }, issueNotFoundMessage(conn, number));
2031
2067
  return normalizeIssue(parseIssueOrThrow(await res.json()));
2032
2068
  }
2033
- //#endregion
2034
- //#region ../forgejo/src/label-sync.ts
2069
+ // ../forgejo/src/label-sync.ts
2035
2070
  function resolveSyncMode(check, dryRun) {
2036
2071
  if (check) return "check";
2037
2072
  if (dryRun) return "dry-run";
@@ -2059,24 +2094,77 @@ function outcomeFromAudit(conn, specs, audit, lists) {
2059
2094
  ...lists
2060
2095
  };
2061
2096
  }
2097
+ async function checkPruneCandidate(fetcher, conn, name) {
2098
+ let objects;
2099
+ try {
2100
+ objects = await listOpenObjectsWithLabel(fetcher, conn, name);
2101
+ } catch (error) {
2102
+ return {
2103
+ ok: false,
2104
+ reason: `could not check open objects: ${describeError(error)}`,
2105
+ objects: []
2106
+ };
2107
+ }
2108
+ if (objects.length === 0) return { ok: true };
2109
+ return {
2110
+ ok: false,
2111
+ reason: `on ${String(objects.length)} open object(s)`,
2112
+ objects
2113
+ };
2114
+ }
2062
2115
  async function syncOneRepo(params) {
2063
- const { fetcher, conn, specs, mode, prune, allowedPruneNames, resolveTargetSpecs } = params;
2116
+ const { fetcher, conn, specs, mode, prune, allowedPruneNames, resolveTargetSpecs, retiring, today } = params;
2064
2117
  const target = resolveTargetSpecs === void 0 ? {
2065
2118
  specs,
2066
2119
  omitted: [],
2067
2120
  aliasNames: []
2068
2121
  } : await resolveTargetSpecs(conn, specs);
2069
- const audit = await auditForgejoLabels(fetcher, conn, target.specs);
2070
- const stale = staleLabelNames(specs, audit.labelIds.keys()).filter((name) => !target.aliasNames.includes(name));
2071
- const pruneCandidates = prune ? [...target.omitted, ...stale].filter((name) => audit.labelIds.has(name) && (allowedPruneNames === void 0 || allowedPruneNames.includes(name))) : [];
2122
+ const retiringNames = new Set(retiring.map((spec) => spec.name));
2123
+ const fullAudit = await auditForgejoLabels(fetcher, conn, [...target.specs, ...retiring.map(retiringAsLabelSpec)]);
2124
+ const presentRetiring = retiring.filter((spec) => fullAudit.labelIds.has(spec.name));
2125
+ const audit = {
2126
+ ...fullAudit,
2127
+ missing: fullAudit.missing.filter((name) => !retiringNames.has(name))
2128
+ };
2129
+ const stale = staleLabelNames(specs, audit.labelIds.keys(), retiring).filter((name) => !target.aliasNames.includes(name));
2130
+ const expiredRetiring = presentRetiring.filter((spec) => isRetiringExpired(spec, today)).map((spec) => spec.name);
2131
+ const pruneCandidates = prune ? [
2132
+ ...target.omitted,
2133
+ ...stale,
2134
+ ...expiredRetiring
2135
+ ].filter((name) => audit.labelIds.has(name) && (allowedPruneNames === void 0 || allowedPruneNames.includes(name))) : [];
2136
+ const pruned = [];
2137
+ const refused = [];
2138
+ for (const name of pruneCandidates) {
2139
+ const verdict = await checkPruneCandidate(fetcher, conn, name);
2140
+ if (verdict.ok) {
2141
+ pruned.push(name);
2142
+ continue;
2143
+ }
2144
+ refused.push({
2145
+ name,
2146
+ reason: verdict.reason,
2147
+ objects: verdict.objects
2148
+ });
2149
+ }
2150
+ const retiringReports = [];
2151
+ for (const spec of presentRetiring) retiringReports.push({
2152
+ name: spec.name,
2153
+ replacement: spec.replacement,
2154
+ until: spec.until,
2155
+ expired: isRetiringExpired(spec, today),
2156
+ openObjects: await listOpenObjectsWithLabel(fetcher, conn, spec.name)
2157
+ });
2072
2158
  const outcome = outcomeFromAudit(conn, target.specs, audit, {
2073
2159
  omitted: target.omitted,
2074
- pruned: pruneCandidates,
2075
- stale: prune ? stale.filter((name) => !pruneCandidates.includes(name)) : stale
2160
+ pruned,
2161
+ stale: prune ? stale.filter((name) => !pruneCandidates.includes(name)) : stale,
2162
+ retiring: retiringReports,
2163
+ refused
2076
2164
  });
2077
2165
  if (mode === "apply") {
2078
- if (outcome.created.length > 0 || outcome.updated.length > 0) await ensureForgejoLabels(fetcher, conn, target.specs);
2079
- for (const name of pruneCandidates) {
2166
+ if (outcome.created.length > 0 || outcome.updated.length > 0) await ensureForgejoLabels(fetcher, conn, [...target.specs, ...presentRetiring.map(retiringAsLabelSpec)]);
2167
+ for (const name of pruned) {
2080
2168
  const id = audit.labelIds.get(name);
2081
2169
  if (id === void 0) throw new UnexpectedError(`Prune candidate "${name}" had no known id after the audit`);
2082
2170
  await deleteForgejoLabel(fetcher, conn, id);
@@ -2086,6 +2174,9 @@ async function syncOneRepo(params) {
2086
2174
  }
2087
2175
  async function syncLabelSet(params) {
2088
2176
  const { fetcher, targets, specs, setName, mode, contained, prune, pruneAllowlist } = params;
2177
+ const retiring = params.retiring ?? [];
2178
+ const today = params.today ?? "";
2179
+ if (retiring.length > 0 && params.today === void 0) throw new UnexpectedError("syncLabelSet was given retiring label specs but no `today` to compare their expiry against: an expired entry is a prune candidate, and a guessed date could delete a name whose retirement window is still open.");
2089
2180
  const specList = [...specs];
2090
2181
  const repos = [];
2091
2182
  const failures = [];
@@ -2098,7 +2189,9 @@ async function syncLabelSet(params) {
2098
2189
  mode,
2099
2190
  prune: prune === true,
2100
2191
  allowedPruneNames,
2101
- resolveTargetSpecs: params.resolveTargetSpecs
2192
+ resolveTargetSpecs: params.resolveTargetSpecs,
2193
+ retiring,
2194
+ today
2102
2195
  };
2103
2196
  if (contained !== true) {
2104
2197
  repos.push(await syncOneRepo(syncOneRepoParams));
@@ -2131,6 +2224,11 @@ function syncSummaryLines(result) {
2131
2224
  if (repo.omitted.length > 0) actions.push(`omitted (repo declares its own priority vocabulary): ${repo.omitted.join(", ")}`);
2132
2225
  if (repo.pruned.length > 0) actions.push(`${verb}prune: ${repo.pruned.join(", ")}`);
2133
2226
  if (repo.stale.length > 0) actions.push(`stale (present, not in this set): ${repo.stale.join(", ")}`);
2227
+ for (const refusal of repo.refused) {
2228
+ const objects = refusal.objects.map((object) => `#${String(object.number)} (${object.kind}) ${object.title}`).join(", ");
2229
+ actions.push(`refused to prune ${refusal.name}: ${refusal.reason}${objects === "" ? "" : ` (${objects})`}`);
2230
+ }
2231
+ for (const retiring of repo.retiring) actions.push(`retiring ${retiring.name}: replaced by ${retiring.replacement}, prunable after ${retiring.until}${retiring.expired ? " (expired)" : ""}; ${String(retiring.openObjects.length)} open object(s)`);
2134
2232
  if (actions.length === 0) {
2135
2233
  repoLines.push(`${repo.repository}: up to date (${String(repo.unchanged.length)} unchanged)`);
2136
2234
  continue;
@@ -2142,8 +2240,7 @@ function syncSummaryLines(result) {
2142
2240
  failureLines: result.failures.map((failure) => `${failure.repository}: failed: ${failure.error}`)
2143
2241
  };
2144
2242
  }
2145
- //#endregion
2146
- //#region src/label-sets-file.ts
2243
+ // src/label-sets-file.ts
2147
2244
  function readLabelSet(path, setName) {
2148
2245
  let raw;
2149
2246
  try {
@@ -2170,8 +2267,7 @@ function readLabelSet(path, setName) {
2170
2267
  }
2171
2268
  return specs;
2172
2269
  }
2173
- //#endregion
2174
- //#region src/labels.ts
2270
+ // src/labels.ts
2175
2271
  async function runLabelsList(fetcher, conn) {
2176
2272
  const labels = await fetchAllForgejoLabels(fetcher, conn);
2177
2273
  return {
@@ -2205,8 +2301,7 @@ function renderSync(result) {
2205
2301
  for (const line of repoLines) rootLogger.info(line);
2206
2302
  for (const line of failureLines) rootLogger.warn(line);
2207
2303
  }
2208
- //#endregion
2209
- //#region src/commands/labels.ts
2304
+ // src/commands/labels.ts
2210
2305
  const GITHUB_REFUSAL = "bst-fj labels requires a Forgejo repository";
2211
2306
  const listCommand$1 = defineCommand({
2212
2307
  meta: {
@@ -2270,7 +2365,7 @@ const syncCommand = defineCommand({
2270
2365
  },
2271
2366
  json: {
2272
2367
  type: "boolean",
2273
- description: "Emit one JSON document: set, mode (apply | check | dry-run), repos ([{repository, created, updated, unchanged, omitted, stale}], every field but repository a list of label names), failures ([{repository, error}]; always present, filled only under --fleet), drifted. drifted is true when any label is missing or drifted, and sets the exit code only under --check."
2368
+ description: "Emit one JSON document: set, mode (apply | check | dry-run), repos ([{repository, created, updated, unchanged, omitted, stale, retiring, refused}], every field but repository and those two a list of label names; retiring and refused are always empty here, since this command never prunes), failures ([{repository, error}]; always present, filled only under --fleet), drifted. drifted is true when any label is missing or drifted, and sets the exit code only under --check."
2274
2369
  }
2275
2370
  },
2276
2371
  async run({ args, rawArgs }) {
@@ -2313,8 +2408,7 @@ const labelsCommand = defineCommand({
2313
2408
  sync: syncCommand
2314
2409
  }
2315
2410
  });
2316
- //#endregion
2317
- //#region ../forgejo/src/releases.ts
2411
+ // ../forgejo/src/releases.ts
2318
2412
  const SETTLE_ATTEMPTS = 4;
2319
2413
  const SETTLE_BASE_DELAY_MS = 2e3;
2320
2414
  const SETTLE_MAX_DELAY_MS = 16e3;
@@ -2536,8 +2630,7 @@ z.looseObject({
2536
2630
  created: z.string().nullish()
2537
2631
  })
2538
2632
  });
2539
- //#endregion
2540
- //#region src/commands/release-create.ts
2633
+ // src/commands/release-create.ts
2541
2634
  const releaseCreateCommand = defineCommand({
2542
2635
  meta: {
2543
2636
  name: "release:create",
@@ -2586,8 +2679,7 @@ const releaseCreateCommand = defineCommand({
2586
2679
  else rootLogger.info(`Created Forgejo release for ${args.tag}`);
2587
2680
  }
2588
2681
  });
2589
- //#endregion
2590
- //#region ../forgejo/src/secrets.ts
2682
+ // ../forgejo/src/secrets.ts
2591
2683
  const ActionSecretListSchema = z.array(z.object({ name: z.string() }));
2592
2684
  const SECRETS_PAGE_SIZE = 50;
2593
2685
  const SECRETS_MAX_PAGES = 20;
@@ -2643,8 +2735,7 @@ async function deleteActionSecret(executor, conn, secretName) {
2643
2735
  resource: secretName
2644
2736
  }));
2645
2737
  }
2646
- //#endregion
2647
- //#region src/secrets.ts
2738
+ // src/secrets.ts
2648
2739
  async function runSecretsSet(fetcher, config) {
2649
2740
  if (!isValidSecretName(config.secretName)) throw new FatalError(`Invalid secret name "${config.secretName}". A name must match [A-Za-z_][A-Za-z0-9_]* and must not start with FORGEJO_, GITEA_, GITHUB_, or a digit.`);
2650
2741
  if (config.dryRun) {
@@ -2686,8 +2777,7 @@ async function runSecretsDelete(fetcher, config) {
2686
2777
  repository: config.conn.repository
2687
2778
  };
2688
2779
  }
2689
- //#endregion
2690
- //#region src/commands/secrets.ts
2780
+ // src/commands/secrets.ts
2691
2781
  function readSecretValue(body) {
2692
2782
  if (body) return body;
2693
2783
  try {
@@ -2816,15 +2906,14 @@ const secretsCommand = defineCommand({
2816
2906
  delete: deleteCommand
2817
2907
  }
2818
2908
  });
2819
- //#endregion
2820
- //#region ../forgejo/src/user.ts
2909
+ // ../forgejo/src/user.ts
2821
2910
  const AuthenticatedUserSchema = z.object({
2822
2911
  login: z.string(),
2823
2912
  email: z.string().nullish()
2824
2913
  });
2825
2914
  async function getAuthenticatedUser(executor, conn, signal = null) {
2826
2915
  const res = await executor.fetch(`${conn.serverUrl}/api/v1/user`, {
2827
- headers: authHeaders$2(conn),
2916
+ headers: authHeaders(conn),
2828
2917
  signal
2829
2918
  });
2830
2919
  if (!res.ok) return {
@@ -2840,8 +2929,7 @@ async function getAuthenticatedUser(executor, conn, signal = null) {
2840
2929
  email: parsed.data.email || null
2841
2930
  };
2842
2931
  }
2843
- //#endregion
2844
- //#region ../forgejo/src/webhooks.ts
2932
+ // ../forgejo/src/webhooks.ts
2845
2933
  const WebhookSchema = z.object({
2846
2934
  id: z.number(),
2847
2935
  type: z.string(),
@@ -2922,8 +3010,7 @@ async function searchRepos(executor, conn) {
2922
3010
  return parsed.data.data;
2923
3011
  })).map((r) => r.full_name);
2924
3012
  }
2925
- //#endregion
2926
- //#region src/commands/webhooks.ts
3013
+ // src/commands/webhooks.ts
2927
3014
  function collectGroup(label, scopeHint, status) {
2928
3015
  if (status.status === "ok") return status.hooks.length > 0 ? {
2929
3016
  label,
@@ -2999,8 +3086,7 @@ const webhooksCommand = defineCommand({
2999
3086
  renderText(inventory, conn.serverUrl);
3000
3087
  }
3001
3088
  });
3002
- //#endregion
3003
- //#region ../forgejo/src/actions.ts
3089
+ // ../forgejo/src/actions.ts
3004
3090
  function forgeTimestamp(value) {
3005
3091
  if (value === null || value === void 0 || value === "") return null;
3006
3092
  const ms = Date.parse(value);
@@ -3178,8 +3264,7 @@ async function dispatchWorkflow(executor, conn, options) {
3178
3264
  inputs: options.inputs
3179
3265
  };
3180
3266
  }
3181
- //#endregion
3182
- //#region ../forgejo/src/actions-log.ts
3267
+ // ../forgejo/src/actions-log.ts
3183
3268
  const CSI_SEQUENCE = /\u001b\[[0-?]*[ -/]*[@-~]/gu;
3184
3269
  const OSC_SEQUENCE = /\u001b\][^\u0007]*(?:\u0007|\u001b\\)/gu;
3185
3270
  const LONE_ESCAPE = /\u001b/gu;
@@ -3221,8 +3306,7 @@ function readActionsJobLog(text) {
3221
3306
  lines
3222
3307
  };
3223
3308
  }
3224
- //#endregion
3225
- //#region src/ci-log.ts
3309
+ // src/ci-log.ts
3226
3310
  const ACTIONS_RUN_URL = /\/actions\/runs\/(\d+)\/?(?:[?#]|$)/u;
3227
3311
  function parseCiLogTarget(target) {
3228
3312
  if (/^\d+$/.test(target)) return {
@@ -3313,8 +3397,7 @@ function renderCiLog(result) {
3313
3397
  for (const line of result.lines) lines.push(line.at === null ? line.text : `${line.at} ${line.text}`);
3314
3398
  return lines.join("\n");
3315
3399
  }
3316
- //#endregion
3317
- //#region src/commands/ci-log.ts
3400
+ // src/commands/ci-log.ts
3318
3401
  const ciLogCommand = defineCommand({
3319
3402
  meta: {
3320
3403
  name: "ci:log",
@@ -3368,13 +3451,7 @@ const ciLogCommand = defineCommand({
3368
3451
  out.text(renderCiLog(result));
3369
3452
  }
3370
3453
  });
3371
- //#endregion
3372
- //#region src/ci-runs.ts
3373
- function parseLimit$2(raw) {
3374
- const limit = Number(raw);
3375
- if (!Number.isInteger(limit) || limit < 1) throw new FatalError(`Invalid --limit "${raw}" (expected a positive integer).`);
3376
- return limit;
3377
- }
3454
+ // src/ci-runs.ts
3378
3455
  function resolveRunsRef(ref, pr) {
3379
3456
  if (ref !== void 0 && pr !== void 0) throw new FatalError("--ref and --pr are the same filter -- give one of them (--pr 273 for a pull request's own runs, --ref main for a branch's).");
3380
3457
  if (pr !== void 0) {
@@ -3399,7 +3476,7 @@ function resolveRunsRef(ref, pr) {
3399
3476
  };
3400
3477
  }
3401
3478
  async function runCiRuns(fetcher, options) {
3402
- const limit = parseLimit$2(options.limit);
3479
+ const limit = parseLimitFlag(options.limit);
3403
3480
  const resolved = resolveRunsRef(options.ref, options.pr);
3404
3481
  const conn = requireForgejoTarget(options.cwd, resolveIssueTarget(resolved.target, options.repo), "ci:runs is Forgejo-only -- gh run list already serves GitHub, first-class. Use that instead.");
3405
3482
  rootLogger.debug(`ci:runs repository=${conn.repository} limit=${String(limit)}`);
@@ -3450,8 +3527,7 @@ function renderCiRuns(result) {
3450
3527
  }
3451
3528
  return lines.join("\n");
3452
3529
  }
3453
- //#endregion
3454
- //#region src/commands/ci-runs.ts
3530
+ // src/commands/ci-runs.ts
3455
3531
  const ciRunsCommand = defineCommand({
3456
3532
  meta: {
3457
3533
  name: "ci:runs",
@@ -3516,8 +3592,7 @@ const ciRunsCommand = defineCommand({
3516
3592
  out.text(renderCiRuns(result));
3517
3593
  }
3518
3594
  });
3519
- //#endregion
3520
- //#region ../cli-kit/src/workflow-inputs.ts
3595
+ // ../cli-kit/src/workflow-inputs.ts
3521
3596
  const INPUT_FLAG = "--input";
3522
3597
  const INPUT_FLAG_EQ = "--input=";
3523
3598
  function collectInputArgs(rawArgs) {
@@ -3544,8 +3619,7 @@ function parseInputs(raw) {
3544
3619
  }
3545
3620
  return inputs;
3546
3621
  }
3547
- //#endregion
3548
- //#region src/workflow-dispatch.ts
3622
+ // src/workflow-dispatch.ts
3549
3623
  async function runWorkflowDispatch(fetcher, options) {
3550
3624
  const dispatched = await dispatchWorkflow(fetcher, requireForgejoTarget(options.cwd, void 0, "workflow:dispatch is Forgejo-only -- use gh workflow run on GitHub."), {
3551
3625
  ...options.repo === void 0 ? {} : { repository: options.repo },
@@ -3562,8 +3636,7 @@ async function runWorkflowDispatch(fetcher, options) {
3562
3636
  inputs: dispatched.inputs
3563
3637
  };
3564
3638
  }
3565
- //#endregion
3566
- //#region src/registry-actions.ts
3639
+ // src/registry-actions.ts
3567
3640
  const actionsSubCommands = {
3568
3641
  "ci:log": ciLogCommand,
3569
3642
  "ci:runs": ciRunsCommand,
@@ -3632,8 +3705,7 @@ const DirEntrySchema = z.looseObject({
3632
3705
  type: z.string()
3633
3706
  });
3634
3707
  z.array(DirEntrySchema);
3635
- //#endregion
3636
- //#region ../forgejo/src/branches.ts
3708
+ // ../forgejo/src/branches.ts
3637
3709
  const BranchEntrySchema = z.object({
3638
3710
  name: z.string(),
3639
3711
  protected: z.boolean().optional(),
@@ -3644,12 +3716,6 @@ const BranchEntrySchema = z.object({
3644
3716
  });
3645
3717
  const BRANCHES_PAGE_SIZE = 50;
3646
3718
  const DEFAULT_MAX_PAGES = 20;
3647
- function repoBase(conn) {
3648
- return `${conn.serverUrl}/api/v1/repos/${conn.repository}`;
3649
- }
3650
- function authHeaders(conn) {
3651
- return { Authorization: `token ${conn.token}` };
3652
- }
3653
3719
  async function listBranches(executor, conn, options = {}) {
3654
3720
  const signal = options.signal;
3655
3721
  const maxPages = options.maxPages ?? DEFAULT_MAX_PAGES;
@@ -3718,8 +3784,7 @@ async function deleteBranch(executor, conn, branch) {
3718
3784
  resource: `${conn.repository}/${branch}`
3719
3785
  });
3720
3786
  }
3721
- //#endregion
3722
- //#region src/branch.ts
3787
+ // src/branch.ts
3723
3788
  const REFUSAL_WORDS = {
3724
3789
  forbidden: "forbidden",
3725
3790
  locked: "locked",
@@ -3745,8 +3810,7 @@ function renderBranchList(listing) {
3745
3810
  if (listing.truncated) lines.push("Listing truncated at the page cap; more branches exist.");
3746
3811
  return lines.join("\n");
3747
3812
  }
3748
- //#endregion
3749
- //#region src/registry-branch.ts
3813
+ // src/registry-branch.ts
3750
3814
  const branchSubCommands = {
3751
3815
  "branch:delete": defineCommand({
3752
3816
  meta: {
@@ -3816,8 +3880,7 @@ const branchSubCommands = {
3816
3880
  }
3817
3881
  })
3818
3882
  };
3819
- //#endregion
3820
- //#region ../cli-kit/src/body-source.ts
3883
+ // ../cli-kit/src/body-source.ts
3821
3884
  function tryRead(read, label) {
3822
3885
  try {
3823
3886
  return read();
@@ -3837,8 +3900,7 @@ function readBodySource(body, bodyFile, options = {}) {
3837
3900
  if (!existsSync(bodyFile)) throw new FatalError(`${flags.file} not found: ${bodyFile}`);
3838
3901
  return tryRead(() => readFileSync(bodyFile, "utf-8"), `${flags.file} "${bodyFile}"`);
3839
3902
  }
3840
- //#endregion
3841
- //#region ../forgejo/src/issue-comments.ts
3903
+ // ../forgejo/src/issue-comments.ts
3842
3904
  const CommentResponseSchema = z.object({
3843
3905
  id: z.number(),
3844
3906
  body: z.string().nullish(),
@@ -3879,7 +3941,7 @@ async function listForgejoComments(executor, conn, number, options = {}) {
3879
3941
  limit: String(COMMENTS_PAGE_SIZE),
3880
3942
  page: String(page)
3881
3943
  });
3882
- const res = await executor.fetch(`${issuesBase(conn)}/${String(number)}/comments?${params.toString()}`, { headers: authHeaders$2(conn) });
3944
+ const res = await executor.fetch(`${issuesBase(conn)}/${String(number)}/comments?${params.toString()}`, { headers: authHeaders(conn) });
3883
3945
  if (!res.ok) await throwForgejoFailure(res, {
3884
3946
  verb: "list comments",
3885
3947
  resource: `#${String(number)}`
@@ -3912,8 +3974,7 @@ async function addForgejoComment(executor, conn, number, body) {
3912
3974
  }, issueNotFoundMessage(conn, number));
3913
3975
  return normalizeComment(parseCommentOrThrow(await res.json()));
3914
3976
  }
3915
- //#endregion
3916
- //#region ../forgejo/src/pr-title.ts
3977
+ // ../forgejo/src/pr-title.ts
3917
3978
  const WIP_PREFIXES = ["WIP:", "[WIP]"];
3918
3979
  function hasWipPrefix(title) {
3919
3980
  return matchedWipPrefix(title) !== void 0;
@@ -3927,8 +3988,7 @@ function stripWipPrefix(title) {
3927
3988
  if (prefix === void 0) return title;
3928
3989
  return title.slice(prefix.length).trimStart();
3929
3990
  }
3930
- //#endregion
3931
- //#region ../forgejo/src/pr.ts
3991
+ // ../forgejo/src/pr.ts
3932
3992
  const CreatedPrSchema = z.object({
3933
3993
  number: z.number(),
3934
3994
  html_url: z.string().optional()
@@ -3979,9 +4039,12 @@ function updatedBefore(since) {
3979
4039
  return (pr) => pr.updatedAt !== null && Date.parse(pr.updatedAt) < since;
3980
4040
  }
3981
4041
  async function listPulls(executor, conn, options = {}) {
4042
+ const startPage = options.startPage ?? 1;
4043
+ if (!Number.isInteger(startPage) || startPage < 1) throw new FatalError(`Cannot list PRs on ${conn.repository}: startPage must be an integer of at least 1, got ${String(startPage)}`);
4044
+ const lastPage = startPage + PULLS_MAX_PAGES - 1;
3982
4045
  const headers = { Authorization: `token ${conn.token}` };
3983
4046
  const pulls = [];
3984
- for (let page = 1; page <= PULLS_MAX_PAGES; page++) {
4047
+ for (let page = startPage; page <= lastPage; page++) {
3985
4048
  options.signal?.throwIfAborted();
3986
4049
  const params = new URLSearchParams({
3987
4050
  state: options.state ?? "all",
@@ -4021,9 +4084,9 @@ async function listPulls(executor, conn, options = {}) {
4021
4084
  if (options.stopWhen !== void 0 && items.some(options.stopWhen)) break;
4022
4085
  if (options.updatedSince !== void 0 && items.some(updatedBefore(options.updatedSince))) break;
4023
4086
  if (options.limit !== void 0 && pulls.length >= options.limit) break;
4024
- if (page === PULLS_MAX_PAGES) {
4087
+ if (page === lastPage) {
4025
4088
  log.warn(`PR listing on ${conn.repository} hit the ${String(PULLS_MAX_PAGES)}-page cap without an empty page; results may be truncated`);
4026
- options.onPageCap?.();
4089
+ options.onPageCap?.(lastPage);
4027
4090
  }
4028
4091
  }
4029
4092
  return pulls;
@@ -4195,8 +4258,7 @@ const PullReviewCommentSchema = z.object({
4195
4258
  user: z.object({ login: z.string() }).nullish()
4196
4259
  });
4197
4260
  z.array(PullReviewCommentSchema);
4198
- //#endregion
4199
- //#region ../forgejo/src/issue-api.ts
4261
+ // ../forgejo/src/issue-api.ts
4200
4262
  var IssueNumberRequiredError = class extends FatalError {
4201
4263
  constructor() {
4202
4264
  super("An issue number or issue URL is required.");
@@ -4223,8 +4285,7 @@ function forgejoIssueApi(fetcher, conn) {
4223
4285
  pullRequest: (number) => getForgejoPrDetail(fetcher, conn, number)
4224
4286
  };
4225
4287
  }
4226
- //#endregion
4227
- //#region ../forgejo/src/issue-messages.ts
4288
+ // ../forgejo/src/issue-messages.ts
4228
4289
  const ISSUE_LIST_JSON_HELP = "Emit one JSON document: issues ([{number, title, state, url, author, labels, createdAt, updatedAt, closedAt (null while open), isPullRequest, assignees, attachments ([{id, name, size, createdAt, downloadUrl}])}]), truncated.";
4229
4290
  const ISSUE_CREATE_JSON_HELP = "Emit one JSON document: number, url, assignees. A dry run reports number 0 and an empty url.";
4230
4291
  const ISSUE_STATE_JSON_HELP = "Emit one JSON document: number, url, action (close | reopen), state ({previous, current}), changed, comment? ({posted}; present only when a comment was given).";
@@ -4255,8 +4316,7 @@ function describeNoLabelsGiven(error, clearFlag) {
4255
4316
  function describeAttachmentExists(error, forceFlag) {
4256
4317
  return `"${error.filePath}" already exists -- pass ${forceFlag} to overwrite it.`;
4257
4318
  }
4258
- //#endregion
4259
- //#region ../forgejo/src/issue-run.ts
4319
+ // ../forgejo/src/issue-run.ts
4260
4320
  var NothingToUpdateError = class extends FatalError {
4261
4321
  constructor() {
4262
4322
  super("Nothing to change: neither a title nor a body was given.");
@@ -4362,7 +4422,7 @@ function parseState$1(state) {
4362
4422
  if (state === "open" || state === "closed" || state === "all") return state;
4363
4423
  throw new InvalidListQueryError("state", state);
4364
4424
  }
4365
- function parseLimit$1(raw) {
4425
+ function parseLimit(raw) {
4366
4426
  const limit = Number(raw);
4367
4427
  if (!Number.isInteger(limit) || limit <= 0) throw new InvalidListQueryError("limit", raw);
4368
4428
  return limit;
@@ -4375,7 +4435,7 @@ async function listIssues(resolve, request) {
4375
4435
  const label = normalizeLabel(request.label);
4376
4436
  const query = {
4377
4437
  state: parseState$1(request.state),
4378
- limit: parseLimit$1(request.limit),
4438
+ limit: parseLimit(request.limit),
4379
4439
  ...label === void 0 ? {} : { label }
4380
4440
  };
4381
4441
  const { api } = resolve(parseRepoTarget(request.repo));
@@ -4512,8 +4572,7 @@ function formatIssueList(result) {
4512
4572
  }
4513
4573
  return lines.join("\n");
4514
4574
  }
4515
- //#endregion
4516
- //#region src/issue-transport.ts
4575
+ // src/issue-transport.ts
4517
4576
  function githubRefusal(command) {
4518
4577
  return `bst-fj is Forgejo-only -- use \`gh\` or \`pnpm exec bst ${command}\` for a github.com repository.`;
4519
4578
  }
@@ -4536,8 +4595,7 @@ function forgejoIssueTransport(cwd, target, command) {
4536
4595
  target: resolved.target
4537
4596
  };
4538
4597
  }
4539
- //#endregion
4540
- //#region src/commands/issue-comment.ts
4598
+ // src/commands/issue-comment.ts
4541
4599
  const issueCommentCommand = defineCommand({
4542
4600
  meta: {
4543
4601
  name: "issue:comment",
@@ -4595,8 +4653,7 @@ const issueCommentCommand = defineCommand({
4595
4653
  }
4596
4654
  }
4597
4655
  });
4598
- //#endregion
4599
- //#region src/commands/issue-create.ts
4656
+ // src/commands/issue-create.ts
4600
4657
  const issueCreateCommand = defineCommand({
4601
4658
  meta: {
4602
4659
  name: "issue:create",
@@ -4669,8 +4726,7 @@ const issueCreateCommand = defineCommand({
4669
4726
  }
4670
4727
  }
4671
4728
  });
4672
- //#endregion
4673
- //#region ../forgejo/src/issue-run-download.ts
4729
+ // ../forgejo/src/issue-run-download.ts
4674
4730
  function isReadableFile(filePath) {
4675
4731
  try {
4676
4732
  readFileSync(filePath);
@@ -4763,8 +4819,7 @@ function formatDownload(result, dryRun) {
4763
4819
  for (const file of result.files) lines.push(` ${file.name} (${String(file.size)} bytes)`);
4764
4820
  return lines.join("\n");
4765
4821
  }
4766
- //#endregion
4767
- //#region src/commands/issue-download.ts
4822
+ // src/commands/issue-download.ts
4768
4823
  const issueDownloadCommand = defineCommand({
4769
4824
  meta: {
4770
4825
  name: "issue:download",
@@ -4823,8 +4878,7 @@ const issueDownloadCommand = defineCommand({
4823
4878
  }
4824
4879
  }
4825
4880
  });
4826
- //#endregion
4827
- //#region ../forgejo/src/issue-labels.ts
4881
+ // ../forgejo/src/issue-labels.ts
4828
4882
  const TimelineEntrySchema = z.object({
4829
4883
  id: z.number(),
4830
4884
  type: z.string(),
@@ -4857,7 +4911,7 @@ async function removeForgejoIssueLabel(executor, conn, number, name, labelIds) {
4857
4911
  const id = await resolveLabelId(executor, conn, name, labelIds);
4858
4912
  const res = await executor.fetch(`${issuesBase(conn)}/${String(number)}/labels/${String(id)}`, {
4859
4913
  method: "DELETE",
4860
- headers: authHeaders$2(conn)
4914
+ headers: authHeaders(conn)
4861
4915
  }, { idempotent: true });
4862
4916
  if (!res.ok) await throwForgejoFailure(res, {
4863
4917
  verb: "remove label",
@@ -4875,8 +4929,7 @@ async function replaceForgejoIssueLabels(executor, conn, number, labelIds) {
4875
4929
  resource: `#${String(number)}`
4876
4930
  }, issueNotFoundMessage(conn, number));
4877
4931
  }
4878
- //#endregion
4879
- //#region ../forgejo/src/issue-run-labels.ts
4932
+ // ../forgejo/src/issue-run-labels.ts
4880
4933
  async function labelIssue(resolve, request) {
4881
4934
  const ref = parseIssueRef(request.ref);
4882
4935
  const number = requireIssueNumber(ref);
@@ -4945,8 +4998,7 @@ function formatIssueSetLabels(result, dryRun) {
4945
4998
  if (result.added.length > 0) lines.push(` ${dryRun ? "would add" : "added"}: ${result.added.join(", ")}`);
4946
4999
  return lines.join("\n");
4947
5000
  }
4948
- //#endregion
4949
- //#region src/commands/issue-label.ts
5001
+ // src/commands/issue-label.ts
4950
5002
  function defineIssueLabelCommand(action) {
4951
5003
  const name = action === "add" ? "issue:label" : "issue:unlabel";
4952
5004
  return defineCommand({
@@ -5005,8 +5057,7 @@ function defineIssueLabelCommand(action) {
5005
5057
  }
5006
5058
  const issueLabelCommand = defineIssueLabelCommand("add");
5007
5059
  const issueUnlabelCommand = defineIssueLabelCommand("remove");
5008
- //#endregion
5009
- //#region src/commands/issue-set-labels.ts
5060
+ // src/commands/issue-set-labels.ts
5010
5061
  const issueSetLabelsCommand = defineCommand({
5011
5062
  meta: {
5012
5063
  name: "issue:set-labels",
@@ -5066,8 +5117,7 @@ const issueSetLabelsCommand = defineCommand({
5066
5117
  }
5067
5118
  }
5068
5119
  });
5069
- //#endregion
5070
- //#region ../forgejo/src/issue-run-state.ts
5120
+ // ../forgejo/src/issue-run-state.ts
5071
5121
  var EmptyCommentError = class extends FatalError {
5072
5122
  constructor() {
5073
5123
  super("The comment resolved to empty text.");
@@ -5163,8 +5213,7 @@ function formatIssueState(result, dryRun) {
5163
5213
  }
5164
5214
  return lines.join("\n");
5165
5215
  }
5166
- //#endregion
5167
- //#region src/commands/issue-state.ts
5216
+ // src/commands/issue-state.ts
5168
5217
  function defineIssueStateCommand(action) {
5169
5218
  const name = action === "close" ? "issue:close" : "issue:reopen";
5170
5219
  return defineCommand({
@@ -5240,8 +5289,7 @@ function defineIssueStateCommand(action) {
5240
5289
  }
5241
5290
  const issueCloseCommand = defineIssueStateCommand("close");
5242
5291
  const issueReopenCommand = defineIssueStateCommand("reopen");
5243
- //#endregion
5244
- //#region src/commands/issue-list.ts
5292
+ // src/commands/issue-list.ts
5245
5293
  const issueListCommand = defineCommand({
5246
5294
  meta: {
5247
5295
  name: "issue:list",
@@ -5299,8 +5347,7 @@ const issueListCommand = defineCommand({
5299
5347
  }
5300
5348
  }
5301
5349
  });
5302
- //#endregion
5303
- //#region src/commands/issue-update.ts
5350
+ // src/commands/issue-update.ts
5304
5351
  const issueUpdateCommand = defineCommand({
5305
5352
  meta: {
5306
5353
  name: "issue:update",
@@ -5371,8 +5418,7 @@ const issueUpdateCommand = defineCommand({
5371
5418
  }
5372
5419
  }
5373
5420
  });
5374
- //#endregion
5375
- //#region src/registry-issue.ts
5421
+ // src/registry-issue.ts
5376
5422
  const issueSubCommands = {
5377
5423
  "issue:view": defineCommand({
5378
5424
  meta: {
@@ -5425,8 +5471,7 @@ const issueSubCommands = {
5425
5471
  "issue:set-labels": issueSetLabelsCommand,
5426
5472
  "issue:download": issueDownloadCommand
5427
5473
  };
5428
- //#endregion
5429
- //#region src/pr-checks.ts
5474
+ // src/pr-checks.ts
5430
5475
  async function resolvePrChecksTarget(fetcher, options) {
5431
5476
  const ref = parseIssueRef(options.pr);
5432
5477
  if (options.repo !== void 0 && ref.number === void 0 && options.branch === void 0) throw new FatalError("--repo requires --pr or --branch -- the current branch of this checkout means nothing in another repository, and inferring it there would silently answer about the wrong PR.");
@@ -5521,75 +5566,45 @@ function buildReport(detail, checks, ctx, extras = {}) {
5521
5566
  return report;
5522
5567
  }
5523
5568
  async function waitForSettledChecks(fetcher, target, baseline, notices, poll) {
5524
- const { timeoutMs, intervalMs, sleep, now } = poll;
5525
- const startedAt = now();
5526
5569
  let currentDetail = target.detail;
5527
- let consecutiveFailures = 0;
5528
5570
  let consecutiveDetailFailures = 0;
5529
- let lastFailureDetail;
5530
- let lastGoodChecks;
5531
- let previousSettledContexts = null;
5532
- for (;;) {
5533
- const sha = currentDetail.head.sha;
5534
- const { checks, failureDetail } = await readPollChecks(fetcher, target.conn, sha);
5535
- if (failureDetail !== void 0) lastFailureDetail = failureDetail;
5536
- if (checks === void 0) {
5537
- consecutiveFailures += 1;
5538
- if (consecutiveFailures >= 3) return buildReport(currentDetail, EMPTY_CHECKS, {
5539
- target,
5540
- baseline,
5541
- notices,
5542
- waited: true,
5543
- elapsedMs: now() - startedAt
5544
- }, {
5545
- statusOverride: "unparseable",
5546
- detailOverride: lastFailureDetail
5547
- });
5548
- } else {
5549
- consecutiveFailures = 0;
5550
- lastGoodChecks = checks;
5551
- pushDisagreementNotice(notices, sha, checks);
5552
- if (isSettled(checks)) {
5553
- const contexts = new Set(checks.contexts.map((check) => check.context));
5554
- if (previousSettledContexts !== null && previousSettledContexts.size === contexts.size && [...previousSettledContexts].every((context) => contexts.has(context))) return buildReport(currentDetail, checks, {
5555
- target,
5556
- baseline,
5557
- notices,
5558
- waited: true,
5559
- elapsedMs: now() - startedAt
5560
- });
5561
- previousSettledContexts = contexts;
5562
- } else previousSettledContexts = null;
5563
- }
5564
- if (now() - startedAt >= timeoutMs) return buildReport(currentDetail, lastGoodChecks ?? EMPTY_CHECKS, {
5565
- target,
5566
- baseline,
5567
- notices,
5568
- waited: true,
5569
- elapsedMs: now() - startedAt
5570
- }, { statusOverride: "timeout" });
5571
- await sleep(intervalMs);
5572
- let nextDetail;
5573
- try {
5574
- nextDetail = await getForgejoPrDetail(fetcher, target.conn, target.prNumber);
5575
- consecutiveDetailFailures = 0;
5576
- } catch (error) {
5577
- consecutiveDetailFailures += 1;
5578
- if (consecutiveDetailFailures >= 3) throw error;
5579
- continue;
5571
+ const reportFor = (detail, checks, elapsedMs, extras = {}) => buildReport(detail, checks, {
5572
+ target,
5573
+ baseline,
5574
+ notices,
5575
+ waited: true,
5576
+ elapsedMs
5577
+ }, extras);
5578
+ return settleChecks(fetcher, target.conn, poll, {
5579
+ sha: () => currentDetail.head.sha,
5580
+ notices,
5581
+ report: (checks, extras) => {
5582
+ const reportExtras = {};
5583
+ if (extras.exit === "timeout") reportExtras.statusOverride = "timeout";
5584
+ else if (extras.exit === "retry-budget") {
5585
+ reportExtras.statusOverride = "unparseable";
5586
+ if (extras.failureDetail !== void 0) reportExtras.detailOverride = extras.failureDetail;
5587
+ }
5588
+ return reportFor(currentDetail, checks, extras.elapsed(), reportExtras);
5589
+ },
5590
+ afterSleep: async (sha, elapsed) => {
5591
+ let nextDetail;
5592
+ try {
5593
+ nextDetail = await getForgejoPrDetail(fetcher, target.conn, target.prNumber);
5594
+ consecutiveDetailFailures = 0;
5595
+ } catch (error) {
5596
+ consecutiveDetailFailures += 1;
5597
+ if (consecutiveDetailFailures >= 3) throw error;
5598
+ return "retry";
5599
+ }
5600
+ if (nextDetail.head.sha !== sha) return { done: reportFor(nextDetail, EMPTY_CHECKS, elapsed(), {
5601
+ statusOverride: "head-moved",
5602
+ previousHeadSha: sha
5603
+ }) };
5604
+ currentDetail = nextDetail;
5605
+ return "next";
5580
5606
  }
5581
- if (nextDetail.head.sha !== sha) return buildReport(nextDetail, EMPTY_CHECKS, {
5582
- target,
5583
- baseline,
5584
- notices,
5585
- waited: true,
5586
- elapsedMs: now() - startedAt
5587
- }, {
5588
- statusOverride: "head-moved",
5589
- previousHeadSha: sha
5590
- });
5591
- currentDetail = nextDetail;
5592
- }
5607
+ });
5593
5608
  }
5594
5609
  async function runPrChecks(fetcher, options) {
5595
5610
  const timings = options.wait === true ? resolvePollTimings(options) : void 0;
@@ -5622,13 +5637,7 @@ function baselineClause(report) {
5622
5637
  return `${String(redChecks.length)} red, ${String(preexistingCount)} of them already red on ` + report.baseline.ref;
5623
5638
  }
5624
5639
  function renderPrChecks(report) {
5625
- const lines = report.checks.map((check) => {
5626
- const parts = [check.state, check.context];
5627
- if (check.description !== null) parts.push(check.description);
5628
- let line = parts.join(" -- ");
5629
- if (check.verdict === "red" && check.targetUrl !== null) line += ` (${check.targetUrl})`;
5630
- return line;
5631
- });
5640
+ const lines = report.checks.map((check) => renderCheckLine(check));
5632
5641
  const summaryParts = [`head ${report.headSha.slice(0, 12)}: ${report.status}`];
5633
5642
  const clause = baselineClause(report);
5634
5643
  if (clause !== null) summaryParts.push(clause);
@@ -5636,8 +5645,7 @@ function renderPrChecks(report) {
5636
5645
  lines.push(summaryParts.join(" -- "));
5637
5646
  return lines.join("\n");
5638
5647
  }
5639
- //#endregion
5640
- //#region src/commands/pr-checks.ts
5648
+ // src/commands/pr-checks.ts
5641
5649
  const prChecksCommand = defineCommand({
5642
5650
  meta: {
5643
5651
  name: "pr:checks",
@@ -5701,8 +5709,7 @@ const prChecksCommand = defineCommand({
5701
5709
  process.exitCode = report.status === "green" ? 0 : 1;
5702
5710
  }
5703
5711
  });
5704
- //#endregion
5705
- //#region ../forgejo/src/pr-close.ts
5712
+ // ../forgejo/src/pr-close.ts
5706
5713
  async function closePullRequest(api, request) {
5707
5714
  rejectEmptyComment(request.comment);
5708
5715
  return changeState(api, request.number, {
@@ -5719,15 +5726,13 @@ function formatPrClose(result, dryRun) {
5719
5726
  return lines.join("\n");
5720
5727
  }
5721
5728
  const PR_CLOSE_JSON_HELP = "Emit one JSON document: number, url, action (close), state ({previous, current}), changed, merged? (present only when the pull request was already closed), comment? ({posted}; present only when a comment was given).";
5722
- //#endregion
5723
- //#region ../forgejo/src/pr-number.ts
5729
+ // ../forgejo/src/pr-number.ts
5724
5730
  function parsePrNumber(raw) {
5725
5731
  const value = Number(raw);
5726
5732
  if (!/^\d+$/.test(raw) || !Number.isSafeInteger(value) || value < 1) throw new FatalError(`Invalid pull request number "${raw}" (expected a positive integer).`);
5727
5733
  return value;
5728
5734
  }
5729
- //#endregion
5730
- //#region src/commands/pr-close.ts
5735
+ // src/commands/pr-close.ts
5731
5736
  const prCloseCommand = defineCommand({
5732
5737
  meta: {
5733
5738
  name: "pr:close",
@@ -5791,8 +5796,7 @@ const prCloseCommand = defineCommand({
5791
5796
  }
5792
5797
  }
5793
5798
  });
5794
- //#endregion
5795
- //#region ../forgejo/src/closing-refs.ts
5799
+ // ../forgejo/src/closing-refs.ts
5796
5800
  const CLOSING_REF = /(?:^|[\s([])(clos(?:e|es|ed)|fix(?:es|ed)?|resolv(?:e|es|ed))(?::[ \t]*|[ \t]+)((?:[\w.-]+\/[\w.-]+)?#\d+)(?![\w-])/gim;
5797
5801
  function capitalize(word) {
5798
5802
  return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
@@ -5822,8 +5826,7 @@ function carryForwardClosingRefs(source, target) {
5822
5826
  const refLines = missing.map((ref) => ref.text).join("\n");
5823
5827
  return target.trim() === "" ? refLines : `${target}\n\n${refLines}`;
5824
5828
  }
5825
- //#endregion
5826
- //#region ../forgejo/src/git.ts
5829
+ // ../forgejo/src/git.ts
5827
5830
  const GIT_LOCAL_TIMEOUT_MS = 6e4;
5828
5831
  const GIT_WRITE_TIMEOUT_MS = 3e5;
5829
5832
  function pickDefaultBranch(override, originHead) {
@@ -5934,8 +5937,7 @@ async function pushBranch(git, cwd, branch, env, forceWithLease = false) {
5934
5937
  });
5935
5938
  if (result.exitCode !== 0) throw new FatalError(`Failed to push ${branch}: ${result.stderr || result.stdout}`);
5936
5939
  }
5937
- //#endregion
5938
- //#region ../forgejo/src/pr-upsert.ts
5940
+ // ../forgejo/src/pr-upsert.ts
5939
5941
  const COMMIT_TYPES = /* @__PURE__ */ new Set([
5940
5942
  "feat",
5941
5943
  "fix",
@@ -6143,8 +6145,7 @@ async function runPrUpsert(git, host, options) {
6143
6145
  ...shape
6144
6146
  };
6145
6147
  }
6146
- //#endregion
6147
- //#region ../forgejo/src/pr-upsert-messages.ts
6148
+ // ../forgejo/src/pr-upsert-messages.ts
6148
6149
  function describePrModeError(error, commands) {
6149
6150
  if (error.reason === "already-open") {
6150
6151
  const createVerb = commands.create.split(" ").at(-1) ?? commands.create;
@@ -6223,8 +6224,7 @@ function formatPrUpdate(result, rewrites, flags) {
6223
6224
  body: null
6224
6225
  };
6225
6226
  }
6226
- //#endregion
6227
- //#region ../forgejo/src/pr-host.ts
6227
+ // ../forgejo/src/pr-host.ts
6228
6228
  function forgejoPrHost(fetcher, conn) {
6229
6229
  return {
6230
6230
  wipTitles: true,
@@ -6257,8 +6257,7 @@ function forgejoPrEditHost(fetcher, conn) {
6257
6257
  ready: (number, write) => updatePr(fetcher, conn, number, { title: write.title })
6258
6258
  };
6259
6259
  }
6260
- //#endregion
6261
- //#region src/git.ts
6260
+ // src/git.ts
6262
6261
  const TIMED_OUT_EXIT_CODE = 124;
6263
6262
  const ABORTED_EXIT_CODE = 143;
6264
6263
  function cliGit() {
@@ -6334,8 +6333,7 @@ function cliGit() {
6334
6333
  });
6335
6334
  }) };
6336
6335
  }
6337
- //#endregion
6338
- //#region src/pr-run.ts
6336
+ // src/pr-run.ts
6339
6337
  const COMMANDS = {
6340
6338
  create: "pnpm exec bst-fj pr:create",
6341
6339
  update: "pnpm exec bst-fj pr:update"
@@ -6369,8 +6367,7 @@ async function runForgejoPr(githubRefusal, options) {
6369
6367
  });
6370
6368
  }
6371
6369
  }
6372
- //#endregion
6373
- //#region src/commands/pr-create.ts
6370
+ // src/commands/pr-create.ts
6374
6371
  const prCreateCommand = defineCommand({
6375
6372
  meta: {
6376
6373
  name: "pr:create",
@@ -6439,8 +6436,7 @@ const prCreateCommand = defineCommand({
6439
6436
  if (summary.body !== null) out.text(`${summary.body.heading}\n${summary.body.text}`);
6440
6437
  }
6441
6438
  });
6442
- //#endregion
6443
- //#region ../forgejo/src/pr-files.ts
6439
+ // ../forgejo/src/pr-files.ts
6444
6440
  const DEFAULT_LIMIT = 100;
6445
6441
  const PrFileEntrySchema = z.object({
6446
6442
  filename: z.string(),
@@ -6485,8 +6481,7 @@ async function readPrFilesPage(executor, conn, prNumber, page, limit) {
6485
6481
  truncated: res.headers.get("x-hasmore") === "true"
6486
6482
  };
6487
6483
  }
6488
- //#endregion
6489
- //#region src/pr-files.ts
6484
+ // src/pr-files.ts
6490
6485
  function runPrFiles(fetcher, options) {
6491
6486
  const number = parsePrNumber(options.number);
6492
6487
  return listAllPrFiles(fetcher, requireForgejoTarget(options.cwd, parseRepoTarget(options.repo), "pr:files is Forgejo-only -- gh pr diff --name-only already serves GitHub."), number);
@@ -6497,8 +6492,7 @@ function renderPrFiles(listing) {
6497
6492
  if (listing.truncated) lines.push("Listing truncated; the pull request changes more files.");
6498
6493
  return lines.join("\n");
6499
6494
  }
6500
- //#endregion
6501
- //#region src/commands/pr-files.ts
6495
+ // src/commands/pr-files.ts
6502
6496
  const prFilesCommand = defineCommand({
6503
6497
  meta: {
6504
6498
  name: "pr:files",
@@ -6537,20 +6531,14 @@ const prFilesCommand = defineCommand({
6537
6531
  out.text(renderPrFiles(listing));
6538
6532
  }
6539
6533
  });
6540
- //#endregion
6541
- //#region src/pr-list.ts
6534
+ // src/pr-list.ts
6542
6535
  function parseState(state) {
6543
6536
  if (state === "open" || state === "closed" || state === "all") return state;
6544
6537
  throw new FatalError(`Invalid --state "${state}" (expected open, closed, or all).`);
6545
6538
  }
6546
- function parseLimit(raw) {
6547
- const limit = Number(raw);
6548
- if (!Number.isInteger(limit) || limit < 1) throw new FatalError(`Invalid --limit "${raw}" (expected a positive integer).`);
6549
- return limit;
6550
- }
6551
6539
  async function runPrList(fetcher, options) {
6552
6540
  const state = parseState(options.state);
6553
- const limit = parseLimit(options.limit);
6541
+ const limit = parseLimitFlag(options.limit);
6554
6542
  rootLogger.debug(`pr:list state=${state} limit=${String(limit)}`);
6555
6543
  const conn = requireForgejoTarget(options.cwd, parseRepoTarget(options.repo), "pr:list is Forgejo-only -- gh pr list already serves GitHub, first-class. Use that instead.");
6556
6544
  const notices = [];
@@ -6580,8 +6568,7 @@ function renderPrList(result) {
6580
6568
  return `#${String(pr.number)} ${statusLabel(pr)} ${branches}: ${pr.title ?? "(no title)"}${labels}${url}`;
6581
6569
  }).join("\n");
6582
6570
  }
6583
- //#endregion
6584
- //#region src/commands/pr-list.ts
6571
+ // src/commands/pr-list.ts
6585
6572
  const prListCommand = defineCommand({
6586
6573
  meta: {
6587
6574
  name: "pr:list",
@@ -6624,8 +6611,7 @@ const prListCommand = defineCommand({
6624
6611
  out.text(renderPrList(result));
6625
6612
  }
6626
6613
  });
6627
- //#endregion
6628
- //#region ../forgejo/src/pr-merge.ts
6614
+ // ../forgejo/src/pr-merge.ts
6629
6615
  async function mergePr(executor, conn, prNumber, options) {
6630
6616
  const url = `${conn.serverUrl}/api/v1/repos/${conn.repository}/pulls/${String(prNumber)}/merge`;
6631
6617
  const body = {
@@ -6728,8 +6714,7 @@ z.object({
6728
6714
  sha: z.string()
6729
6715
  })
6730
6716
  });
6731
- //#endregion
6732
- //#region ../forgejo/src/pr-merge-run.ts
6717
+ // ../forgejo/src/pr-merge-run.ts
6733
6718
  const MERGE_METHODS = [
6734
6719
  "merge",
6735
6720
  "rebase",
@@ -6800,8 +6785,7 @@ async function runPrMergeCore(fetcher, conn, number, options = {}) {
6800
6785
  }
6801
6786
  };
6802
6787
  }
6803
- //#endregion
6804
- //#region src/pr-merge.ts
6788
+ // src/pr-merge.ts
6805
6789
  function parseMethod(raw) {
6806
6790
  if (raw === void 0) return "merge";
6807
6791
  const method = MERGE_METHODS.find((candidate) => candidate === raw);
@@ -6828,8 +6812,7 @@ function renderPrMerge(result) {
6828
6812
  const what = `PR #${String(result.number)} (${result.method}) at ${result.headSha.slice(0, 12)}, checks ${result.checks}`;
6829
6813
  return result.merged ? `Merged ${what}.` : `Dry run: would merge ${what}.`;
6830
6814
  }
6831
- //#endregion
6832
- //#region src/commands/pr-merge.ts
6815
+ // src/commands/pr-merge.ts
6833
6816
  const prMergeCommand = defineCommand({
6834
6817
  meta: {
6835
6818
  name: "pr:merge",
@@ -6885,8 +6868,7 @@ const prMergeCommand = defineCommand({
6885
6868
  out.text(renderPrMerge(result));
6886
6869
  }
6887
6870
  });
6888
- //#endregion
6889
- //#region ../forgejo/src/pr-edit.ts
6871
+ // ../forgejo/src/pr-edit.ts
6890
6872
  const MERGEABLE_RECHECK_DELAY_MS = 2e3;
6891
6873
  function isOpen(live) {
6892
6874
  return live.state === "open" && live.merged === false;
@@ -7013,8 +6995,7 @@ function renderPrReady(result) {
7013
6995
  if (result.action === "would-ready") return `Dry run: pull request ${number} would stop being a draft, reading "${result.title}".`;
7014
6996
  return `Dry run: pull request ${number} would read "${result.title}".`;
7015
6997
  }
7016
- //#endregion
7017
- //#region src/registry-pr.ts
6998
+ // src/registry-pr.ts
7018
6999
  const prSubCommands = {
7019
7000
  "pr:checks": prChecksCommand,
7020
7001
  "pr:close": prCloseCommand,
@@ -7179,8 +7160,7 @@ const prSubCommands = {
7179
7160
  }
7180
7161
  })
7181
7162
  };
7182
- //#endregion
7183
- //#region src/registry.ts
7163
+ // src/registry.ts
7184
7164
  const bstFjSubCommands = {
7185
7165
  "commit:status": commitStatusCommand,
7186
7166
  labels: labelsCommand,
@@ -7192,8 +7172,7 @@ const bstFjSubCommands = {
7192
7172
  webhooks: webhooksCommand,
7193
7173
  ...issueSubCommands
7194
7174
  };
7195
- //#endregion
7196
- //#region src/bin.ts
7175
+ // src/bin.ts
7197
7176
  applyVerbosityFromArgs(process.argv.slice(2));
7198
7177
  rootLogger.debug(describeEnvFileLoad(envFileLoad));
7199
7178
  setForgejoLogger(rootLogger);
@@ -7205,5 +7184,4 @@ await runCli({
7205
7184
  subCommands: bstFjSubCommands,
7206
7185
  describeError: describeForgeError
7207
7186
  });
7208
- //#endregion
7209
7187
  export {};