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:
+81
-13
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user