fix: allow gh pr numbers after options
This commit is contained in:
@@ -517,6 +517,12 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
assertCondition(aliasPreflightData.command === "preflight", "top-level gh preflight should report alias command", aliasPreflightData);
|
||||
assertCondition((aliasPreflightData.policy as JsonRecord).mergesPr === false, "top-level gh preflight alias must not merge", aliasPreflightData);
|
||||
|
||||
const optionsFirstPreflight = await runCli(["gh", "pr", "preflight", "--repo", "pikasTech/unidesk", "42"], env);
|
||||
assertCondition(optionsFirstPreflight.status === 0, "pr preflight should accept PR number after --repo", optionsFirstPreflight.json ?? { stdout: optionsFirstPreflight.stdout });
|
||||
const optionsFirstPreflightData = dataOf(optionsFirstPreflight.json ?? {});
|
||||
assertCondition(optionsFirstPreflightData.command === "pr preflight", "options-first pr preflight should report command", optionsFirstPreflightData);
|
||||
assertCondition((optionsFirstPreflightData.pullRequest as JsonRecord).number === 42, "options-first pr preflight should read the requested PR", optionsFirstPreflightData);
|
||||
|
||||
const fullPreflight = await runCli(["gh", "pr", "preflight", "42", "--repo", "pikasTech/unidesk", "--full"], env);
|
||||
assertCondition(fullPreflight.status === 0, "pr preflight --full should succeed", fullPreflight.json ?? { stdout: fullPreflight.stdout });
|
||||
const fullPreflightData = dataOf(fullPreflight.json ?? {});
|
||||
@@ -736,6 +742,7 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
"pr read unsupported fields fail structurally with supported closeout fields listed",
|
||||
"pr preflight exposes redacted auth plus compact merge/status closeout metadata",
|
||||
"top-level gh preflight alias works for commander closeout",
|
||||
"pr preflight accepts the PR number after options",
|
||||
"pr preflight --full is the explicit status-context disclosure path",
|
||||
"pr create dry-run exposes planned operation",
|
||||
"pr comment dry-run preserves markdown text",
|
||||
|
||||
@@ -196,6 +196,14 @@ export async function runGhCliPrFilesContract(): Promise<JsonRecord> {
|
||||
assertCondition(mock.requests.some((request) => request.method === "GET" && request.url === "/repos/pikasTech/unidesk/pulls/42/files?per_page=1&page=1"), "files endpoint should use bounded per_page", mock.requests);
|
||||
checks.push("gh pr files returns bounded REST file/stat JSON without raw patches");
|
||||
|
||||
mock.requests.length = 0;
|
||||
const filesOptionsFirst = await runCli(["gh", "pr", "files", "--repo", "pikasTech/unidesk", "--limit", "1", "42"], env);
|
||||
assertCondition(filesOptionsFirst.status === 0, "gh pr files should accept PR number after options", filesOptionsFirst.json ?? { stdout: filesOptionsFirst.stdout, stderr: filesOptionsFirst.stderr });
|
||||
const filesOptionsFirstData = dataOf(filesOptionsFirst.json ?? {});
|
||||
assertCondition(filesOptionsFirstData.command === "pr files" && filesOptionsFirstData.filesReturned === 1, "options-first pr files should return compact file summary", filesOptionsFirstData);
|
||||
assertCondition(mock.requests.some((request) => request.method === "GET" && request.url === "/repos/pikasTech/unidesk/pulls/42/files?per_page=1&page=1"), "options-first pr files should query the requested PR number", mock.requests);
|
||||
checks.push("gh pr files accepts the PR number after options");
|
||||
|
||||
mock.requests.length = 0;
|
||||
const stat = await runCli(["gh", "pr", "diff", "42", "--stat", "--repo", "pikasTech/unidesk", "--limit", "2"], env);
|
||||
assertCondition(stat.status === 0, "gh pr diff --stat should succeed", stat.json ?? { stdout: stat.stdout, stderr: stat.stderr });
|
||||
@@ -207,6 +215,14 @@ export async function runGhCliPrFilesContract(): Promise<JsonRecord> {
|
||||
assertCondition(statRows.every((file) => file.patch === undefined), "diff --stat file rows must not include patch", statRows);
|
||||
checks.push("gh pr diff --stat is a compact summary alias");
|
||||
|
||||
mock.requests.length = 0;
|
||||
const statOptionsFirst = await runCli(["gh", "pr", "diff", "--repo", "pikasTech/unidesk", "--stat", "--limit", "2", "42"], env);
|
||||
assertCondition(statOptionsFirst.status === 0, "gh pr diff --stat should accept PR number after options", statOptionsFirst.json ?? { stdout: statOptionsFirst.stdout, stderr: statOptionsFirst.stderr });
|
||||
const statOptionsFirstData = dataOf(statOptionsFirst.json ?? {});
|
||||
assertCondition(statOptionsFirstData.command === "pr diff --stat" && statOptionsFirstData.filesReturned === 2, "options-first pr diff --stat should return compact file summary", statOptionsFirstData);
|
||||
assertCondition(mock.requests.some((request) => request.method === "GET" && request.url === "/repos/pikasTech/unidesk/pulls/42/files?per_page=2&page=1"), "options-first pr diff --stat should query the requested PR number", mock.requests);
|
||||
checks.push("gh pr diff --stat accepts the PR number after options");
|
||||
|
||||
const rawDiff = await runCli(["gh", "pr", "diff", "42", "--repo", "pikasTech/unidesk"], env);
|
||||
assertCondition(rawDiff.status !== 0, "gh pr diff without --stat should fail closed", rawDiff.json ?? { stdout: rawDiff.stdout, stderr: rawDiff.stderr });
|
||||
const rawData = failedDataOf(rawDiff.json ?? {});
|
||||
|
||||
+21
-8
@@ -801,6 +801,19 @@ function parseNumberForCommand(repo: string, raw: string | undefined, label: str
|
||||
}
|
||||
}
|
||||
|
||||
function parsePositionalNumberForCommand(repo: string, args: string[], startIndex: number, label: string): number | GitHubCommandResult {
|
||||
const targets = positionalArgs(args.slice(startIndex));
|
||||
if (targets.length !== 1) {
|
||||
return validationError(label, repo, `${label} requires exactly one positive integer positional argument`, {
|
||||
supportedCommands: [
|
||||
`bun scripts/cli.ts gh ${label} <number> --repo ${repo}`,
|
||||
`bun scripts/cli.ts gh ${label} --repo ${repo} <number>`,
|
||||
],
|
||||
});
|
||||
}
|
||||
return parseNumberForCommand(repo, targets[0], label);
|
||||
}
|
||||
|
||||
function parseOwnerRepoNumberShorthand(raw: string | undefined): GitHubShorthandReference | null {
|
||||
if (raw === undefined) return null;
|
||||
const match = /^([^/#\s]+)\/([^/#\s]+)#([1-9]\d*)$/u.exec(raw);
|
||||
@@ -5900,12 +5913,12 @@ export function ghHelp(): unknown {
|
||||
"bun scripts/cli.ts gh issue board-row delete <issueNumber> [--repo owner/name] --board-issue 20 [--dry-run] [--expect-body-sha sha256]",
|
||||
"bun scripts/cli.ts gh preflight <prNumber> [--repo owner/name] [--full|--raw] [compatibility alias for gh pr preflight]",
|
||||
"bun scripts/cli.ts gh pr list [owner/repo] [--repo owner/name] [--state open|closed|all] [--limit N] [--json number,title,state,url,updatedAt,createdAt,author,head,base,draft]",
|
||||
"bun scripts/cli.ts gh pr files <number> [--repo owner/name] [--limit N]",
|
||||
"bun scripts/cli.ts gh pr diff <number> --stat [--repo owner/name] [--limit N] [compatibility alias for pr files; no raw diff]",
|
||||
"bun scripts/cli.ts gh pr files <number> [--repo owner/name] [--limit N] [number may appear before or after options]",
|
||||
"bun scripts/cli.ts gh pr diff <number> --stat [--repo owner/name] [--limit N] [number may appear before or after options; compatibility alias for pr files; no raw diff]",
|
||||
"bun scripts/cli.ts gh pr read <number|owner/repo#number> [--repo owner/name] [--number N] [--json body,title,state,stateDetail,closed,closedAt,merged,mergedAt,mergeCommit,head,base,draft,headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup] [--raw|--full]",
|
||||
"bun scripts/cli.ts gh pr view <number|owner/repo#number> [--repo owner/name] [--raw|--full] [compatibility alias for pr read]",
|
||||
"bun scripts/cli.ts gh pr preflight <number> [--repo owner/name] [--full|--raw]",
|
||||
"bun scripts/cli.ts gh pr closeout <number> [--repo owner/name] [--full|--raw] [compatibility alias for pr preflight]",
|
||||
"bun scripts/cli.ts gh pr preflight <number> [--repo owner/name] [--full|--raw] [number may appear before or after options]",
|
||||
"bun scripts/cli.ts gh pr closeout <number> [--repo owner/name] [--full|--raw] [number may appear before or after options; compatibility alias for pr preflight]",
|
||||
"bun scripts/cli.ts gh pr create --title <title> --body-file <file>|--body <text> --base <branch> --head <branch> [--repo owner/name] [--draft] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr edit <number> [--title title] [--body-file <file>|--body-file -|--body <text>] [--repo owner/name] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr update <number> --mode replace|append [--body-file <file>|--body-file -|--body <text>] [--title title] [--repo owner/name] [--dry-run]",
|
||||
@@ -6060,7 +6073,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
if (top === "auth" && sub === "status") return authStatus(options.repo);
|
||||
|
||||
if (top === "preflight") {
|
||||
const number = parseNumberForCommand(options.repo, sub, "preflight");
|
||||
const number = parsePositionalNumberForCommand(options.repo, args, 1, "preflight");
|
||||
if (typeof number !== "number") return number;
|
||||
return prPreflight(options.repo, number, "preflight", options.full || options.raw);
|
||||
}
|
||||
@@ -6158,7 +6171,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
|
||||
if (top === "pr") {
|
||||
if (sub === "diff") {
|
||||
const number = parseNumberForCommand(options.repo, third, "pr diff");
|
||||
const number = parsePositionalNumberForCommand(options.repo, args, 2, "pr diff");
|
||||
if (typeof number !== "number") return number;
|
||||
if (!optionWasProvided(args, "--stat")) {
|
||||
return unsupportedCommand("pr diff", options.repo, "Raw PR diff output is intentionally unsupported by UniDesk CLI; use gh pr diff <number> --stat or gh pr files for a bounded REST file/stat summary.", {
|
||||
@@ -6175,7 +6188,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
return prFiles(options.repo, token, number, options.limit, "pr diff --stat");
|
||||
}
|
||||
if (sub === "files") {
|
||||
const number = parseNumberForCommand(options.repo, third, "pr files");
|
||||
const number = parsePositionalNumberForCommand(options.repo, args, 2, "pr files");
|
||||
if (typeof number !== "number") return number;
|
||||
const { token, probe } = resolveToken(true);
|
||||
const missing = authRequired(options.repo, "pr files", probe);
|
||||
@@ -6184,7 +6197,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
}
|
||||
if (sub === "delete") return unsupportedCommand("pr delete", options.repo, "GitHub REST does not support hard-deleting pull requests; use gh pr close for lifecycle deletion semantics.");
|
||||
if (sub === "preflight" || sub === "closeout") {
|
||||
const number = parseNumberForCommand(options.repo, third, `pr ${sub}`);
|
||||
const number = parsePositionalNumberForCommand(options.repo, args, 2, `pr ${sub}`);
|
||||
if (typeof number !== "number") return number;
|
||||
return prPreflight(options.repo, number, sub === "closeout" ? "pr closeout" : "pr preflight", options.full || options.raw);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user