@bensandee/forgejo-cli 0.4.3 → 0.5.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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/bin.mjs +237 -52
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -55,6 +55,7 @@ command takes `--json`, which prints exactly one JSON document, failures include
55
55
  | `pr:files` | List the files a pull request changes |
56
56
  | `pr:merge` | Merge an open, non-draft, mergeable PR pinned to the head it read; refuses closed, draft, conflicted, unconfirmed or non-green checks (zero statuses included) unless `--ignore-checks`; `--delete-branch` is opt-in |
57
57
  | `pr:ready` | Mark a draft pull request ready, dropping one `WIP:` prefix or taking `--title`, and confirm the forge no longer reports a draft |
58
+ | `pr:close` | Close an open pull request, optionally posting `--comment`/`--comment-file` first; reports an already-closed or merged PR without writing; refuses an issue |
58
59
  | `pr:retarget` | Repoint an open pull request at a new base branch, confirm the forge took it, and report its mergeability as advice |
59
60
  | `workflow:dispatch` | Dispatch a workflow with `--workflow`, `--ref` and repeatable `--input`; takes `--repo owner/repo`, not a URL |
60
61
  | `ci:runs` | List a repository's workflow runs with their jobs, filtered by workflow, ref, pull request, head SHA or status |
package/dist/bin.mjs CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync, writeSync } from "node:fs";
3
- import * as path$1 from "node:path";
4
- import path from "node:path";
3
+ import path, * as path$1 from "node:path";
5
4
  import { parseArgs, parseEnv } from "node:util";
6
5
  import { FatalError, TransientError, UnexpectedError, describeError, redact, redact as redact$1 } from "@bensandee/common";
7
6
  import { z } from "zod";
