fix(gh): support owner/repo#number shorthand for issue write commands

gh issue comment create/delete, close, reopen, update, edit, and board-row
all now accept the owner/repo#number positional shorthand that gh issue
read/view and gh pr * already accept. This removes the friction of having
to split shorthand into a separate --repo flag and a bare number, and
keeps error messages consistent with the existing shorthand validation.

Discovered during HWLAB #621 CLI acceptance: posting the acceptance
results to the issue required gh issue comment create pikasTech/HWLAB#621,
which previously failed with 'issue comment create must be a positive
integer' and forced a separate --repo flag.
This commit is contained in:
Codex
2026-06-03 05:38:09 +00:00
parent faee528ed4
commit e20134ad90
+81 -13
View File
@@ -906,6 +906,32 @@ function resolvePositionalPrReference(args: string[], startIndex: number, label:
return { repo: options.repo, number };
}
function resolvePositionalIssueReference(args: string[], startIndex: number, label: string, options: GitHubOptions): GitHubResolvedNumberReference | GitHubCommandResult {
const targets = positionalArgs(args.slice(startIndex));
if (targets.length !== 1) {
return validationError(label, options.repo, `${label} requires exactly one positive integer or owner/repo#number positional argument`, {
supportedCommands: [
`bun scripts/cli.ts gh ${label} <number> --repo ${options.repo}`,
`bun scripts/cli.ts gh ${label} ${options.repo}#<number>`,
],
});
}
const shorthand = parseOwnerRepoNumberShorthand(targets[0]);
if (shorthand !== null) {
const explicitRepo = optionValue(args, "--repo");
if (explicitRepo !== undefined && explicitRepo !== shorthand.repo) {
return validationError(label, explicitRepo, `${label} target ${shorthand.input} resolves to repo ${shorthand.repo}, but --repo ${explicitRepo} was also provided.`, {
shorthand,
explicitRepo,
});
}
return { repo: shorthand.repo, number: shorthand.number, shorthand };
}
const number = parseNumberForCommand(options.repo, targets[0], label);
if (typeof number !== "number") return number;
return { repo: options.repo, number };
}
function parseOwnerRepoNumberShorthand(raw: string | undefined): GitHubShorthandReference | null {
if (raw === undefined) return null;
const match = /^([^/#\s]+)\/([^/#\s]+)#([1-9]\d*)$/u.exec(raw);
@@ -6533,7 +6559,9 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
if (top === "issue") {
if (sub === "delete") return unsupportedCommand("issue delete", options.repo, "GitHub REST does not support hard-deleting issues; use gh issue close for lifecycle deletion semantics.");
if (sub === "comment" && third === "delete") {
const commentId = parseNumberForCommand(options.repo, args[3], "issue comment delete");
const resolved = resolvePositionalIssueReference(args, 3, "issue comment delete", options);
if (isGitHubCommandResult(resolved)) return resolved;
const commentId = resolved.number;
if (typeof commentId !== "number") return commentId;
if (options.dryRun) return commentDelete(options.repo, "", "issue", commentId, true);
const { token, probe } = resolveToken(true);
@@ -6542,7 +6570,9 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
return commentDelete(options.repo, token, "issue", commentId, false);
}
if (sub === "comment" && third === "create") {
const issueNumber = parseNumberForCommand(options.repo, args[3], "issue comment create");
const resolved = resolvePositionalIssueReference(args, 3, "issue comment create", options);
if (isGitHubCommandResult(resolved)) return resolved;
const issueNumber = resolved.number;
if (typeof issueNumber !== "number") return issueNumber;
if (options.dryRun) return issueComment(options.repo, "", issueNumber, options);
const { token, probe } = resolveToken(true);
@@ -6573,7 +6603,9 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
const missing = authRequired(options.repo, commandName, probe);
if (missing !== null || token === null) return missing ?? authRequired(options.repo, commandName, { present: false, source: null, ghFallbackAttempted: true });
if (action === "list") return issueBoardRowList(options.repo, token, options);
const issueNumber = parseNumberForCommand(options.repo, args[3], commandName);
const resolvedBoardRow = resolvePositionalIssueReference(args, 3, commandName, options);
if (isGitHubCommandResult(resolvedBoardRow)) return resolvedBoardRow;
const issueNumber = resolvedBoardRow.number;
if (typeof issueNumber !== "number") return issueNumber;
if (action === "get") return issueBoardRowGet(options.repo, token, issueNumber, options);
if (action === "update") return issueBoardRowUpdate(options.repo, token, issueNumber, options);
@@ -6585,18 +6617,34 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
if (options.dryRun) {
if (sub === "create") return issueCreate(options.repo, "", options);
if (sub === "edit") {
const issueNumber = parseNumber(third, "issue edit");
const issueEditRef = resolvePositionalIssueReference(args, 2, "issue edit", options);
if (isGitHubCommandResult(issueEditRef)) return issueEditRef;
const issueNumber = issueEditRef.number;
const { token } = resolveToken(false);
return issueEdit(options.repo, token ?? "", issueNumber, options);
}
if (sub === "update") {
const issueNumber = parseNumber(third, "issue update");
const issueUpdateRef = resolvePositionalIssueReference(args, 2, "issue update", options);
if (isGitHubCommandResult(issueUpdateRef)) return issueUpdateRef;
const issueNumber = issueUpdateRef.number;
const { token } = resolveToken(false);
return issueEdit(options.repo, token ?? "", issueNumber, options, "issue update");
}
if (sub === "comment") return issueComment(options.repo, "", parseNumber(third, "issue comment"), options);
if (sub === "close") return issueState(options.repo, "", parseNumber(third, "issue close"), "closed", true, options);
if (sub === "reopen") return issueState(options.repo, "", parseNumber(third, "issue reopen"), "open", true, options);
if (sub === "comment") {
const r = resolvePositionalIssueReference(args, 2, "issue comment", options);
if (isGitHubCommandResult(r)) return r;
return issueComment(r.repo, "", r.number, { ...options, repo: r.repo });
}
if (sub === "close") {
const r = resolvePositionalIssueReference(args, 2, "issue close", options);
if (isGitHubCommandResult(r)) return r;
return issueState(r.repo, "", r.number, "closed", true, { ...options, repo: r.repo });
}
if (sub === "reopen") {
const r = resolvePositionalIssueReference(args, 2, "issue reopen", options);
if (isGitHubCommandResult(r)) return r;
return issueState(r.repo, "", r.number, "open", true, { ...options, repo: r.repo });
}
}
if (sub === "read" || sub === "view") {
const resolved = resolveReadViewNumberReference("issue", sub, third, options, args);
@@ -6615,11 +6663,31 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
if (sub === "list") return issueList(options.repo, token, options.listState, options.limit, options.issueListJsonFields, options.search, options.labels, options.raw || options.full);
if (sub === "stale-close") return issueStaleClose(options.repo, token, options);
if (sub === "create") return issueCreate(options.repo, token, options);
if (sub === "edit") return issueEdit(options.repo, token, parseNumber(third, "issue edit"), options);
if (sub === "update") return issueEdit(options.repo, token, parseNumber(third, "issue update"), options, "issue update");
if (sub === "comment") return issueComment(options.repo, token, parseNumber(third, "issue comment"), options);
if (sub === "close") return issueState(options.repo, token, parseNumber(third, "issue close"), "closed", options.dryRun, options);
if (sub === "reopen") return issueState(options.repo, token, parseNumber(third, "issue reopen"), "open", options.dryRun, options);
if (sub === "edit") {
const r = resolvePositionalIssueReference(args, 2, "issue edit", options);
if (isGitHubCommandResult(r)) return r;
return issueEdit(r.repo, token, r.number, { ...options, repo: r.repo });
}
if (sub === "update") {
const r = resolvePositionalIssueReference(args, 2, "issue update", options);
if (isGitHubCommandResult(r)) return r;
return issueEdit(r.repo, token, r.number, { ...options, repo: r.repo }, "issue update");
}
if (sub === "comment") {
const r = resolvePositionalIssueReference(args, 2, "issue comment", options);
if (isGitHubCommandResult(r)) return r;
return issueComment(r.repo, token, r.number, { ...options, repo: r.repo });
}
if (sub === "close") {
const r = resolvePositionalIssueReference(args, 2, "issue close", options);
if (isGitHubCommandResult(r)) return r;
return issueState(r.repo, token, r.number, "closed", options.dryRun, { ...options, repo: r.repo });
}
if (sub === "reopen") {
const r = resolvePositionalIssueReference(args, 2, "issue reopen", options);
if (isGitHubCommandResult(r)) return r;
return issueState(r.repo, token, r.number, "open", options.dryRun, { ...options, repo: r.repo });
}
}
if (top === "pr") {