feat: 支持 gh issue stdin 写入
This commit is contained in:
+35
-25
@@ -943,12 +943,6 @@ function unknownGhOptionDetails(args: string[], option: string): Record<string,
|
||||
return details;
|
||||
}
|
||||
|
||||
function readBodyFile(path: string | undefined, command: string): string {
|
||||
if (path === undefined) throw new Error(`${command} requires --body-file <file>`);
|
||||
if (!existsSync(path)) throw new Error(`body file not found: ${path}`);
|
||||
return readFileSync(path, "utf8");
|
||||
}
|
||||
|
||||
function readMarkdownBodyFileOrStdin(path: string): { body: string; bodySource: Record<string, unknown> } {
|
||||
if (path === "-") {
|
||||
return { body: readFileSync(0, "utf8"), bodySource: { kind: "stdin", path: "-" } };
|
||||
@@ -994,7 +988,6 @@ function readIssueCommentBody(options: GitHubOptions): { body: string; bodySourc
|
||||
throw new Error("issue comment create accepts only one body source: --body-file or --body");
|
||||
}
|
||||
if (options.bodyFile !== undefined) {
|
||||
if (options.bodyFile === "-") throw new Error("issue comment create does not support --body-file - stdin; write Markdown to a file and pass --body-file <file>");
|
||||
return readMarkdownBodyFileOrStdin(options.bodyFile);
|
||||
}
|
||||
if (options.body === undefined) throw new Error("issue comment create requires --body-file <file> or --body <text>");
|
||||
@@ -5026,8 +5019,16 @@ async function issueBoardAudit(repo: string, token: string, options: GitHubOptio
|
||||
|
||||
async function issueCreate(repo: string, token: string, options: GitHubOptions): Promise<GitHubCommandResult> {
|
||||
if (options.title === undefined) throw new Error("issue create requires --title <title>");
|
||||
const body = readBodyFile(options.bodyFile, "issue create");
|
||||
const bodySource = { kind: "body-file", path: options.bodyFile ?? null };
|
||||
if (options.body !== undefined) {
|
||||
return validationError("issue create", repo, "issue create does not support --body; use --body-file <file|-> for Markdown", {
|
||||
title: options.title,
|
||||
bodySource: "inline",
|
||||
});
|
||||
}
|
||||
const { body, bodySource } = readMarkdownBody({
|
||||
...options,
|
||||
body: undefined,
|
||||
}, "issue create");
|
||||
const labels = options.labels;
|
||||
if (options.dryRun) {
|
||||
return {
|
||||
@@ -5060,7 +5061,16 @@ async function issueCreate(repo: string, token: string, options: GitHubOptions):
|
||||
}
|
||||
|
||||
async function issueEdit(repo: string, token: string, issueNumber: number, options: GitHubOptions, commandName = "issue edit"): Promise<GitHubCommandResult> {
|
||||
const body = readBodyFile(options.bodyFile, commandName);
|
||||
if (options.bodyFile === undefined) {
|
||||
return validationError(commandName, repo, `${commandName} requires --body-file <file|->`, { issueNumber });
|
||||
}
|
||||
let bodyInput: { body: string; bodySource: Record<string, unknown> };
|
||||
try {
|
||||
bodyInput = readMarkdownBodyFileOrStdin(options.bodyFile);
|
||||
} catch (error) {
|
||||
return validationError(commandName, repo, error instanceof Error ? error.message : String(error), { issueNumber });
|
||||
}
|
||||
const { body, bodySource } = bodyInput;
|
||||
const needsProfileMetadata = issueProfileNeedsMetadata(issueNumber, options.bodyProfile);
|
||||
if (options.mode === "replace" && !needsProfileMetadata) {
|
||||
const bodyGuard = validateIssueBodyGuard(repo, issueNumber, body, options);
|
||||
@@ -5077,7 +5087,7 @@ async function issueEdit(repo: string, token: string, issueNumber: number, optio
|
||||
}
|
||||
const needsReadBeforeEdit = options.mode === "append"
|
||||
|| needsProfileMetadata && token.length > 0
|
||||
|| !options.dryRun && (options.notifyClaudeQqBriefDiff || options.expectUpdatedAt !== undefined || options.expectBodySha !== undefined);
|
||||
|| !options.dryRun;
|
||||
if (needsReadBeforeEdit) {
|
||||
const issue = await getIssue(token, repo, issueNumber);
|
||||
if (isGitHubError(issue)) return commandError(commandName, repo, issue, { issueNumber, phase: "read-before-update" });
|
||||
@@ -5146,7 +5156,7 @@ async function issueEdit(repo: string, token: string, issueNumber: number, optio
|
||||
disclosure: issueWriteDisclosure(options, repo, issueNumber, true),
|
||||
readCommands: issueBodyReadCommands(repo, issueNumber),
|
||||
guard,
|
||||
update: bodyUpdatePlan(commandName, repo, issueNumber, options.mode, body, { kind: "body-file", path: options.bodyFile ?? null }, oldIssue?.body ?? null),
|
||||
update: bodyUpdatePlan(commandName, repo, issueNumber, options.mode, body, bodySource, oldIssue?.body ?? null),
|
||||
bodyOnlySafety: {
|
||||
oldBody: dryRunOldBody,
|
||||
newBody: {
|
||||
@@ -5165,7 +5175,7 @@ async function issueEdit(repo: string, token: string, issueNumber: number, optio
|
||||
wouldPatch: {
|
||||
issueNumber,
|
||||
title: options.title ?? null,
|
||||
bodyFromFile: options.bodyFile,
|
||||
bodySource,
|
||||
mode: options.mode,
|
||||
bodyChars: finalBody.length,
|
||||
bodySha: bodySha(finalBody),
|
||||
@@ -5197,6 +5207,7 @@ async function issueEdit(repo: string, token: string, issueNumber: number, optio
|
||||
repo,
|
||||
issue: issueSummary(issue, { includeBody: options.raw || options.full }),
|
||||
mode: options.mode,
|
||||
bodySource,
|
||||
guard,
|
||||
concurrency,
|
||||
disclosure: issueWriteDisclosure(options, repo, issueNumber, false),
|
||||
@@ -5869,11 +5880,11 @@ export function ghHelp(): unknown {
|
||||
"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]",
|
||||
"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] [--full|--raw]",
|
||||
"bun scripts/cli.ts gh issue edit <number> --body-file <file> [--full|--raw] [compat alias for issue update --mode replace]",
|
||||
"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] [--full|--raw]",
|
||||
"bun scripts/cli.ts gh issue edit <number> --body-file <file|-> [--full|--raw] [compat alias for issue update --mode replace]",
|
||||
"bun scripts/cli.ts gh issue edit 24 --body-file <file> --notify-claudeqq-brief-diff [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue comment create <number> --body-file <file>|--body <short-text> [--repo owner/name] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue comment create <number> --body-file <file|->|--body <short-text> [--repo owner/name] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue comment delete <commentId> [--repo owner/name] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue close|reopen <number> [--repo owner/name] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue delete <number> [unsupported: use close]",
|
||||
@@ -5914,16 +5925,15 @@ export function ghHelp(): unknown {
|
||||
"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.",
|
||||
"--raw and --full are explicit full-disclosure aliases for gh issue read/view/update/edit and gh pr read/view. For issue writes, default success output omits full issue.body and returns bodyChars/bodySha/bodyPreview plus readCommands; --full|--raw includes the full returned issue body.",
|
||||
"GitHub CLI output larger than 20 KiB is automatically written to /tmp/unidesk-cli-output/*.json; stdout stays bounded JSON with outputTruncated=true, the dump path, total bytes/lines, and head/tail previews.",
|
||||
"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.",
|
||||
"issue create accepts --body-file <file|-> plus repeatable --label values and comma-separated labels; inline --body is intentionally unsupported for issue creation. 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.",
|
||||
"issue edit is a compatibility alias for issue update --mode replace.",
|
||||
"issue update --body-file refuses literal null, blank, and too-short bodies by default. Use --allow-short-body only for intentional short writes; #20 requires its board heading, warns when HWLAB product/user issue routing appears in favor of pikasTech/HWLAB, and still rejects commander brief update sections; commander-brief requires its stable heading on legacy #24 plus daily rolling brief issues titled YYYY-MM-DD 指挥简报(北京时间).",
|
||||
"issue update dry-run reports bounded bodyPreview/bodyPreviewLines, old/new body length slots, body SHA, required heading checks, literal \\n detection, shell-pollution signals, guard/concurrency summary, wouldPatch, and readCommands without printing an unbounded full body. Non-dry-run can use --expect-updated-at or --expect-body-sha for stale-cache protection.",
|
||||
"issue comment create accepts --body only for short single-line text. Blank, multiline, shell-polluted, secret-like, and overlong inline bodies fail structurally; use --body-file for Markdown, generated content, or long comments.",
|
||||
"Issue body stdin is intentionally unsupported in this CLI; issue comment create also rejects --body-file - stdin. Write generated Markdown to a file and pass --body-file.",
|
||||
"When staging a body file from a shell, use a quoted heredoc such as cat <<'EOF' > /tmp/body.md so backticks and backslashes are not expanded before --body-file reads the file.",
|
||||
"For JSON request bodies in other CLI namespaces, prefer --body-file or --body-stdin over long inline shell arguments. GitHub issue Markdown writes intentionally use --body-file for long or multiline content; PR edit/update also accepts --body-file - for stdin when a runner already has reviewed Markdown on stdin.",
|
||||
"issue update --body-file accepts files or - for stdin, refuses literal null, blank, and too-short bodies by default. Use --allow-short-body only for intentional short writes; #20 requires its board heading, warns when HWLAB product/user issue routing appears in favor of pikasTech/HWLAB, and still rejects commander brief update sections; commander-brief requires its stable heading on legacy #24 plus daily rolling brief issues titled YYYY-MM-DD 指挥简报(北京时间).",
|
||||
"issue update dry-run reports bounded bodyPreview/bodyPreviewLines, old/new body length slots, body SHA, required heading checks, literal \\n detection, shell-pollution signals, guard/concurrency summary, wouldPatch, and readCommands without printing an unbounded full body. Non-dry-run automatically reads current issue metadata before PATCH and returns oldBodySha/updatedAt; --expect-updated-at or --expect-body-sha remain available for explicit stale-cache protection.",
|
||||
"issue comment create accepts --body-file <file|-> for Markdown/generated content and --body only for short single-line text. Blank, multiline, shell-polluted, secret-like, and overlong inline bodies fail structurally.",
|
||||
"For one-shot issue writes, pipe reviewed Markdown through stdin: cat body.md | bun scripts/cli.ts gh issue update <number> --repo owner/name --body-file - or gh issue comment create <number> --body-file -. When staging a body file from a shell, use a quoted heredoc such as cat <<'EOF' > /tmp/body.md so backticks and backslashes are not expanded before --body-file reads the file.",
|
||||
"For JSON request bodies in other CLI namespaces, prefer --body-file or --body-stdin over long inline shell arguments. GitHub issue/PR Markdown writes use --body-file <file|-> for long or multiline content.",
|
||||
"issue scan-escape classifies literal \\n findings as suspected-pollution, explanatory-mention, or risk, and emits cleanupSuggestions with body/comment ids plus diff-like previews. cleanup-plan is an alias that remains dry-run/read-only.",
|
||||
"issue board-audit is read-only and defaults to repo pikasTech/unidesk plus board issue #20. It reads only the board issue body, returns body size/SHA and parsed Markdown board sections, and no longer validates GitHub open/closed issue coverage against OPEN/CLOSED tables. The legacy coverage fields remain present as empty arrays/zero counts for compatibility.",
|
||||
"issue board-row list/get reuse the board-audit table parser and are read-only. board-row update changes one table cell by issue number, returns old/new row, body SHA, body guard and request plan, and defaults to dry-run unless --expect-updated-at or --expect-body-sha is supplied for the guarded PATCH. Field aliases map status and validation to the 验收状态 column, tasks to 相关 Code Queue 任务, and focus to 当前关注点.",
|
||||
@@ -5977,7 +5987,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
supportedCommands: [
|
||||
"bun scripts/cli.ts gh issue read owner/name#<number> --raw",
|
||||
"bun scripts/cli.ts gh issue read <number> --repo owner/name --json body,title,state,comments",
|
||||
"bun scripts/cli.ts gh issue update <number> --repo owner/name --body-file <file> --expect-body-sha <sha> --full",
|
||||
"cat body.md | bun scripts/cli.ts gh issue update <number> --repo owner/name --body-file - --full",
|
||||
"bun scripts/cli.ts gh pr read owner/name#<number> --raw",
|
||||
`bun scripts/cli.ts gh pr read <number> --repo owner/name --json ${readViewSupportedJsonFields("pr")}`,
|
||||
"bun scripts/cli.ts gh pr preflight <number> --repo owner/name --full",
|
||||
|
||||
Reference in New Issue
Block a user