fix: 修复 CLI repo 与状态别名解析
This commit is contained in:
+106
-10
@@ -28,6 +28,8 @@ const ISSUE_LIST_STATES = ["open", "closed", "all"] as const;
|
||||
const BODY_UPDATE_MODES = ["replace", "append"] as const;
|
||||
const BOARD_MUTATION_SECTIONS = ["open", "closed"] as const;
|
||||
const BOARD_GITHUB_STATUSES = ["OPEN", "CLOSED"] as const;
|
||||
const GH_VALUE_OPTIONS = new Set(["--repo", "--limit", "--board-issue", "--known-meta-issue", "--ignore-issue", "--title", "--body-file", "--body", "--base", "--head", "--json", "--state", "--mode", "--expect-updated-at", "--expect-body-sha", "--body-profile", "--label", "--field", "--value", "--section", "--to", "--status", "--row-file", "--category", "--branch", "--tasks", "--summary", "--focus", "--validation", "--progress", "--number"]);
|
||||
const GH_FLAG_OPTIONS = new Set(["--dry-run", "--draft", "--notify-claudeqq-brief-diff", "--allow-short-body", "--raw", "--full", "--stat"]);
|
||||
const MIN_SAFE_BODY_SCAN_CHARS = MIN_SAFE_ISSUE_BODY_CHARS;
|
||||
const ISSUE_SCAN_MAX_FINDINGS = 60;
|
||||
const ISSUE_BODY_PROFILES = {
|
||||
@@ -678,13 +680,11 @@ function parseBoardRowUpsertValues(args: string[]): BoardRowUpsertValues {
|
||||
}
|
||||
|
||||
function validateKnownOptions(args: string[]): void {
|
||||
const valueOptions = new Set(["--repo", "--limit", "--board-issue", "--known-meta-issue", "--ignore-issue", "--title", "--body-file", "--body", "--base", "--head", "--json", "--state", "--mode", "--expect-updated-at", "--expect-body-sha", "--body-profile", "--label", "--field", "--value", "--section", "--to", "--status", "--row-file", "--category", "--branch", "--tasks", "--summary", "--focus", "--validation", "--progress"]);
|
||||
const flagOptions = new Set(["--dry-run", "--draft", "--notify-claudeqq-brief-diff", "--allow-short-body", "--raw", "--full", "--stat"]);
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (!arg.startsWith("--")) continue;
|
||||
if (flagOptions.has(arg)) continue;
|
||||
if (valueOptions.has(arg)) {
|
||||
if (GH_FLAG_OPTIONS.has(arg)) continue;
|
||||
if (GH_VALUE_OPTIONS.has(arg)) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
@@ -692,13 +692,52 @@ function validateKnownOptions(args: string[]): void {
|
||||
}
|
||||
}
|
||||
|
||||
function positionalArgs(args: string[]): string[] {
|
||||
const positionals: string[] = [];
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index] ?? "";
|
||||
if (arg.startsWith("--")) {
|
||||
if (GH_VALUE_OPTIONS.has(arg)) index += 1;
|
||||
continue;
|
||||
}
|
||||
positionals.push(arg);
|
||||
}
|
||||
return positionals;
|
||||
}
|
||||
|
||||
function isOwnerRepo(value: string): boolean {
|
||||
return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(value);
|
||||
}
|
||||
|
||||
function resolveRepoOption(args: string[]): string {
|
||||
const [top, sub] = args;
|
||||
const explicitRepo = optionValue(args, "--repo");
|
||||
if ((top === "issue" || top === "pr") && sub === "list") {
|
||||
const positionals = positionalArgs(args.slice(2));
|
||||
if (positionals.length > 1) {
|
||||
throw new Error(`gh ${top} list accepts at most one positional owner/repo; use --repo owner/name`);
|
||||
}
|
||||
const positionalRepo = positionals[0];
|
||||
if (positionalRepo !== undefined) {
|
||||
if (!isOwnerRepo(positionalRepo)) {
|
||||
throw new Error(`gh ${top} list positional argument must be owner/repo; use --repo owner/name`);
|
||||
}
|
||||
if (explicitRepo !== undefined && explicitRepo !== positionalRepo) {
|
||||
throw new Error(`gh ${top} list received positional repo ${positionalRepo} and --repo ${explicitRepo}; use one repo target, for example --repo ${positionalRepo}`);
|
||||
}
|
||||
return positionalRepo;
|
||||
}
|
||||
}
|
||||
return explicitRepo ?? DEFAULT_REPO;
|
||||
}
|
||||
|
||||
function parseOptions(args: string[]): GitHubOptions {
|
||||
validateKnownOptions(args);
|
||||
const [top, sub] = args;
|
||||
const requestedJsonFields = commaListOption(args, "--json");
|
||||
const limitMax = top === "pr" && (sub === "files" || sub === "diff") ? MAX_PR_FILES_LIMIT : 100;
|
||||
return {
|
||||
repo: optionValue(args, "--repo") ?? DEFAULT_REPO,
|
||||
repo: resolveRepoOption(args),
|
||||
dryRun: hasFlag(args, "--dry-run"),
|
||||
raw: hasFlag(args, "--raw"),
|
||||
full: hasFlag(args, "--full"),
|
||||
@@ -774,10 +813,32 @@ function readViewSupportedJsonFields(kind: "issue" | "pr"): string {
|
||||
: "body,title,state,stateDetail,closed,closedAt,merged,mergedAt,mergeCommit,head,base,draft,headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup";
|
||||
}
|
||||
|
||||
function resolveReadViewNumberReference(kind: "issue" | "pr", sub: "read" | "view", raw: string | undefined, options: GitHubOptions, args: string[]): GitHubResolvedNumberReference | GitHubCommandResult {
|
||||
function resolveReadViewNumberReference(kind: "issue" | "pr", sub: "read" | "view", _raw: string | undefined, options: GitHubOptions, args: string[]): GitHubResolvedNumberReference | GitHubCommandResult {
|
||||
const command = `${kind} ${sub}`;
|
||||
const targets = positionalArgs(args.slice(2));
|
||||
if (targets.length > 1) {
|
||||
return validationError(command, options.repo, `${command} accepts one number or owner/repo#number target; use --repo owner/name and --number N when passing options first`, {
|
||||
supportedCommands: [
|
||||
`bun scripts/cli.ts gh ${kind} read <number> --repo owner/name --json ${readViewSupportedJsonFields(kind)}`,
|
||||
`bun scripts/cli.ts gh ${kind} read owner/name#<number> --raw`,
|
||||
],
|
||||
});
|
||||
}
|
||||
const raw = targets[0];
|
||||
const numberAliasRaw = kind === "pr" ? optionValue(args, "--number") : undefined;
|
||||
const shorthand = parseOwnerRepoNumberShorthand(raw);
|
||||
if (shorthand !== null) {
|
||||
if (numberAliasRaw !== undefined) {
|
||||
const aliasNumber = parseNumberForCommand(shorthand.repo, numberAliasRaw, command);
|
||||
if (typeof aliasNumber !== "number") return aliasNumber;
|
||||
if (aliasNumber !== shorthand.number) {
|
||||
return validationError(command, shorthand.repo, `${command} target ${shorthand.input} conflicts with --number ${aliasNumber}; use one PR number target.`, {
|
||||
shorthand,
|
||||
numberAlias: aliasNumber,
|
||||
supportedCommands: readViewSupportedCommands(kind, shorthand.repo, shorthand.number),
|
||||
});
|
||||
}
|
||||
}
|
||||
const explicitRepo = optionValue(args, "--repo");
|
||||
if (explicitRepo !== undefined && explicitRepo !== shorthand.repo) {
|
||||
const message = `${command} target ${shorthand.input} resolves to repo ${shorthand.repo}, but --repo ${explicitRepo} was also provided. Use either the shorthand or a matching --repo, not both.`;
|
||||
@@ -790,6 +851,31 @@ function resolveReadViewNumberReference(kind: "issue" | "pr", sub: "read" | "vie
|
||||
}
|
||||
return { repo: shorthand.repo, number: shorthand.number, shorthand };
|
||||
}
|
||||
if (numberAliasRaw !== undefined) {
|
||||
const aliasNumber = parseNumberForCommand(options.repo, numberAliasRaw, command);
|
||||
if (typeof aliasNumber !== "number") return {
|
||||
...aliasNumber,
|
||||
supportedCommands: [
|
||||
`bun scripts/cli.ts gh pr read --repo owner/name --number <number> --json ${readViewSupportedJsonFields("pr")}`,
|
||||
"bun scripts/cli.ts gh pr read owner/name#<number> --raw",
|
||||
],
|
||||
};
|
||||
if (raw !== undefined) {
|
||||
const parsedRaw = parseNumberForCommand(options.repo, raw, command);
|
||||
if (typeof parsedRaw !== "number") return parsedRaw;
|
||||
if (parsedRaw !== aliasNumber) {
|
||||
return validationError(command, options.repo, `${command} positional number ${parsedRaw} conflicts with --number ${aliasNumber}; use one PR number target.`, {
|
||||
positionalNumber: parsedRaw,
|
||||
numberAlias: aliasNumber,
|
||||
supportedCommands: [
|
||||
`bun scripts/cli.ts gh pr read ${aliasNumber} --repo ${options.repo} --json ${readViewSupportedJsonFields("pr")}`,
|
||||
`bun scripts/cli.ts gh pr read --repo ${options.repo} --number ${aliasNumber} --full`,
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
return { repo: options.repo, number: aliasNumber };
|
||||
}
|
||||
const parsed = parseNumberForCommand(options.repo, raw, command);
|
||||
if (typeof parsed !== "number") {
|
||||
return {
|
||||
@@ -5521,7 +5607,7 @@ export function ghHelp(): unknown {
|
||||
output: "json",
|
||||
usage: [
|
||||
"bun scripts/cli.ts gh auth status [--repo owner/name]",
|
||||
"bun scripts/cli.ts gh issue list [--state open|closed|all] [--limit N] [--repo owner/name] [--json number,title,state,url,updatedAt,createdAt,author,labels]",
|
||||
"bun scripts/cli.ts gh issue list [owner/repo] [--state open|closed|all] [--limit N] [--repo owner/name] [--json number,title,state,url,updatedAt,createdAt,author,labels]",
|
||||
"bun scripts/cli.ts gh issue read <number|owner/repo#number> [--repo owner/name] [--json body,title,state,comments] [--raw|--full]",
|
||||
"bun scripts/cli.ts gh issue view <number|owner/repo#number> [--repo owner/name] [--raw|--full] [compatibility alias for issue read]",
|
||||
"bun scripts/cli.ts gh issue create --title <title> --body-file <file> [--label label[,label...]]... [--repo owner/name] [--dry-run]",
|
||||
@@ -5543,10 +5629,10 @@ export function ghHelp(): unknown {
|
||||
"bun scripts/cli.ts gh issue board-row move <issueNumber> [--repo owner/name] --board-issue 20 --to open|closed [--status OPEN|CLOSED] [--dry-run] [--expect-body-sha sha256]",
|
||||
"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 [--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 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 read <number|owner/repo#number> [--repo owner/name] [--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 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]",
|
||||
@@ -5562,6 +5648,7 @@ export function ghHelp(): unknown {
|
||||
notes: [
|
||||
"Issue and PR create/read/update/comment/close/reopen use GitHub REST and do not require the gh binary when GH_TOKEN or GITHUB_TOKEN is present.",
|
||||
"Token values are never printed; auth status reports only token source and presence.",
|
||||
"issue list and pr list accept a single positional owner/repo as a compatibility alias for --repo owner/name. The positional repo and --repo must match if both are supplied; non-repo positionals fail structurally instead of falling back to the default repo.",
|
||||
"issue list defaults to --state open and bounded --limit 30; supported --json fields are number,title,state,url,updatedAt,createdAt,author,labels and unknown fields fail structurally.",
|
||||
"PR list defaults to --state all for compatibility with earlier UniDesk CLI behavior; supported states are open, closed, and all.",
|
||||
"issue read is the canonical read path; view remains a compatibility alias. Read/view accept owner/repo#number shorthand and derive --repo unless an explicit conflicting --repo is supplied, which fails structurally with suggested commands. Read supports legacy --json field selection such as --json body and still exposes .data.issue.body for compatibility; unsupported fields fail structurally.",
|
||||
@@ -5586,7 +5673,7 @@ export function ghHelp(): unknown {
|
||||
"comment delete is supported because GitHub supports deleting issue comments; issue/pr hard delete is unsupported and close is the lifecycle alternative.",
|
||||
"PR files is the canonical compact changed-file/stat summary. It uses GitHub REST, returns bounded file rows, additions/deletions/changes when available, truncation metadata, and a next command for full details. Raw diff patches are not emitted by default; gh pr diff <number> --stat is a compatibility alias for the same JSON summary.",
|
||||
"PR edit/update PATCHes /repos/{owner}/{repo}/pulls/{number} through REST only, never GitHub Projects Classic GraphQL/projectCards, and returns low-noise JSON with repo, PR number, changedFields, url, and body size/SHA metadata instead of echoing the full body.",
|
||||
"PR read is the canonical read path; view remains a compatibility alias. PR read/view accept owner/repo#number shorthand and derive --repo unless an explicit conflicting --repo is supplied, which fails structurally with suggested commands. PR read/view supports REST closeout fields stateDetail, closed, closedAt, merged, mergedAt, mergeCommit, headRefName, and baseRefName; mergeable, mergeStateStatus, and statusCheckRollup are fetched through GitHub GraphQL only when requested or when --raw/--full requests full disclosure, and closeoutMetadata makes GraphQL errors plus UNKNOWN/null metadata explicit.",
|
||||
"PR read is the canonical read path; view remains a compatibility alias. PR read/view accept owner/repo#number shorthand and --number N as a compatibility alias for the positional PR number; shorthand derives --repo unless an explicit conflicting --repo is supplied, which fails structurally with suggested commands. PR read/view supports REST closeout fields stateDetail, closed, closedAt, merged, mergedAt, mergeCommit, headRefName, and baseRefName; mergeable, mergeStateStatus, and statusCheckRollup are fetched through GitHub GraphQL only when requested or when --raw/--full requests full disclosure, and closeoutMetadata makes GraphQL errors plus UNKNOWN/null metadata explicit.",
|
||||
"PR list does not fetch mergeability or statusCheckRollup; request those closeout fields with gh pr view <number> --json headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup.",
|
||||
"PR preflight is a low-noise read-only closeout helper. It combines redacted auth capability, PR branch/state metadata, mergeability, mergeStateStatus, compact status check counts, and the explicit UniDesk REST CLI no-merge policy. Use --full or --raw to include all fetched status contexts.",
|
||||
"PR create/edit/update/comment are safe-write operations with dry-run planning; merge is intentionally unsupported in this phase.",
|
||||
@@ -5644,6 +5731,15 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
],
|
||||
});
|
||||
}
|
||||
if (optionWasProvided(args, "--number") && !(top === "pr" && isPrReadCommand(sub))) {
|
||||
const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh";
|
||||
return validationError(command, options.repo, "--number is only supported by gh pr read/view; use gh pr read --repo owner/name --number N.", {
|
||||
supportedCommands: [
|
||||
"bun scripts/cli.ts gh pr read --repo owner/name --number <number> --full",
|
||||
"bun scripts/cli.ts gh pr read <number> --repo owner/name --json body,title,state,head,base",
|
||||
],
|
||||
});
|
||||
}
|
||||
if (optionWasProvided(args, "--mode") && !((top === "issue" && (sub === "update" || sub === "edit")) || (top === "pr" && (sub === "update" || sub === "edit")))) {
|
||||
const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh";
|
||||
return validationError(command, options.repo, "--mode is only supported by gh issue update/edit and gh pr update/edit");
|
||||
|
||||
Reference in New Issue
Block a user