fix: unify gh read and CRUD semantics
This commit is contained in:
+37
-20
@@ -444,7 +444,7 @@ function validateJsonFields<T extends string>(command: string, requested: string
|
||||
}
|
||||
|
||||
function parseIssueViewJsonFields(requested: string[] | undefined): IssueViewJsonField[] | undefined {
|
||||
return validateJsonFields("gh issue view", requested, ISSUE_VIEW_JSON_FIELDS);
|
||||
return validateJsonFields("gh issue read/view", requested, ISSUE_VIEW_JSON_FIELDS);
|
||||
}
|
||||
|
||||
function parseIssueListJsonFields(requested: string[] | undefined): IssueListJsonField[] | undefined {
|
||||
@@ -455,6 +455,14 @@ function parsePrJsonFields(command: string, requested: string[] | undefined): Pr
|
||||
return validateJsonFields(command, requested, PR_JSON_FIELDS);
|
||||
}
|
||||
|
||||
function isIssueReadCommand(sub: string | undefined): boolean {
|
||||
return sub === "read" || sub === "view";
|
||||
}
|
||||
|
||||
function isPrReadCommand(sub: string | undefined): boolean {
|
||||
return sub === "read" || sub === "view";
|
||||
}
|
||||
|
||||
function parseIssueListState(args: string[]): IssueListState {
|
||||
const raw = optionValue(args, "--state") ?? "open";
|
||||
if ((ISSUE_LIST_STATES as readonly string[]).includes(raw)) return raw as IssueListState;
|
||||
@@ -508,9 +516,9 @@ function parseOptions(args: string[]): GitHubOptions {
|
||||
bodyFile: optionValue(args, "--body-file"),
|
||||
base: optionValue(args, "--base"),
|
||||
head: optionValue(args, "--head"),
|
||||
jsonFields: top === "issue" && sub === "view" ? parseIssueViewJsonFields(requestedJsonFields) : undefined,
|
||||
jsonFields: top === "issue" && isIssueReadCommand(sub) ? parseIssueViewJsonFields(requestedJsonFields) : undefined,
|
||||
issueListJsonFields: top === "issue" && sub === "list" ? parseIssueListJsonFields(requestedJsonFields) : undefined,
|
||||
prJsonFields: top === "pr" && (sub === "view" || sub === "list") ? parsePrJsonFields(`gh pr ${sub}`, requestedJsonFields) : undefined,
|
||||
prJsonFields: top === "pr" && (isPrReadCommand(sub) || sub === "list") ? parsePrJsonFields(`gh pr ${sub}`, requestedJsonFields) : undefined,
|
||||
listState: parseIssueListState(args),
|
||||
mode: parseBodyUpdateMode(args),
|
||||
expectUpdatedAt: optionValue(args, "--expect-updated-at"),
|
||||
@@ -1975,15 +1983,15 @@ function selectedIssueJson(issue: GitHubIssue, comments: GitHubComment[] | null,
|
||||
return selected;
|
||||
}
|
||||
|
||||
async function issueView(repo: string, token: string, issueNumber: number, jsonFields: IssueViewJsonField[] | undefined): Promise<GitHubCommandResult> {
|
||||
async function issueRead(repo: string, token: string, issueNumber: number, jsonFields: IssueViewJsonField[] | undefined, commandName = "issue read"): Promise<GitHubCommandResult> {
|
||||
const issue = await getIssue(token, repo, issueNumber);
|
||||
if (isGitHubError(issue)) return commandError("issue view", repo, issue, { issueNumber });
|
||||
if (isGitHubError(issue)) return commandError(commandName, repo, issue, { issueNumber });
|
||||
const needsComments = jsonFields === undefined || jsonFields.includes("comments");
|
||||
const comments = needsComments ? await listIssueComments(token, repo, issueNumber) : null;
|
||||
if (isGitHubError(comments)) return commandError("issue view", repo, comments, { issueNumber, issue: issueSummary(issue) });
|
||||
if (isGitHubError(comments)) return commandError(commandName, repo, comments, { issueNumber, issue: issueSummary(issue) });
|
||||
return {
|
||||
ok: true,
|
||||
command: "issue view",
|
||||
command: commandName,
|
||||
repo,
|
||||
issue: issueSummary(issue),
|
||||
...(comments === null ? {} : { comments: comments.map(commentSummary) }),
|
||||
@@ -1997,6 +2005,10 @@ async function issueView(repo: string, token: string, issueNumber: number, jsonF
|
||||
};
|
||||
}
|
||||
|
||||
async function issueView(repo: string, token: string, issueNumber: number, jsonFields: IssueViewJsonField[] | undefined): Promise<GitHubCommandResult> {
|
||||
return issueRead(repo, token, issueNumber, jsonFields, "issue view");
|
||||
}
|
||||
|
||||
async function issueList(repo: string, token: string, state: IssueListState, limit: number, jsonFields: IssueListJsonField[] | undefined): Promise<GitHubCommandResult> {
|
||||
const rawIssues = await listIssues(token, repo, state, limit);
|
||||
if (isGitHubError(rawIssues)) return commandError("issue list", repo, rawIssues, { state, limit });
|
||||
@@ -2872,19 +2884,23 @@ async function prList(repo: string, token: string, limit: number, jsonFields: Pr
|
||||
};
|
||||
}
|
||||
|
||||
async function prView(repo: string, token: string, number: number, jsonFields: PrJsonField[] | undefined): Promise<GitHubCommandResult> {
|
||||
async function prRead(repo: string, token: string, number: number, jsonFields: PrJsonField[] | undefined, commandName = "pr read"): Promise<GitHubCommandResult> {
|
||||
const { owner, name } = repoParts(repo);
|
||||
const pr = await githubRequest<GitHubPullRequest>(token, "GET", `/repos/${owner}/${name}/pulls/${number}`);
|
||||
if (isGitHubError(pr)) return commandError("pr view", repo, pr, { number });
|
||||
if (isGitHubError(pr)) return commandError(commandName, repo, pr, { number });
|
||||
return {
|
||||
ok: true,
|
||||
command: "pr view",
|
||||
command: commandName,
|
||||
repo,
|
||||
pullRequest: prSummary(pr),
|
||||
...(jsonFields === undefined ? {} : { jsonFields, json: selectedPrJson(pr, jsonFields) }),
|
||||
};
|
||||
}
|
||||
|
||||
async function prView(repo: string, token: string, number: number, jsonFields: PrJsonField[] | undefined): Promise<GitHubCommandResult> {
|
||||
return prRead(repo, token, number, jsonFields, "pr view");
|
||||
}
|
||||
|
||||
export function ghHelp(): unknown {
|
||||
return {
|
||||
command: "gh",
|
||||
@@ -2892,7 +2908,7 @@ export function ghHelp(): unknown {
|
||||
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 view <number> [--repo owner/name] [--json body,title,state,comments]",
|
||||
"bun scripts/cli.ts gh issue read|view <number> [--repo owner/name] [--json body,title,state,comments]",
|
||||
"bun scripts/cli.ts gh issue create --title <title> --body-file <file> [--label label[,label...]]... [--repo owner/name] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue update <number> --mode replace|append --body-file <file> [--title title] [--repo owner/name] [--dry-run] [--expect-updated-at ts|--expect-body-sha sha256] [--body-profile auto|code-queue-board|commander-brief] [--allow-short-body]",
|
||||
"bun scripts/cli.ts gh issue edit <number> --body-file <file> [compat alias for issue update --mode replace]",
|
||||
@@ -2905,7 +2921,7 @@ export function ghHelp(): unknown {
|
||||
"bun scripts/cli.ts gh issue cleanup-plan [--repo owner/name] [--limit N] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue board-audit [--repo owner/name] [--board-issue 20] [--limit N] [--known-meta-issue N[,N...]] [--ignore-issue N[,N...]] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr list [--repo owner/name] [--limit N] [--json number,title,state,url,updatedAt,createdAt,author,head,base,draft]",
|
||||
"bun scripts/cli.ts gh pr view <number> [--repo owner/name] [--json body,title,state,head,base,draft]",
|
||||
"bun scripts/cli.ts gh pr read|view <number> [--repo owner/name] [--json body,title,state,head,base,draft]",
|
||||
"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 update <number> --mode replace|append --body-file <file>|--body <text> [--title title] [--repo owner/name] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr comment create <number> --body-file <file>|--body <text> [--repo owner/name] [--dry-run]",
|
||||
@@ -2918,7 +2934,7 @@ export function ghHelp(): unknown {
|
||||
"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 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.",
|
||||
"issue view supports legacy --json field selection such as --json body and still exposes .data.issue.body for compatibility; unsupported fields fail structurally.",
|
||||
"issue read is the canonical read path; view remains a compatibility alias. Read supports legacy --json field selection such as --json body and still exposes .data.issue.body for compatibility; unsupported fields fail structurally.",
|
||||
"issue create accepts repeatable --label values and comma-separated labels; dry-run prints the parsed labels and non-dry-run sends them in the GitHub REST create-issue payload.",
|
||||
"--body-file is the recommended source for Markdown bodies so real newlines, backticks, and tables are read as file bytes instead of shell arguments.",
|
||||
"update defaults to --mode replace; --mode append reads the current body and appends file bytes so real newlines, backticks, and Markdown tables are preserved.",
|
||||
@@ -2933,7 +2949,7 @@ export function ghHelp(): unknown {
|
||||
"issue edit 24 --notify-claudeqq-brief-diff reads the old issue body, PATCHes the new body, and sends only newly added '## 更新 ... 北京时间' sections to ClaudeQQ; ClaudeQQ failure does not roll back GitHub.",
|
||||
"Commander brief ClaudeQQ defaults to private target 645275593 through backend-core /api/microservices/claudeqq/proxy; UNIDESK_COMMANDER_BRIEF_CLAUDEQQ_* env vars can override target, base URL, timeout, and enabled state.",
|
||||
"comment delete is supported because GitHub supports deleting issue comments; issue/pr hard delete is unsupported and close is the lifecycle alternative.",
|
||||
"PR create/update/comment are safe-write operations with dry-run planning; merge is intentionally unsupported in this phase.",
|
||||
"PR read is the canonical read path; view remains a compatibility alias. PR create/update/comment are safe-write operations with dry-run planning; merge is intentionally unsupported in this phase.",
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -2960,10 +2976,10 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh";
|
||||
return validationError(command, options.repo, "--state is only supported by gh issue list");
|
||||
}
|
||||
if (optionWasProvided(args, "--json") && !(top === "issue" && (sub === "view" || sub === "list"))) {
|
||||
if (optionWasProvided(args, "--json") && !(top === "issue" && (isIssueReadCommand(sub) || sub === "list"))) {
|
||||
const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh";
|
||||
if (!(top === "pr" && (sub === "view" || sub === "list"))) {
|
||||
return validationError(command, options.repo, "--json field selection is only supported by gh issue view/list and gh pr view/list");
|
||||
if (!(top === "pr" && (isPrReadCommand(sub) || sub === "list"))) {
|
||||
return validationError(command, options.repo, "--json field selection is only supported by gh issue read/view/list and gh pr read/view/list");
|
||||
}
|
||||
}
|
||||
if (optionWasProvided(args, "--mode") && !((top === "issue" && (sub === "update" || sub === "edit")) || (top === "pr" && sub === "update"))) {
|
||||
@@ -3039,6 +3055,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
if (missing !== null || token === null) return missing ?? authRequired(options.repo, `issue ${sub ?? ""}`.trim(), { present: false, source: null, ghFallbackAttempted: true });
|
||||
|
||||
if (sub === "list") return issueList(options.repo, token, options.listState, options.limit, options.issueListJsonFields);
|
||||
if (sub === "read") return issueRead(options.repo, token, parseNumber(third, "issue read"), options.jsonFields);
|
||||
if (sub === "view") return issueView(options.repo, token, parseNumber(third, "issue view"), options.jsonFields);
|
||||
if (sub === "create") return issueCreate(options.repo, token, options);
|
||||
if (sub === "edit") return issueEdit(options.repo, token, parseNumber(third, "issue edit"), options);
|
||||
@@ -3046,7 +3063,6 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
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);
|
||||
if (sub === "reopen") return issueState(options.repo, token, parseNumber(third, "issue reopen"), "open", options.dryRun);
|
||||
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 (top === "pr") {
|
||||
@@ -3110,13 +3126,14 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
if (sub === "merge") {
|
||||
return unsupportedCommand("pr merge", options.repo, "PR merge is intentionally unsupported in this phase; use create/comment/read only.");
|
||||
}
|
||||
if (sub !== "list" && sub !== "view") {
|
||||
return unsupportedCommand(`pr ${sub ?? ""}`.trim(), options.repo, "PR supported commands are list, view, create, update, close, reopen, comment create/delete, and unsupported merge/delete.");
|
||||
if (sub !== "list" && !isPrReadCommand(sub)) {
|
||||
return unsupportedCommand(`pr ${sub ?? ""}`.trim(), options.repo, "PR supported commands are list, read/view, create, update, close, reopen, comment create/delete, and unsupported merge/delete.");
|
||||
}
|
||||
const { token, probe } = resolveToken(true);
|
||||
const missing = authRequired(options.repo, `pr ${sub}`, probe);
|
||||
if (missing !== null || token === null) return missing ?? authRequired(options.repo, `pr ${sub}`, { present: false, source: null, ghFallbackAttempted: true });
|
||||
if (sub === "list") return prList(options.repo, token, options.limit, options.prJsonFields);
|
||||
if (sub === "read") return prRead(options.repo, token, parseNumber(third, "pr read"), options.prJsonFields);
|
||||
return prView(options.repo, token, parseNumber(third, "pr view"), options.prJsonFields);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user