feat: add heredoc body input to gh cli
This commit is contained in:
@@ -665,7 +665,7 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
assertCondition(notes.some((line) => line.includes("GitHub issue URLs") && line.includes("owner/repo#number shorthand")), "gh help should explain issue view/read URL and shorthand targets", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("--number is accepted on single issue/comment numeric target commands") && line.includes("Comment delete treats --number as commentId")), "gh help should document issue --number compatibility scope", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("--raw and --full are explicit full-disclosure aliases")), "gh help should explain raw/full read disclosure", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("issue comment create accepts --body-file <file|->") && line.includes("--body only for short single-line text")), "gh help should document issue comment stdin and inline safety limits", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("issue comment create accepts --body-stdin") && line.includes("--body only for short single-line text")), "gh help should document issue comment heredoc stdin and inline safety limits", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("board-row update changes one table cell")), "gh help should describe board-row update safety", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("board-row upsert updates an existing row")), "gh help should describe board-row upsert safety", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("board-row add/move/delete are row-scoped")), "gh help should describe board-row row mutation safety", { notes });
|
||||
@@ -785,7 +785,7 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
assertCondition(scanFindings.some((finding) => finding.issueNumber === 53 && finding.kind === "null-body" && finding.classification === "risk"), "null body should be guarded as risk", scanFindings);
|
||||
const cleanupSuggestions = scanData.cleanupSuggestions as JsonRecord[];
|
||||
assertCondition(Array.isArray(cleanupSuggestions), "scan should expose cleanupSuggestions", scanData);
|
||||
assertCondition(cleanupSuggestions.some((suggestion) => suggestion.issueNumber === 51 && suggestion.type === "issue-body" && typeof suggestion.bodyId === "string" && suggestion.action === "rewrite-issue-body-with-body-file"), "issue body cleanup suggestion should use body-file rewrite with body id", cleanupSuggestions);
|
||||
assertCondition(cleanupSuggestions.some((suggestion) => suggestion.issueNumber === 51 && suggestion.type === "issue-body" && typeof suggestion.bodyId === "string" && suggestion.action === "rewrite-issue-body-with-body-stdin"), "issue body cleanup suggestion should use heredoc/stdin rewrite with body id", cleanupSuggestions);
|
||||
assertCondition(cleanupSuggestions.some((suggestion) => suggestion.commentId === 5101 && suggestion.type === "comment-body" && String(suggestion.bodyId ?? "").includes("comment:5101") && suggestion.action === "review-comment-manually"), "comment cleanup suggestion should be manual review with body id", cleanupSuggestions);
|
||||
assertCondition(cleanupSuggestions.every((suggestion) => suggestion.issueNumber !== 52), "explanatory mention should not create cleanup suggestion", cleanupSuggestions);
|
||||
const scanPatchCount = mock.requests.filter((request) => request.method === "PATCH" || request.method === "DELETE" || request.method === "POST").length;
|
||||
@@ -1471,7 +1471,7 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
"",
|
||||
"## 常驻观察与长期建议",
|
||||
"",
|
||||
"- 保持滚动简报正文只通过 body-file 更新。",
|
||||
"- 保持滚动简报正文只通过 heredoc/stdin 更新。",
|
||||
"",
|
||||
"## 更新 2026-05-21 15:18 北京时间",
|
||||
"",
|
||||
@@ -1570,10 +1570,16 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
const issueCreateStdinWriteCount = mock.requests.slice(issueCreateStdinRequestCountBefore).filter((request) => request.method === "POST" && request.url === "/repos/pikasTech/unidesk/issues").length;
|
||||
assertCondition(issueCreateStdinWriteCount === 0, "issue create stdin dry-run must not POST GitHub", { requests: mock.requests.slice(issueCreateStdinRequestCountBefore) });
|
||||
|
||||
const issueCreateBodyStdin = await runCli(["gh", "issue", "create", "--repo", "pikasTech/unidesk", "--title", "body-stdin issue create", "--body-stdin", "--dry-run"], env, issueCreateStdinBody);
|
||||
assertCondition(issueCreateBodyStdin.status === 0, "issue create dry-run should accept --body-stdin", issueCreateBodyStdin.json ?? { stdout: issueCreateBodyStdin.stdout });
|
||||
const issueCreateBodyStdinData = dataOf(issueCreateBodyStdin.json ?? {});
|
||||
const issueCreateBodyStdinSource = issueCreateBodyStdinData.bodySource as JsonRecord;
|
||||
assertCondition(issueCreateBodyStdinSource.kind === "stdin" && issueCreateBodyStdinSource.path === "-", "issue create --body-stdin should expose stdin source", issueCreateBodyStdinData);
|
||||
|
||||
const issueCreateInline = await runCli(["gh", "issue", "create", "--repo", "pikasTech/unidesk", "--title", "inline rejected", "--body", "inline body", "--dry-run"], env);
|
||||
assertCondition(issueCreateInline.status !== 0, "issue create inline --body should fail", issueCreateInline.json ?? { stdout: issueCreateInline.stdout });
|
||||
const issueCreateInlineData = failedDataOf(issueCreateInline.json ?? {});
|
||||
assertCondition(failureMessageOf(issueCreateInlineData).includes("does not support --body"), "issue create inline --body should point to body-file", issueCreateInlineData);
|
||||
assertCondition(failureMessageOf(issueCreateInlineData).includes("does not support --body"), "issue create inline --body should point to body-stdin", issueCreateInlineData);
|
||||
|
||||
const issueCreateMissingLabel = await runCli(["gh", "issue", "create", "--repo", "pikasTech/unidesk", "--title", "bad label", "--body-file", appendFile, "--label", "missing-label"], env);
|
||||
assertCondition(issueCreateMissingLabel.status !== 0, "issue create missing label should fail structurally", issueCreateMissingLabel.json ?? { stdout: issueCreateMissingLabel.stdout });
|
||||
@@ -1729,6 +1735,17 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
const closePatchPayload = JSON.parse(closeWriteRequests[1]?.body ?? "{}") as JsonRecord;
|
||||
assertCondition(closeCommentPayload.body === closeComment && closePatchPayload.state === "closed", "issue close --comment payloads should preserve comment and state", { closeCommentPayload, closePatchPayload });
|
||||
|
||||
const reopenCommentStdinBody = "reopen heredoc comment\n\n- keeps `code`\n";
|
||||
const reopenStdinRequestCountBefore = mock.requests.length;
|
||||
const reopenStdin = await runCli(["gh", "issue", "reopen", "20", "--repo", "pikasTech/unidesk", "--comment-stdin", "--dry-run"], env, reopenCommentStdinBody);
|
||||
assertCondition(reopenStdin.status === 0, "issue reopen --comment-stdin dry-run should succeed", reopenStdin.json ?? { stdout: reopenStdin.stdout });
|
||||
const reopenStdinData = dataOf(reopenStdin.json ?? {});
|
||||
const reopenStdinComment = reopenStdinData.comment as JsonRecord;
|
||||
const reopenStdinCommentSource = (reopenStdinComment.bodySource ?? {}) as JsonRecord;
|
||||
assertCondition(reopenStdinCommentSource.kind === "stdin" && reopenStdinCommentSource.path === "-", "issue reopen --comment-stdin should expose stdin source", reopenStdinComment);
|
||||
const reopenStdinWriteCount = mock.requests.slice(reopenStdinRequestCountBefore).filter((request) => request.method === "POST" || request.method === "PATCH").length;
|
||||
assertCondition(reopenStdinWriteCount === 0, "issue reopen --comment-stdin dry-run must not write GitHub", { requests: mock.requests.slice(reopenStdinRequestCountBefore) });
|
||||
|
||||
const closeCommentWrongCommand = await runCli(["gh", "issue", "comment", "create", "36", "--repo", "pikasTech/unidesk", "--comment", closeComment, "--dry-run"], env);
|
||||
assertCondition(closeCommentWrongCommand.status !== 0, "--comment outside issue close/reopen should fail structurally", closeCommentWrongCommand.json ?? { stdout: closeCommentWrongCommand.stdout });
|
||||
const closeCommentWrongData = failedDataOf(closeCommentWrongCommand.json ?? {});
|
||||
@@ -1737,7 +1754,7 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
const missingCommentBody = await runCli(["gh", "issue", "comment", "create", "36", "--repo", "pikasTech/unidesk", "--dry-run"], env);
|
||||
assertCondition(missingCommentBody.status !== 0, "issue comment create without body source should fail", missingCommentBody.json ?? { stdout: missingCommentBody.stdout });
|
||||
const missingCommentBodyData = failedDataOf(missingCommentBody.json ?? {});
|
||||
assertCondition(missingCommentBodyData.degradedReason === "validation-failed" && failureMessageOf(missingCommentBodyData).includes("requires --body-file <file> or --body <text>"), "missing issue comment body should be structured validation failure", missingCommentBodyData);
|
||||
assertCondition(missingCommentBodyData.degradedReason === "validation-failed" && failureMessageOf(missingCommentBodyData).includes("requires --body-stdin"), "missing issue comment body should be structured validation failure", missingCommentBodyData);
|
||||
|
||||
const mutualCommentBody = await runCli(["gh", "issue", "comment", "create", "36", "--repo", "pikasTech/unidesk", "--body", "inline", "--body-file", appendFile, "--dry-run"], env);
|
||||
assertCondition(mutualCommentBody.status !== 0, "issue comment create with body and body-file should fail", mutualCommentBody.json ?? { stdout: mutualCommentBody.stdout });
|
||||
@@ -1752,7 +1769,7 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
const multilineInlineComment = await runCli(["gh", "issue", "comment", "create", "36", "--repo", "pikasTech/unidesk", "--body", "line1\nline2", "--dry-run"], env);
|
||||
assertCondition(multilineInlineComment.status !== 0, "multiline inline issue comment body should fail", multilineInlineComment.json ?? { stdout: multilineInlineComment.stdout });
|
||||
const multilineInlineCommentData = failedDataOf(multilineInlineComment.json ?? {});
|
||||
assertCondition(failureMessageOf(multilineInlineCommentData).includes("single-line text only"), "multiline inline issue comment body should point to body-file", multilineInlineCommentData);
|
||||
assertCondition(failureMessageOf(multilineInlineCommentData).includes("single-line text only"), "multiline inline issue comment body should point to body-stdin", multilineInlineCommentData);
|
||||
|
||||
const pollutedInlineComment = await runCli(["gh", "issue", "comment", "create", "36", "--repo", "pikasTech/unidesk", "--body", "literal \\n pollution", "--dry-run"], env);
|
||||
assertCondition(pollutedInlineComment.status !== 0, "polluted inline issue comment body should fail", pollutedInlineComment.json ?? { stdout: pollutedInlineComment.stdout });
|
||||
@@ -1774,6 +1791,12 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
assertCondition(stdinCommentSource.kind === "stdin" && stdinCommentSource.path === "-" && stdinCommentData.source === "stdin", "stdin issue comment dry-run should expose stdin source", stdinCommentData);
|
||||
assertCondition(stdinCommentData.containsBackticks === true && stdinCommentData.containsLiteralBackslashN === false, "stdin issue comment should preserve Markdown signals", stdinCommentData);
|
||||
|
||||
const bodyStdinComment = await runCli(["gh", "issue", "comment", "create", "36", "--repo", "pikasTech/unidesk", "--body-stdin", "--dry-run"], env, stdinCommentBody);
|
||||
assertCondition(bodyStdinComment.status === 0, "issue comment --body-stdin should be supported", bodyStdinComment.json ?? { stdout: bodyStdinComment.stdout });
|
||||
const bodyStdinCommentData = dataOf(bodyStdinComment.json ?? {});
|
||||
const bodyStdinCommentSource = bodyStdinCommentData.bodySource as JsonRecord;
|
||||
assertCondition(bodyStdinCommentSource.kind === "stdin" && bodyStdinCommentSource.path === "-" && bodyStdinCommentData.source === "stdin", "issue comment --body-stdin should expose stdin source", bodyStdinCommentData);
|
||||
|
||||
const commentDeleteDryRun = await runCli(["gh", "issue", "comment", "delete", "9001", "--repo", "pikasTech/unidesk", "--dry-run"], env);
|
||||
assertCondition(commentDeleteDryRun.status === 0, "issue comment delete dry-run should succeed", commentDeleteDryRun.json ?? { stdout: commentDeleteDryRun.stdout });
|
||||
const commentDeleteDryRunData = dataOf(commentDeleteDryRun.json ?? {});
|
||||
@@ -1819,7 +1842,7 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
"issue board-row update rejects literal backslash-n cell values",
|
||||
"issue board-row update escapes markdown table pipes and performs guarded PATCH with --expect-body-sha",
|
||||
"issue board-row move is supported, defaults to dry-run, and can migrate OPEN rows into CLOSED",
|
||||
"issue create dry-run parses repeated/comma labels, supports stdin, rejects inline --body, and exposes request plan",
|
||||
"issue create dry-run parses repeated/comma labels, supports --body-stdin and compatible --body-file -, rejects inline --body, and exposes request plan",
|
||||
"issue create sends labels through REST and preserves GitHub validation errors for missing labels",
|
||||
"issue list unsupported fields and states fail structurally",
|
||||
"issue read supports body,title,state,closed,closedAt,comments selection",
|
||||
@@ -1838,8 +1861,9 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
"issue update replace/append modes preserve Markdown and support stdin with automatic current issue metadata checks",
|
||||
"issue update non-dry-run success defaults to compact output without full issue.body and exposes bodySha plus drill-down commands",
|
||||
"issue update --full explicitly includes full issue.body",
|
||||
"issue close/reopen supports --comment-stdin dry-run without writes",
|
||||
"issue comment create supports short inline --body dry-run and write with bounded output",
|
||||
"issue comment create supports stdin and still rejects missing, blank, multiline inline, polluted inline, secret-like inline, and mixed body sources",
|
||||
"issue comment create supports --body-stdin and compatible --body-file -, and still rejects missing, blank, multiline inline, polluted inline, secret-like inline, and mixed body sources",
|
||||
"issue comment create/delete follows CRUD shape",
|
||||
"issue hard delete is structurally unsupported",
|
||||
],
|
||||
|
||||
@@ -684,7 +684,7 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
|
||||
const editStdinBody = "stdin line\n`stdin code`\n| c | d |\n";
|
||||
const beforeEditRequests = mock2.requests.length;
|
||||
const editStdin = await runCli(["gh", "pr", "edit", "42", "--repo", "pikasTech/unidesk", "--title", "stdin title", "--body-file", "-"], env2, editStdinBody);
|
||||
const editStdin = await runCli(["gh", "pr", "edit", "42", "--repo", "pikasTech/unidesk", "--title", "stdin title", "--body-stdin"], env2, editStdinBody);
|
||||
assertCondition(editStdin.status === 0, "pr edit stdin should succeed", editStdin.json ?? { stdout: editStdin.stdout });
|
||||
const editStdinData = dataOf(editStdin.json ?? {});
|
||||
assertCondition(editStdinData.command === "pr edit" && editStdinData.pr === 42 && editStdinData.url === "https://github.com/pikasTech/unidesk/pull/42", "pr edit stdin should return low-noise summary", editStdinData);
|
||||
@@ -720,6 +720,16 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
const commentCreateData = dataOf(commentCreate.json ?? {});
|
||||
assertCondition(commentCreateData.command === "pr comment create", "pr comment create should use CRUD command name", commentCreateData);
|
||||
|
||||
const prBodyStdinCommentBody = "PR heredoc comment line\n\n- keeps `code`\n";
|
||||
const prBodyStdinComment = await runCli(["gh", "pr", "comment", "create", "42", "--repo", "pikasTech/unidesk", "--body-stdin"], env2, prBodyStdinCommentBody);
|
||||
assertCondition(prBodyStdinComment.status === 0, "pr comment create --body-stdin should succeed", prBodyStdinComment.json ?? { stdout: prBodyStdinComment.stdout });
|
||||
const prBodyStdinCommentData = dataOf(prBodyStdinComment.json ?? {});
|
||||
const prBodyStdinSource = prBodyStdinCommentData.bodySource as JsonRecord;
|
||||
assertCondition(prBodyStdinSource.kind === "stdin" && prBodyStdinSource.path === "-", "pr comment create --body-stdin should expose stdin source", prBodyStdinCommentData);
|
||||
const prBodyStdinRequest = mock2.requests.filter((request) => request.method === "POST" && request.url === "/repos/pikasTech/unidesk/issues/42/comments").at(-1);
|
||||
const prBodyStdinPayload = JSON.parse(prBodyStdinRequest?.body ?? "{}") as JsonRecord;
|
||||
assertCondition(prBodyStdinPayload.body === prBodyStdinCommentBody, "pr comment --body-stdin payload should preserve stdin markdown", prBodyStdinPayload);
|
||||
|
||||
const prInlineCommentBody = "short PR inline comment remains supported";
|
||||
const prInlineComment = await runCli(["gh", "pr", "comment", "create", "42", "--repo", "pikasTech/unidesk", "--body", prInlineCommentBody], env2);
|
||||
assertCondition(prInlineComment.status === 0, "pr comment create --body should remain supported", prInlineComment.json ?? { stdout: prInlineComment.stdout });
|
||||
@@ -787,9 +797,9 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
"pr create dry-run exposes planned operation",
|
||||
"pr comment dry-run preserves markdown text",
|
||||
"pr update/edit use low-noise REST PATCH without GraphQL projectCards",
|
||||
"pr edit supports --body-file - stdin without echoing full body",
|
||||
"pr edit supports --body-stdin without echoing full body",
|
||||
"pr update append and close/reopen are available",
|
||||
"pr comment create/delete follows CRUD shape and --body remains supported",
|
||||
"pr comment create/delete follows CRUD shape, --body-stdin, and --body remains supported",
|
||||
"pr merge is guarded by preflight and uses REST",
|
||||
"pr hard delete is blocked",
|
||||
"pr create validation failures are structured",
|
||||
|
||||
+57
-43
@@ -60,7 +60,7 @@ const GH_VALUE_OPTIONS = new Set([
|
||||
"--tasks", "--summary", "--focus", "--validation", "--progress", "--number", "--pr",
|
||||
"--search", "--inactive-hours", "--comment", "--comment-file", "--description",
|
||||
]);
|
||||
const GH_FLAG_OPTIONS = new Set(["--dry-run", "--draft", "--notify-claudeqq-brief-diff", "--allow-short-body", "--raw", "--full", "--stat", "--merge", "--squash", "--rebase", "--delete-branch", "--private", "--public", "--auto-init"]);
|
||||
const GH_FLAG_OPTIONS = new Set(["--dry-run", "--draft", "--notify-claudeqq-brief-diff", "--allow-short-body", "--body-stdin", "--comment-stdin", "--raw", "--full", "--stat", "--merge", "--squash", "--rebase", "--delete-branch", "--private", "--public", "--auto-init"]);
|
||||
const MIN_SAFE_BODY_SCAN_CHARS = MIN_SAFE_ISSUE_BODY_CHARS;
|
||||
const ISSUE_SCAN_MAX_FINDINGS = 60;
|
||||
const ISSUE_BODY_PROFILES = {
|
||||
@@ -100,7 +100,7 @@ type IssueBodyProfileOption = "auto" | IssueBodyProfileName;
|
||||
type EscapeFindingClassification = "suspected-pollution" | "explanatory-mention" | "risk";
|
||||
type EscapeFindingSeverity = "low" | "medium" | "high";
|
||||
type EscapeBodyKind = "issue-body" | "comment-body";
|
||||
type CleanupSuggestionAction = "rewrite-issue-body-with-body-file" | "review-comment-manually" | "review-body-length" | "no-cleanup-needed";
|
||||
type CleanupSuggestionAction = "rewrite-issue-body-with-body-stdin" | "review-comment-manually" | "review-body-length" | "no-cleanup-needed";
|
||||
type BoardSectionKind = "open" | "closed";
|
||||
type BoardRequiredColumn = typeof BOARD_AUDIT_REQUIRED_COLUMNS[number];
|
||||
type BoardColumnKind = BoardRequiredColumn | "focus" | "githubStatus" | "category" | "summary";
|
||||
@@ -219,7 +219,7 @@ interface EscapeCleanupSuggestion {
|
||||
reason: string;
|
||||
findings: Array<Pick<EscapeMatchFinding, "kind" | "classification" | "severity" | "lineNumber" | "column" | "snippet" | "match">>;
|
||||
plannedCommand?: string;
|
||||
bodyFileHint?: string;
|
||||
bodyInputHint?: string;
|
||||
preview?: {
|
||||
before: string;
|
||||
after: string;
|
||||
@@ -868,6 +868,15 @@ function resolveRepoOption(args: string[]): string {
|
||||
return explicitRepo ?? DEFAULT_REPO;
|
||||
}
|
||||
|
||||
function stdinAliasFileOption(args: string[], fileOption: string, stdinFlag: string): string | undefined {
|
||||
const file = optionValue(args, fileOption);
|
||||
const stdin = hasFlag(args, stdinFlag);
|
||||
if (file !== undefined && stdin) {
|
||||
throw new Error(`${fileOption} and ${stdinFlag} are mutually exclusive; use one body source`);
|
||||
}
|
||||
return stdin ? "-" : file;
|
||||
}
|
||||
|
||||
function parseOptions(args: string[]): GitHubOptions {
|
||||
validateKnownOptions(args);
|
||||
const [top, sub] = args;
|
||||
@@ -894,9 +903,9 @@ function parseOptions(args: string[]): GitHubOptions {
|
||||
search: optionValue(args, "--search"),
|
||||
title: optionValue(args, "--title"),
|
||||
body: optionValue(args, "--body"),
|
||||
bodyFile: optionValue(args, "--body-file"),
|
||||
bodyFile: stdinAliasFileOption(args, "--body-file", "--body-stdin"),
|
||||
comment: optionValue(args, "--comment"),
|
||||
commentFile: optionValue(args, "--comment-file"),
|
||||
commentFile: stdinAliasFileOption(args, "--comment-file", "--comment-stdin"),
|
||||
repoDescription: optionValue(args, "--description"),
|
||||
repoVisibility: parseRepoVisibility(args),
|
||||
repoAutoInit: hasFlag(args, "--auto-init"),
|
||||
@@ -1299,7 +1308,7 @@ function readMarkdownBodyFileOrStdin(path: string): { body: string; bodySource:
|
||||
}
|
||||
|
||||
function readOptionalMarkdownBody(options: GitHubOptions, command: string): { body: string; bodySource: Record<string, unknown> } | null {
|
||||
if (options.bodyFile !== undefined && options.body !== undefined) throw new Error(`${command} accepts only one body source: --body-file or --body`);
|
||||
if (options.bodyFile !== undefined && options.body !== undefined) throw new Error(`${command} accepts only one body source: --body-file/--body-stdin or --body`);
|
||||
if (options.bodyFile !== undefined) {
|
||||
return readMarkdownBodyFileOrStdin(options.bodyFile);
|
||||
}
|
||||
@@ -1308,7 +1317,7 @@ function readOptionalMarkdownBody(options: GitHubOptions, command: string): { bo
|
||||
body: options.body,
|
||||
bodySource: {
|
||||
kind: "inline",
|
||||
warning: options.body.includes("\n") ? "inline body contains real newlines; --body-file is safer for generated Markdown" : "inline body is intended only for short single-line text",
|
||||
warning: options.body.includes("\n") ? "inline body contains real newlines; --body-stdin with a quoted heredoc is safer for generated Markdown" : "inline body is intended only for short single-line text",
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1318,7 +1327,7 @@ function readOptionalMarkdownBody(options: GitHubOptions, command: string): { bo
|
||||
function readMarkdownBody(options: GitHubOptions, command: string): { body: string; bodySource: Record<string, unknown> } {
|
||||
const body = readOptionalMarkdownBody(options, command);
|
||||
if (body !== null) return body;
|
||||
throw new Error(`${command} requires --body-file <file> or --body <text>`);
|
||||
throw new Error(`${command} requires --body-stdin, --body-file <file|->, or --body <text>`);
|
||||
}
|
||||
|
||||
function secretLikeInlineFindings(body: string): string[] {
|
||||
@@ -1332,27 +1341,27 @@ function secretLikeInlineFindings(body: string): string[] {
|
||||
|
||||
function readIssueCommentBody(options: GitHubOptions): { body: string; bodySource: Record<string, unknown> } {
|
||||
if (options.bodyFile !== undefined && options.body !== undefined) {
|
||||
throw new Error("issue comment create accepts only one body source: --body-file or --body");
|
||||
throw new Error("issue comment create accepts only one body source: --body-file/--body-stdin or --body");
|
||||
}
|
||||
if (options.bodyFile !== undefined) {
|
||||
return readMarkdownBodyFileOrStdin(options.bodyFile);
|
||||
}
|
||||
if (options.body === undefined) throw new Error("issue comment create requires --body-file <file> or --body <text>");
|
||||
if (options.body === undefined) throw new Error("issue comment create requires --body-stdin, --body-file <file|->, or --body <text>");
|
||||
const body = options.body;
|
||||
const trimmed = body.trim();
|
||||
const shellPollution = shellPollutionEvidence(body);
|
||||
const secretLike = secretLikeInlineFindings(body);
|
||||
if (trimmed.length === 0) {
|
||||
throw new Error("issue comment create --body must not be blank; use --body-file for reviewed Markdown");
|
||||
throw new Error("issue comment create --body must not be blank; use --body-stdin with a quoted heredoc for reviewed Markdown");
|
||||
}
|
||||
if (body.length > MAX_INLINE_ISSUE_COMMENT_BODY_CHARS) {
|
||||
throw new Error(`issue comment create --body is limited to ${MAX_INLINE_ISSUE_COMMENT_BODY_CHARS} characters; use --body-file for long Markdown`);
|
||||
throw new Error(`issue comment create --body is limited to ${MAX_INLINE_ISSUE_COMMENT_BODY_CHARS} characters; use --body-stdin with a quoted heredoc for long Markdown`);
|
||||
}
|
||||
if (body.includes("\n") || body.includes("\r")) {
|
||||
throw new Error("issue comment create --body supports short single-line text only; use --body-file for multiline Markdown");
|
||||
throw new Error("issue comment create --body supports short single-line text only; use --body-stdin with a quoted heredoc for multiline Markdown");
|
||||
}
|
||||
if (shellPollution.length > 0) {
|
||||
throw new Error(`issue comment create --body contains shell-pollution signals (${shellPollution.join(",")}); use --body-file with reviewed Markdown bytes`);
|
||||
throw new Error(`issue comment create --body contains shell-pollution signals (${shellPollution.join(",")}); use --body-stdin with a quoted heredoc for reviewed Markdown bytes`);
|
||||
}
|
||||
if (secretLike.length > 0) {
|
||||
throw new Error(`issue comment create --body appears to contain secret-like text (${secretLike.join(",")}); refusing to print or submit it`);
|
||||
@@ -1362,7 +1371,7 @@ function readIssueCommentBody(options: GitHubOptions): { body: string; bodySourc
|
||||
bodySource: {
|
||||
kind: "inline",
|
||||
maxInlineBodyChars: MAX_INLINE_ISSUE_COMMENT_BODY_CHARS,
|
||||
warning: "inline issue comments are intended only for short single-line text; use --body-file for Markdown or generated content",
|
||||
warning: "inline issue comments are intended only for short single-line text; use --body-stdin with a quoted heredoc for Markdown or generated content",
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1370,10 +1379,10 @@ function readIssueCommentBody(options: GitHubOptions): { body: string; bodySourc
|
||||
function readIssueLifecycleCommentBody(options: GitHubOptions, command: string): { body: string; bodySource: Record<string, unknown> } | null {
|
||||
if (options.comment === undefined && options.commentFile === undefined) return null;
|
||||
if (options.comment !== undefined && options.commentFile !== undefined) {
|
||||
throw new Error(`${command} --comment and --comment-file are mutually exclusive`);
|
||||
throw new Error(`${command} --comment and --comment-file/--comment-stdin are mutually exclusive`);
|
||||
}
|
||||
if (options.body !== undefined || options.bodyFile !== undefined) {
|
||||
throw new Error(`${command} --comment or --comment-file cannot be combined with --body or --body-file`);
|
||||
throw new Error(`${command} --comment/--comment-file/--comment-stdin cannot be combined with --body/--body-file/--body-stdin`);
|
||||
}
|
||||
if (options.commentFile !== undefined) {
|
||||
return readIssueCommentBody({ ...options, body: undefined, bodyFile: options.commentFile });
|
||||
@@ -1593,7 +1602,7 @@ function codeQueueBoardCommanderBriefHint(boardIssueNumber: number, body?: strin
|
||||
? "Move HWLAB product/user issue tracking to pikasTech/HWLAB; keep #20 limited to UniDesk commander/Code Queue/CLI/infra governance."
|
||||
: "Use #20 only for UniDesk commander/Code Queue/CLI/infra governance rows.",
|
||||
forbiddenHeadings: ["## 更新 YYYY-MM-DD HH:mm 北京时间", "## YYYY-MM-DD HH:mm 北京时间指挥更新", "### YYYY-MM-DD HH:mm CST:..."],
|
||||
suggestedCommand: "bun scripts/cli.ts gh issue update <daily-brief-issue> --mode replace --body-file <file> --body-profile commander-brief --expect-body-sha <sha>",
|
||||
suggestedCommand: "bun scripts/cli.ts gh issue update <daily-brief-issue> --mode replace --body-stdin --body-profile commander-brief --expect-body-sha <sha> <<'EOF'\n<reviewed body>\nEOF",
|
||||
detected: commanderBriefDetected || hwlabProductDetected,
|
||||
detectedHeadings: headings,
|
||||
commanderBrief: {
|
||||
@@ -5275,6 +5284,11 @@ async function prComment(repo: string, token: string, issueNumber: number, optio
|
||||
issueNumber,
|
||||
pr: prSummary(prResult),
|
||||
comment: commentSummary(comment),
|
||||
bodySource: bodySource.bodySource,
|
||||
source: String(bodySource.bodySource.kind ?? "unknown"),
|
||||
bodyChars: body.length,
|
||||
bodySha: bodySha(body),
|
||||
bodyPreview: preview(body),
|
||||
request: {
|
||||
method: "POST",
|
||||
path: `/repos/${owner}/${name}/issues/${issueNumber}/comments`,
|
||||
@@ -5297,7 +5311,7 @@ async function prUpdate(repo: string, token: string, number: number, options: Gi
|
||||
return validationError(commandName, repo, error instanceof Error ? error.message : String(error), { number });
|
||||
}
|
||||
if (bodySource === null && options.title === undefined) {
|
||||
return validationError(commandName, repo, `${commandName} requires --title <title>, --body-file <file|->, or --body <text>`, { number });
|
||||
return validationError(commandName, repo, `${commandName} requires --title <title>, --body-stdin, --body-file <file|->, or --body <text>`, { number });
|
||||
}
|
||||
if (options.mode === "append" && bodySource === null) {
|
||||
return validationError(commandName, repo, `${commandName} --mode append requires a body source`, { number });
|
||||
@@ -5657,7 +5671,7 @@ 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>");
|
||||
if (options.body !== undefined) {
|
||||
return validationError("issue create", repo, "issue create does not support --body; use --body-file <file|-> for Markdown", {
|
||||
return validationError("issue create", repo, "issue create does not support --body; use --body-stdin with a quoted heredoc for Markdown", {
|
||||
title: options.title,
|
||||
bodySource: "inline",
|
||||
});
|
||||
@@ -5699,7 +5713,7 @@ 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> {
|
||||
if (options.bodyFile === undefined) {
|
||||
return validationError(commandName, repo, `${commandName} requires --body-file <file|->`, { issueNumber });
|
||||
return validationError(commandName, repo, `${commandName} requires --body-stdin or --body-file <file|->`, { issueNumber });
|
||||
}
|
||||
let bodyInput: { body: string; bodySource: Record<string, unknown> };
|
||||
try {
|
||||
@@ -6401,10 +6415,10 @@ function summarizeCleanupSuggestion(findings: EscapeScanEntry[]): EscapeCleanupS
|
||||
url: first.url,
|
||||
classification: suspected ? "suspected-pollution" : "risk",
|
||||
action: first.bodyKind === "issue-body"
|
||||
? (suspected ? "rewrite-issue-body-with-body-file" : "review-body-length")
|
||||
? (suspected ? "rewrite-issue-body-with-body-stdin" : "review-body-length")
|
||||
: "review-comment-manually",
|
||||
reason: suspected
|
||||
? "suspected shell escape pollution should be rewritten from a body file"
|
||||
? "suspected shell escape pollution should be rewritten from heredoc/stdin"
|
||||
: bodyRisk
|
||||
? "body length/null risk should be reviewed before any write"
|
||||
: "no cleanup needed",
|
||||
@@ -6419,8 +6433,8 @@ function summarizeCleanupSuggestion(findings: EscapeScanEntry[]): EscapeCleanupS
|
||||
})),
|
||||
...(first.bodyKind === "issue-body"
|
||||
? {
|
||||
plannedCommand: `bun scripts/cli.ts gh issue update ${first.issueNumber} --mode replace --body-file <file> --dry-run`,
|
||||
bodyFileHint: "write a cleaned Markdown body to a file, then pass it with --body-file; use a quoted heredoc to stage the file instead of inline shell text",
|
||||
plannedCommand: `bun scripts/cli.ts gh issue update ${first.issueNumber} --mode replace --body-stdin --dry-run <<'EOF'\n<cleaned body>\nEOF`,
|
||||
bodyInputHint: "pass cleaned Markdown through --body-stdin with a quoted heredoc; --body-file is only for reusable files",
|
||||
preview: {
|
||||
before: first.cleanupPreview?.before ?? first.snippet,
|
||||
after: first.cleanupPreview?.after ?? first.snippet,
|
||||
@@ -6700,13 +6714,13 @@ export function ghHelp(): unknown {
|
||||
"bun scripts/cli.ts gh issue list [owner/repo] [--state open|closed|all] [--limit N] [--search text] [--label label[,label...]]... [--repo owner/name] [--json number,title,state,closed,closedAt,url,updatedAt,createdAt,author,labels] [--raw|--full]",
|
||||
"bun scripts/cli.ts gh issue view <number|url|owner/repo#number> [--repo owner/name] [--number N compat] [--json body,title,state,closed,closedAt,comments] [--raw|--full]",
|
||||
"bun scripts/cli.ts gh issue read <number|url|owner/repo#number> [--repo owner/name] [--number N compat] [--raw|--full] [compatibility alias for issue view]",
|
||||
"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] [--number N compat] [--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|-> [--repo owner/name] [--number N compat] [--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] [--number N compat] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue create --title <title> (--body-stdin|--body-file <file|->) [--label label[,label...]]... [--repo owner/name] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue update <number> --mode replace|append (--body-stdin|--body-file <file|->) [--title title] [--repo owner/name] [--number N compat] [--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-stdin|--body-file <file|->) [--repo owner/name] [--number N compat] [--full|--raw] [compat alias for issue update --mode replace]",
|
||||
"bun scripts/cli.ts gh issue edit 24 --body-stdin --notify-claudeqq-brief-diff [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue comment create <number> (--body-stdin|--body-file <file|->|--body <short-text>) [--repo owner/name] [--number N compat] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue comment delete <commentId> [--repo owner/name] [--number N compat] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue close|reopen <number> [--repo owner/name] [--number N compat] [--comment <short-text>|--comment-file <file|->] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue close|reopen <number> [--repo owner/name] [--number N compat] [--comment <short-text>|--comment-stdin|--comment-file <file|->] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue stale-close [--repo owner/name] [--inactive-hours N] [--limit N] [--label label[,label...]]... [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue delete <number> [unsupported: use close]",
|
||||
"bun scripts/cli.ts gh issue scan-escape [--repo owner/name] [--limit N] [--dry-run]",
|
||||
@@ -6727,10 +6741,10 @@ export function ghHelp(): unknown {
|
||||
"bun scripts/cli.ts gh pr read <number|url|owner/repo#number> [--repo owner/name] [--number N compat] [--raw|--full] [compatibility alias for pr view]",
|
||||
"bun scripts/cli.ts gh pr preflight <number|owner/repo#number> [--repo owner/name] [--number N compat] [--full|--raw] [number may appear before or after options]",
|
||||
"bun scripts/cli.ts gh pr closeout <number|owner/repo#number> [--repo owner/name] [--number N compat] [--full|--raw] [number may appear before or after options; compatibility alias for pr preflight]",
|
||||
"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 edit <number> [--title title] [--body-file <file>|--body-file -|--body <text>] [--repo owner/name] [--number N compat] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr update <number> --mode replace|append [--body-file <file>|--body-file -|--body <text>] [--title title] [--repo owner/name] [--number N compat] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr comment create <number> --body-file <file>|--body <text> [--repo owner/name] [--number N compat] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr create --title <title> (--body-stdin|--body-file <file|->|--body <text>) --base <branch> --head <branch> [--repo owner/name] [--draft] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr edit <number> [--title title] [--body-stdin|--body-file <file|->|--body <text>] [--repo owner/name] [--number N compat] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr update <number> --mode replace|append [--body-stdin|--body-file <file|->|--body <text>] [--title title] [--repo owner/name] [--number N compat] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr comment create <number> (--body-stdin|--body-file <file|->|--body <text>) [--repo owner/name] [--number N compat] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr comment delete <commentId> [--repo owner/name] [--number N compat] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr close|reopen <number> [--repo owner/name] [--number N compat] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr merge <number> [--repo owner/name] [--number N compat] [--merge|--squash|--rebase] [--delete-branch] [--dry-run]",
|
||||
@@ -6747,17 +6761,17 @@ export function ghHelp(): unknown {
|
||||
"issue view is the canonical GitHub CLI-compatible read path; read remains a UniDesk compatibility alias. View/read accept positional numbers, GitHub issue URLs, and owner/repo#number shorthand, deriving --repo unless an explicit conflicting --repo is supplied. --number is accepted on single issue/comment numeric target commands for low-friction compatibility and returns a standard syntax hint; list/create/scan-escape/cleanup-plan/board-audit/board-row list do not accept it. Comment delete treats --number as commentId, not an issue number. View supports lifecycle fields closed/closedAt plus 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 list/read/view/update/edit and gh pr list/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 only on commands that explicitly support full disclosure.",
|
||||
"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 --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.",
|
||||
"issue create accepts --body-stdin or --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-stdin is the first-class heredoc/stdin source for Markdown bodies. Use quoted heredoc syntax such as bun scripts/cli.ts gh issue comment create 1 --body-stdin <<'EOF' so real newlines, backticks, and tables are read as stdin 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 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 accepts --body-stdin or --body-file <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 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.",
|
||||
"issue close/reopen default success output is compact and omits full issue.body. Optional --comment <short-text> or --comment-file <file|-> posts a bounded lifecycle comment before the state change and aborts the state change if the comment POST fails. --comment-file is the recommended path for generated Markdown closeout evidence; --comment remains the short inline form. Use gh issue view <number> --json body or --full/--raw on view when full text is needed.",
|
||||
"issue comment create accepts --body-stdin or --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.",
|
||||
"issue close/reopen default success output is compact and omits full issue.body. Optional --comment <short-text>, --comment-stdin, or --comment-file <file|-> posts a bounded lifecycle comment before the state change and aborts the state change if the comment POST fails. --comment-stdin is the first-class heredoc path for generated Markdown closeout evidence; --comment remains the short inline form. Use gh issue view <number> --json body or --full/--raw on view when full text is needed.",
|
||||
"issue stale-close is the reusable lifecycle cleanup path for policies such as closing open issues inactive for more than 48 hours. It selects open issues by GitHub updatedAt older than observedAt - --inactive-hours, treats comments and state changes as activity, filters pull requests, supports --dry-run, and returns bounded candidate/closed/failure summaries without echoing full bodies.",
|
||||
"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.",
|
||||
"For one-shot issue writes, prefer quoted heredoc stdin: bun scripts/cli.ts gh issue update <number> --repo owner/name --body-stdin <<'EOF' ... EOF or gh issue comment create <number> --body-stdin <<'EOF' ... EOF. --body-file <file|-> remains available for reusable files and pipes.",
|
||||
"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-stdin for heredoc/stdin or --body-file <file|-> for reusable files.",
|
||||
"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 当前关注点.",
|
||||
@@ -6813,7 +6827,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
"bun scripts/cli.ts gh issue list --repo owner/name --limit 200 --full",
|
||||
"bun scripts/cli.ts gh issue view owner/name#<number> --raw",
|
||||
"bun scripts/cli.ts gh issue view <number> --repo owner/name --json body,title,state,comments",
|
||||
"cat body.md | bun scripts/cli.ts gh issue update <number> --repo owner/name --body-file - --full",
|
||||
"bun scripts/cli.ts gh issue update <number> --repo owner/name --body-stdin --full <<'EOF'\n<reviewed body>\nEOF",
|
||||
"bun scripts/cli.ts gh pr list --repo owner/name --limit 100 --full",
|
||||
"bun scripts/cli.ts gh pr view owner/name#<number> --raw",
|
||||
`bun scripts/cli.ts gh pr view <number> --repo owner/name --json ${readViewSupportedJsonFields("pr")}`,
|
||||
|
||||
Reference in New Issue
Block a user