@@ -163,7 +162,7 @@ function loadEnvFileForBin(rawArgs, bin, io = processIo) {
163
162
  //#region src/version.ts
164
163
  const parsed = z.object({ version: z.string() }).safeParse({
165
164
  name: "@bensandee/forgejo-cli",
166
- version: "0.4.3",
165
+ version: "0.5.0",
167
166
  description: "bst-fj: Forgejo API commands without the rest of bst",
168
167
  license: "MIT",
169
168
  repository: {
@@ -2337,8 +2336,8 @@ async function createRelease(executor, conn, tag, options = {}) {
2337
2336
  const url = `${conn.serverUrl}/api/v1/repos/${conn.repository}/releases`;
2338
2337
  const payload = {
2339
2338
  tag_name: tag,
2340
- name: tag,
2341
- body: `Published ${tag}`,
2339
+ name: options.name ?? tag,
2340
+ body: options.body ?? `Published ${tag}`,
2342
2341
  prerelease: options.prerelease ?? false
2343
2342
  };
2344
2343
  if (options.target) payload["target_commitish"] = options.target;
@@ -2434,7 +2433,9 @@ async function ensureRelease(executor, conn, tag, options = {}) {
2434
2433
  try {
2435
2434
  await createRelease(executor, conn, tag, {
2436
2435
  target: options.target,
2437
- prerelease: options.prerelease
2436
+ prerelease: options.prerelease,
2437
+ name: options.name,
2438
+ body: options.body
2438
2439
  });
2439
2440
  } catch (error) {
2440
2441
  const raced = await getReleaseByTag(executor, conn, tag);
@@ -2458,6 +2459,8 @@ async function ensureRelease(executor, conn, tag, options = {}) {
2458
2459
  const recreated = options.target ? await recreateTagViaApi(executor, conn, tag, {
2459
2460
  target: options.target,
2460
2461
  prerelease: options.prerelease,
2462
+ name: options.name,
2463
+ body: options.body,
2461
2464
  current,
2462
2465
  attempts,
2463
2466
  baseDelayMs
@@ -2469,7 +2472,7 @@ async function ensureRelease(executor, conn, tag, options = {}) {
2469
2472
  throw new TransientError(`Release for ${tag} never got a real (non-epoch) created date after ${String(attempts)} attempts. This is an intermittent Forgejo bug: a pushed tag's date settles asynchronously and a racing release creation freezes it at the Unix epoch (1969/1970). ` + (options.target ? `Recreating the tag via the API did not help -- investigate the server or upgrade Forgejo.` : `Pass the release tag's target commit so the tag can be recreated, or retry the release.`));
2470
2473
  }
2471
2474
  async function recreateTagViaApi(executor, conn, tag, opts) {
2472
- const { target, prerelease, current, attempts, baseDelayMs } = opts;
2475
+ const { target, prerelease, name, body, current, attempts, baseDelayMs } = opts;
2473
2476
  log.warn(`Release for ${tag} stayed epoch-dated -- recreating the tag via the API so Forgejo stamps a real date (the tag becomes lightweight)`);
2474
2477
  if (current) await deleteRelease(executor, conn, current.id);
2475
2478
  await deleteTag(executor, conn, tag);
@@ -2477,7 +2480,9 @@ async function recreateTagViaApi(executor, conn, tag, opts) {
2477
2480
  try {
2478
2481
  await createRelease(executor, conn, tag, {
2479
2482
  target,
2480
- prerelease
2483
+ prerelease,
2484
+ name,
2485
+ body
2481
2486
  });
2482
2487
  } catch (error) {
2483
2488
  if (attempt >= attempts) throw new TransientError(`Release for ${tag} was deleted to repair its epoch date, but recreating it from ${target} failed: ${describeError(error)}. The tag no longer exists on the server -- re-push it (or re-run the release) to restore it.`);
@@ -2501,6 +2506,24 @@ async function settleBackoff(attempt, baseDelayMs) {
2501
2506
  const delay = Math.min(baseDelayMs * 2 ** (attempt - 1), SETTLE_MAX_DELAY_MS);
2502
2507
  await new Promise((resolve) => setTimeout(resolve, delay));
2503
2508
  }
2509
+ z.looseObject({
2510
+ id: z.number(),
2511
+ tag_name: z.string(),
2512
+ name: z.string().nullish(),
2513
+ target_commitish: z.string().nullish(),
2514
+ created_at: z.string().nullish(),
2515
+ html_url: z.string().nullish(),
2516
+ draft: z.boolean().optional(),
2517
+ prerelease: z.boolean().optional(),
2518
+ assets: z.array(z.unknown()).nullish()
2519
+ });
2520
+ z.looseObject({
2521
+ name: z.string(),
2522
+ commit: z.looseObject({
2523
+ sha: z.string(),
2524
+ created: z.string().nullish()
2525
+ })
2526
+ });
2504
2527
  //#endregion
2505
2528
  //#region src/commands/release-create.ts
2506
2529
  const releaseCreateCommand = defineCommand({
@@ -5040,6 +5063,9 @@ var EmptyCommentError = class extends FatalError {
5040
5063
  super("The comment resolved to empty text.");
5041
5064
  }
5042
5065
  };
5066
+ function rejectEmptyComment(comment) {
5067
+ if (comment !== void 0 && comment.trim() === "") throw new EmptyCommentError();
5068
+ }
5043
5069
  var PullRequestStateError = class extends FatalError {
5044
5070
  number;
5045
5071
  action;
@@ -5049,20 +5075,35 @@ var PullRequestStateError = class extends FatalError {
5049
5075
  this.action = action;
5050
5076
  }
5051
5077
  };
5078
+ var NotAPullRequestError = class extends FatalError {
5079
+ number;
5080
+ constructor(number) {
5081
+ super(`#${String(number)} is an issue, not a pull request.`);
5082
+ this.number = number;
5083
+ }
5084
+ };
5085
+ var StateNotTakenError = class extends FatalError {
5086
+ number;
5087
+ expected;
5088
+ actual;
5089
+ constructor(number, expected, actual) {
5090
+ super(`The forge did not change #${String(number)}: it is still ${actual}, not ${expected}.`);
5091
+ this.number = number;
5092
+ this.expected = expected;
5093
+ this.actual = actual;
5094
+ }
5095
+ };
5052
5096
  const TARGET_STATE = {
5053
5097
  close: "closed",
5054
5098
  reopen: "open"
5055
5099
  };
5056
- async function setIssueState(resolve, request) {
5057
- const ref = parseIssueRef(request.ref);
5058
- const number = requireIssueNumber(ref);
5059
- if (request.comment !== void 0 && request.comment.trim() === "") throw new EmptyCommentError();
5060
- const commentGiven = request.comment !== void 0;
5061
- const { api } = resolve(resolveIssueTarget(ref.target, request.repo));
5100
+ async function changeState(api, number, request, subject) {
5062
5101
  const before = await api.get(number);
5063
- if (before.isPullRequest) throw new PullRequestStateError(number, request.action);
5102
+ if (subject === "issue" && before.isPullRequest) throw new PullRequestStateError(number, request.action);
5103
+ if (subject === "pull-request" && !before.isPullRequest) throw new NotAPullRequestError(number);
5104
+ const commentGiven = request.comment !== void 0;
5064
5105
  const targetState = TARGET_STATE[request.action];
5065
- const buildResult = (url, current, changed, posted) => ({
5106
+ const buildResult = (url, current, changed, posted, merged) => ({
5066
5107
  number,
5067
5108
  url,
5068
5109
  action: request.action,
@@ -5071,20 +5112,32 @@ async function setIssueState(resolve, request) {
5071
5112
  current
5072
5113
  },
5073
5114
  changed,
5074
- ...commentGiven ? { comment: { posted } } : {}
5115
+ ...commentGiven ? { comment: { posted } } : {},
5116
+ ...merged === void 0 ? {} : { merged }
5075
5117
  });
5076
- if (before.state === targetState) return buildResult(before.url, before.state, false, false);
5118
+ if (before.state === targetState) {
5119
+ const merged = subject === "pull-request" ? (await api.pullRequest(number)).merged : void 0;
5120
+ return buildResult(before.url, before.state, false, false, merged);
5121
+ }
5077
5122
  if (request.dryRun) return buildResult(before.url, targetState, true, true);
5078
5123
  if (commentGiven) await api.addComment(number, request.comment ?? "");
5079
5124
  let after;
5080
5125
  try {
5081
5126
  after = await api.setState(number, targetState);
5127
+ if (after.state !== targetState) throw new StateNotTakenError(number, targetState, after.state);
5082
5128
  } catch (error) {
5083
5129
  if (commentGiven && error instanceof Error) error.message = `The comment on #${String(number)} was already posted; a retry will duplicate it. ` + error.message;
5084
5130
  throw error;
5085
5131
  }
5086
5132
  return buildResult(after.url, after.state, true, true);
5087
5133
  }
5134
+ async function setIssueState(resolve, request) {
5135
+ const ref = parseIssueRef(request.ref);
5136
+ const number = requireIssueNumber(ref);
5137
+ rejectEmptyComment(request.comment);
5138
+ const { api } = resolve(resolveIssueTarget(ref.target, request.repo));
5139
+ return changeState(api, number, request, "issue");
5140
+ }
5088
5141
  const PAST_TENSE = {
5089
5142
  close: "Closed",
5090
5143
  reopen: "Reopened"
@@ -5163,10 +5216,13 @@ function defineIssueStateCommand(action) {
5163
5216
  } catch (error) {
5164
5217
  if (error instanceof IssueNumberRequiredError) throw new FatalError(describeIssueNumberRequired(`bst-fj ${name} 273`));
5165
5218
  if (error instanceof EmptyCommentError) throw new FatalError(describeEmptyComment(flags));
5166
- if (error instanceof PullRequestStateError) throw new FatalError(describePullRequestState(error, {
5167
- command: `bst-fj ${name}`,
5168
- advice: "Use the forge UI to change a pull request's state."
5169
- }));
5219
+ if (error instanceof PullRequestStateError) {
5220
+ const advice = action === "close" ? `Run \`bst-fj pr:close ${String(error.number)}\` to close it.` : "Use the forge UI to change a pull request's state.";
5221
+ throw new FatalError(describePullRequestState(error, {
5222
+ command: `bst-fj ${name}`,
5223
+ advice
5224
+ }));
5225
+ }
5170
5226
  throw error;
5171
5227
  }
5172
5228
  }
@@ -5636,6 +5692,95 @@ const prChecksCommand = defineCommand({
5636
5692
  }
5637
5693
  });
5638
5694
  //#endregion
5695
+ //#region ../forgejo/src/pr-close.ts
5696
+ async function closePullRequest(api, request) {
5697
+ rejectEmptyComment(request.comment);
5698
+ return changeState(api, request.number, {
5699
+ action: "close",
5700
+ dryRun: request.dryRun,
5701
+ ...request.comment === void 0 ? {} : { comment: request.comment }
5702
+ }, "pull-request");
5703
+ }
5704
+ function formatPrClose(result, dryRun) {
5705
+ if (result.changed) return formatIssueState(result, dryRun);
5706
+ const what = result.merged === true ? "merged" : "closed";
5707
+ const lines = [`#${String(result.number)} is already ${what} -- nothing to close. (${result.url})`];
5708
+ if (result.comment !== void 0) lines.push(" comment: not posted (pull request was already closed)");
5709
+ return lines.join("\n");
5710
+ }
5711
+ 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).";
5712
+ //#endregion
5713
+ //#region ../forgejo/src/pr-number.ts
5714
+ function parsePrNumber(raw) {
5715
+ const value = Number(raw);
5716
+ if (!/^\d+$/.test(raw) || !Number.isSafeInteger(value) || value < 1) throw new FatalError(`Invalid pull request number "${raw}" (expected a positive integer).`);
5717
+ return value;
5718
+ }
5719
+ //#endregion
5720
+ //#region src/commands/pr-close.ts
5721
+ const prCloseCommand = defineCommand({
5722
+ meta: {
5723
+ name: "pr:close",
5724
+ description: "Close an open pull request, optionally posting a comment first. A pull request already closed or merged is reported, not an error, and no comment is posted. An issue is refused (Forgejo-only; use gh pr close on GitHub)"
5725
+ },
5726
+ args: {
5727
+ number: {
5728
+ type: "positional",
5729
+ required: true,
5730
+ description: "Pull request number"
5731
+ },
5732
+ comment: {
5733
+ type: "string",
5734
+ description: "Post this comment before writing state"
5735
+ },
5736
+ "comment-file": {
5737
+ type: "string",
5738
+ description: "Read the comment from a file ('-' for stdin)"
5739
+ },
5740
+ repo: {
5741
+ type: "string",
5742
+ description: "Target repository URL (default: this repo's origin)"
5743
+ },
5744
+ "dry-run": {
5745
+ type: "boolean",
5746
+ description: "Report what would change, mutate nothing"
5747
+ },
5748
+ json: {
5749
+ type: "boolean",
5750
+ description: PR_CLOSE_JSON_HELP
5751
+ },
5752
+ verbose: {
5753
+ type: "boolean",
5754
+ description: "Emit detailed debug output"
5755
+ }
5756
+ },
5757
+ async run({ args }) {
5758
+ const out = createCliOutput(args.json === true);
5759
+ const flags = {
5760
+ text: "--comment",
5761
+ file: "--comment-file"
5762
+ };
5763
+ const number = parsePrNumber(args.number);
5764
+ const comment = readBodySource(args.comment ?? void 0, args["comment-file"] ?? void 0, { flags });
5765
+ const conn = requireForgejoTargetFromRepo(process.cwd(), args.repo ?? void 0, "pr:close is Forgejo-only -- use gh pr close <n> on GitHub.");
5766
+ const dryRun = args["dry-run"] === true;
5767
+ try {
5768
+ const result = await closePullRequest(forgejoIssueApi(cliFetcher(), conn), {
5769
+ number,
5770
+ dryRun,
5771
+ ...comment === void 0 ? {} : { comment }
5772
+ });
5773
+ out.json(result);
5774
+ out.text(formatPrClose(result, dryRun));
5775
+ if (dryRun && comment !== void 0 && result.comment?.posted === true) out.text(`[dry-run] Comment that would be posted:\n${comment}`);
5776
+ } catch (error) {
5777
+ if (error instanceof EmptyCommentError) throw new FatalError(describeEmptyComment(flags));
5778
+ if (error instanceof NotAPullRequestError) throw new FatalError(`#${String(error.number)} is an issue, not a pull request -- pr:close refuses to write issue state. Use \`bst-fj issue:close ${String(error.number)}\` to close it.`);
5779
+ throw error;
5780
+ }
5781
+ }
5782
+ });
5783
+ //#endregion
5639
5784
  //#region ../forgejo/src/closing-refs.ts
5640
5785
  const CLOSING_REF = /(?:^|[\s([])(clos(?:e|es|ed)|fix(?:es|ed)?|resolv(?:e|es|ed))(?::[ \t]*|[ \t]+)((?:[\w.-]+\/[\w.-]+)?#\d+)(?![\w-])/gim;
5641
5786
  function capitalize(word) {
@@ -6315,13 +6460,6 @@ async function readPrFilesPage(executor, conn, prNumber, page, limit) {
6315
6460
  };
6316
6461
  }
6317
6462
  //#endregion
6318
- //#region ../forgejo/src/pr-number.ts
6319
- function parsePrNumber(raw) {
6320
- const value = Number(raw);
6321
- if (!/^\d+$/.test(raw) || !Number.isSafeInteger(value) || value < 1) throw new FatalError(`Invalid pull request number "${raw}" (expected a positive integer).`);
6322
- return value;
6323
- }
6324
- //#endregion
6325
6463
  //#region src/pr-files.ts
6326
6464
  function runPrFiles(fetcher, options) {
6327
6465
  const number = parsePrNumber(options.number);
@@ -6565,36 +6703,52 @@ z.object({
6565
6703
  })
6566
6704
  });
6567
6705
  //#endregion
6568
- //#region src/pr-merge.ts
6569
- const METHODS = [
6706
+ //#region ../forgejo/src/pr-merge-run.ts
6707
+ const MERGE_METHODS = [
6570
6708
  "merge",
6571
6709
  "rebase",
6572
6710
  "squash"
6573
6711
  ];
6574
6712
  const RECHECK_DELAY_MS = 2e3;
6575
- function parseMethod(raw) {
6576
- if (raw === void 0) return "merge";
6577
- const method = METHODS.find((candidate) => candidate === raw);
6578
- if (method === void 0) throw new FatalError(`Invalid --method "${raw}" (expected one of: ${METHODS.join(", ")}).`);
6579
- return method;
6713
+ const MIN_SHA_PREFIX = 7;
6714
+ function sleepDefault(ms) {
6715
+ return new Promise((resolve) => {
6716
+ setTimeout(resolve, ms);
6717
+ });
6580
6718
  }
6581
- async function runPrMerge(fetcher, options) {
6582
- const number = parsePrNumber(options.number);
6583
- const method = parseMethod(options.method);
6584
- const conn = requireForgejoTarget(options.cwd, parseRepoTarget(options.repo), "pr:merge is Forgejo-only -- use gh pr merge on GitHub.");
6719
+ function headMatches(expected, actual) {
6720
+ const want = expected.trim().toLowerCase();
6721
+ return want.length >= MIN_SHA_PREFIX && actual.toLowerCase().startsWith(want);
6722
+ }
6723
+ async function runPrMergeCore(fetcher, conn, number, options = {}) {
6585
6724
  const pr = `PR #${String(number)}`;
6725
+ const method = options.method ?? "merge";
6726
+ const expected = options.expectedHeadSha;
6727
+ const moved = (headSha) => expected !== void 0 && !headMatches(expected, headSha);
6586
6728
  const confirmation = await confirmMergeable(fetcher, conn, number, {
6587
6729
  delayMs: RECHECK_DELAY_MS,
6588
- sleep: options.sleep ?? sleep,
6589
- settleOnFirstRead: (gate) => !gate.open || gate.draft
6730
+ sleep: options.sleep ?? sleepDefault,
6731
+ settleOnFirstRead: (gate) => !gate.open || gate.draft || moved(gate.headSha)
6590
6732
  });
6591
6733
  const { gate, verdict } = confirmation;
6592
- if (!gate.open) throw new FatalError(`Refusing to merge ${pr}: it is closed.`);
6593
- if (gate.draft) throw new FatalError(`Refusing to merge ${pr}: it is a draft.`);
6594
- if (verdict === "conflicted") throw new FatalError(`Refusing to merge ${pr}: it has conflicts. ${confirmation.evidence ?? ""}`.trimEnd());
6595
- if (verdict === "unconfirmed") throw new FatalError(`Refusing to merge ${pr}: the forge could not confirm it is mergeable. ${confirmation.evidence ?? ""}`.trimEnd());
6596
- const checks = classifyBaseStatus(await readCommitChecks(fetcher, conn, gate.headSha));
6597
- if (checks !== "green" && !options.ignoreChecks) throw new FatalError(`Refusing to merge ${pr}: its checks at ${gate.headSha} are ${checks}, not green. Pass --ignore-checks to merge anyway.`);
6734
+ const refuse = (why) => ({
6735
+ kind: "refused",
6736
+ reason: `Refusing to merge ${pr}: ${why}`
6737
+ });
6738
+ if (!gate.open) return refuse("it is closed.");
6739
+ if (gate.draft) return refuse("it is a draft.");
6740
+ if (options.refuseEmpty === true && (gate.headSha === gate.mergeBase || gate.changedFiles === 0)) return refuse("its diff against its base is empty, so its branch is left alone.");
6741
+ if (expected !== void 0 && moved(gate.headSha)) return refuse(`its head is ${gate.headSha}, not the approved ${expected}.`);
6742
+ if (verdict === "conflicted") return refuse(`it has conflicts. ${confirmation.evidence ?? ""}`.trimEnd());
6743
+ if (verdict === "unconfirmed") return refuse(`the forge could not confirm it is mergeable. ${confirmation.evidence ?? ""}`.trimEnd());
6744
+ const read = await readCommitChecks(fetcher, conn, gate.headSha);
6745
+ const checks = classifyBaseStatus(read);
6746
+ const skippedOnly = read.parsed && read.contexts.length > 0 && read.contexts.every((context) => context.verdict === "skipped");
6747
+ if (!(checks === "green" || options.ignoreChecks === true || (checks === "no-status" || skippedOnly) && options.allowNoStatus === true)) return {
6748
+ kind: "refused",
6749
+ reason: `Refusing to merge ${pr}: its checks at ${gate.headSha} are ${checks}, not green.`,
6750
+ checks
6751
+ };
6598
6752
  const result = {
6599
6753
  number,
6600
6754
  merged: false,
@@ -6603,17 +6757,47 @@ async function runPrMerge(fetcher, options) {
6603
6757
  verdict,
6604
6758
  checks
6605
6759
  };
6606
- if (options.dryRun) return result;
6760
+ if (options.dryRun === true) return {
6761
+ kind: "ok",
6762
+ result
6763
+ };
6607
6764
  await mergePr(fetcher, conn, number, {
6608
6765
  method,
6609
- deleteBranch: options.deleteBranch,
6766
+ deleteBranch: options.deleteBranch ?? false,
6610
6767
  expectedHeadSha: gate.headSha
6611
6768
  });
6612
6769
  return {
6613
- ...result,
6614
- merged: true
6770
+ kind: "ok",
6771
+ result: {
6772
+ ...result,
6773
+ merged: true
6774
+ }
6615
6775
  };
6616
6776
  }
6777
+ //#endregion
6778
+ //#region src/pr-merge.ts
6779
+ function parseMethod(raw) {
6780
+ if (raw === void 0) return "merge";
6781
+ const method = MERGE_METHODS.find((candidate) => candidate === raw);
6782
+ if (method === void 0) throw new FatalError(`Invalid --method "${raw}" (expected one of: ${MERGE_METHODS.join(", ")}).`);
6783
+ return method;
6784
+ }
6785
+ async function runPrMerge(fetcher, options) {
6786
+ const number = parsePrNumber(options.number);
6787
+ const method = parseMethod(options.method);
6788
+ const outcome = await runPrMergeCore(fetcher, requireForgejoTarget(options.cwd, parseRepoTarget(options.repo), "pr:merge is Forgejo-only -- use gh pr merge on GitHub."), number, {
6789
+ method,
6790
+ deleteBranch: options.deleteBranch,
6791
+ ignoreChecks: options.ignoreChecks,
6792
+ dryRun: options.dryRun,
6793
+ sleep: options.sleep ?? sleep
6794
+ });
6795
+ if (outcome.kind === "refused") {
6796
+ const hint = outcome.checks === void 0 ? "" : " Pass --ignore-checks to merge anyway.";
6797
+ throw new FatalError(`${outcome.reason}${hint}`);
6798
+ }
6799
+ return outcome.result;
6800
+ }
6617
6801
  function renderPrMerge(result) {
6618
6802
  const what = `PR #${String(result.number)} (${result.method}) at ${result.headSha.slice(0, 12)}, checks ${result.checks}`;
6619
6803
  return result.merged ? `Merged ${what}.` : `Dry run: would merge ${what}.`;
@@ -6807,6 +6991,7 @@ function renderPrReady(result) {
6807
6991
  //#region src/registry-pr.ts
6808
6992
  const prSubCommands = {
6809
6993
  "pr:checks": prChecksCommand,
6994
+ "pr:close": prCloseCommand,
6810
6995
  "pr:create": prCreateCommand,
6811
6996
  "pr:files": prFilesCommand,
6812
6997
  "pr:list": prListCommand,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bensandee/forgejo-cli",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "description": "bst-fj: Forgejo API commands without the rest of bst",
5
5
  "license": "MIT",
6
6
  "repository": {