@bensandee/forgejo-cli 0.4.4 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/bin.mjs +140 -24
- 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.
|
|
165
|
+
version: "0.5.1",
|
|
167
166
|
description: "bst-fj: Forgejo API commands without the rest of bst",
|
|
168
167
|
license: "MIT",
|
|
169
168
|
repository: {
|
|
@@ -5064,6 +5063,9 @@ var EmptyCommentError = class extends FatalError {
|
|
|
5064
5063
|
super("The comment resolved to empty text.");
|
|
5065
5064
|
}
|
|
5066
5065
|
};
|
|
5066
|
+
function rejectEmptyComment(comment) {
|
|
5067
|
+
if (comment !== void 0 && comment.trim() === "") throw new EmptyCommentError();
|
|
5068
|
+
}
|
|
5067
5069
|
var PullRequestStateError = class extends FatalError {
|
|
5068
5070
|
number;
|
|
5069
5071
|
action;
|
|
@@ -5073,20 +5075,35 @@ var PullRequestStateError = class extends FatalError {
|
|
|
5073
5075
|
this.action = action;
|
|
5074
5076
|
}
|
|
5075
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
|
+
};
|
|
5076
5096
|
const TARGET_STATE = {
|
|
5077
5097
|
close: "closed",
|
|
5078
5098
|
reopen: "open"
|
|
5079
5099
|
};
|
|
5080
|
-
async function
|
|
5081
|
-
const ref = parseIssueRef(request.ref);
|
|
5082
|
-
const number = requireIssueNumber(ref);
|
|
5083
|
-
if (request.comment !== void 0 && request.comment.trim() === "") throw new EmptyCommentError();
|
|
5084
|
-
const commentGiven = request.comment !== void 0;
|
|
5085
|
-
const { api } = resolve(resolveIssueTarget(ref.target, request.repo));
|
|
5100
|
+
async function changeState(api, number, request, subject) {
|
|
5086
5101
|
const before = await api.get(number);
|
|
5087
|
-
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;
|
|
5088
5105
|
const targetState = TARGET_STATE[request.action];
|
|
5089
|
-
const buildResult = (url, current, changed, posted) => ({
|
|
5106
|
+
const buildResult = (url, current, changed, posted, merged) => ({
|
|
5090
5107
|
number,
|
|
5091
5108
|
url,
|
|
5092
5109
|
action: request.action,
|
|
@@ -5095,20 +5112,32 @@ async function setIssueState(resolve, request) {
|
|
|
5095
5112
|
current
|
|
5096
5113
|
},
|
|
5097
5114
|
changed,
|
|
5098
|
-
...commentGiven ? { comment: { posted } } : {}
|
|
5115
|
+
...commentGiven ? { comment: { posted } } : {},
|
|
5116
|
+
...merged === void 0 ? {} : { merged }
|
|
5099
5117
|
});
|
|
5100
|
-
if (before.state === targetState)
|
|
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
|
+
}
|
|
5101
5122
|
if (request.dryRun) return buildResult(before.url, targetState, true, true);
|
|
5102
5123
|
if (commentGiven) await api.addComment(number, request.comment ?? "");
|
|
5103
5124
|
let after;
|
|
5104
5125
|
try {
|
|
5105
5126
|
after = await api.setState(number, targetState);
|
|
5127
|
+
if (after.state !== targetState) throw new StateNotTakenError(number, targetState, after.state);
|
|
5106
5128
|
} catch (error) {
|
|
5107
5129
|
if (commentGiven && error instanceof Error) error.message = `The comment on #${String(number)} was already posted; a retry will duplicate it. ` + error.message;
|
|
5108
5130
|
throw error;
|
|
5109
5131
|
}
|
|
5110
5132
|
return buildResult(after.url, after.state, true, true);
|
|
5111
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
|
+
}
|
|
5112
5141
|
const PAST_TENSE = {
|
|
5113
5142
|
close: "Closed",
|
|
5114
5143
|
reopen: "Reopened"
|
|
@@ -5187,10 +5216,13 @@ function defineIssueStateCommand(action) {
|
|
|
5187
5216
|
} catch (error) {
|
|
5188
5217
|
if (error instanceof IssueNumberRequiredError) throw new FatalError(describeIssueNumberRequired(`bst-fj ${name} 273`));
|
|
5189
5218
|
if (error instanceof EmptyCommentError) throw new FatalError(describeEmptyComment(flags));
|
|
5190
|
-
if (error instanceof PullRequestStateError)
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
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
|
+
}
|
|
5194
5226
|
throw error;
|
|
5195
5227
|
}
|
|
5196
5228
|
}
|
|
@@ -5660,6 +5692,96 @@ const prChecksCommand = defineCommand({
|
|
|
5660
5692
|
}
|
|
5661
5693
|
});
|
|
5662
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 dryRun = args["dry-run"] === true;
|
|
5766
|
+
try {
|
|
5767
|
+
rejectEmptyComment(comment);
|
|
5768
|
+
const conn = requireForgejoTargetFromRepo(process.cwd(), args.repo ?? void 0, "pr:close is Forgejo-only -- use gh pr close <n> on GitHub.");
|
|
5769
|
+
const result = await closePullRequest(forgejoIssueApi(cliFetcher(), conn), {
|
|
5770
|
+
number,
|
|
5771
|
+
dryRun,
|
|
5772
|
+
...comment === void 0 ? {} : { comment }
|
|
5773
|
+
});
|
|
5774
|
+
out.json(result);
|
|
5775
|
+
out.text(formatPrClose(result, dryRun));
|
|
5776
|
+
if (dryRun && comment !== void 0 && result.comment?.posted === true) out.text(`[dry-run] Comment that would be posted:\n${comment}`);
|
|
5777
|
+
} catch (error) {
|
|
5778
|
+
if (error instanceof EmptyCommentError) throw new FatalError(describeEmptyComment(flags));
|
|
5779
|
+
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.`);
|
|
5780
|
+
throw error;
|
|
5781
|
+
}
|
|
5782
|
+
}
|
|
5783
|
+
});
|
|
5784
|
+
//#endregion
|
|
5663
5785
|
//#region ../forgejo/src/closing-refs.ts
|
|
5664
5786
|
const CLOSING_REF = /(?:^|[\s([])(clos(?:e|es|ed)|fix(?:es|ed)?|resolv(?:e|es|ed))(?::[ \t]*|[ \t]+)((?:[\w.-]+\/[\w.-]+)?#\d+)(?![\w-])/gim;
|
|
5665
5787
|
function capitalize(word) {
|
|
@@ -6339,13 +6461,6 @@ async function readPrFilesPage(executor, conn, prNumber, page, limit) {
|
|
|
6339
6461
|
};
|
|
6340
6462
|
}
|
|
6341
6463
|
//#endregion
|
|
6342
|
-
//#region ../forgejo/src/pr-number.ts
|
|
6343
|
-
function parsePrNumber(raw) {
|
|
6344
|
-
const value = Number(raw);
|
|
6345
|
-
if (!/^\d+$/.test(raw) || !Number.isSafeInteger(value) || value < 1) throw new FatalError(`Invalid pull request number "${raw}" (expected a positive integer).`);
|
|
6346
|
-
return value;
|
|
6347
|
-
}
|
|
6348
|
-
//#endregion
|
|
6349
6464
|
//#region src/pr-files.ts
|
|
6350
6465
|
function runPrFiles(fetcher, options) {
|
|
6351
6466
|
const number = parsePrNumber(options.number);
|
|
@@ -6877,6 +6992,7 @@ function renderPrReady(result) {
|
|
|
6877
6992
|
//#region src/registry-pr.ts
|
|
6878
6993
|
const prSubCommands = {
|
|
6879
6994
|
"pr:checks": prChecksCommand,
|
|
6995
|
+
"pr:close": prCloseCommand,
|
|
6880
6996
|
"pr:create": prCreateCommand,
|
|
6881
6997
|
"pr:files": prFilesCommand,
|
|
6882
6998
|
"pr:list": prListCommand,
|