feat: add GitHub issue body patching
This commit is contained in:
@@ -93,6 +93,22 @@ EOF
|
||||
|
||||
`edit` 是 `update --mode replace` 的兼容别名。正式写入默认先读当前 issue 做 body guard。
|
||||
|
||||
### 局部修补正文
|
||||
|
||||
```bash
|
||||
bun scripts/cli.ts gh issue patch <number|owner/repo#number> \
|
||||
--repo owner/name --body-patch-stdin <<'PATCH'
|
||||
*** Begin Patch
|
||||
*** Update File: issue.md
|
||||
@@
|
||||
-old text
|
||||
+new text
|
||||
*** End Patch
|
||||
PATCH
|
||||
```
|
||||
|
||||
`issue patch` 会先读取当前 issue 正文,把 Codex `apply_patch` envelope 应用到虚拟文件 `issue.md`,再通过现有 issue body guard 写回。支持 `--dry-run`、`--expect-updated-at`、`--expect-body-sha`、`--body-patch-file <file|->`;输出包含 old/new bodySha、updatedAt、patch 摘要和 readCommands,不回显完整正文。
|
||||
|
||||
### 评论
|
||||
|
||||
```bash
|
||||
@@ -108,11 +124,22 @@ bun scripts/cli.ts gh issue comment update <commentId> \
|
||||
新的评论正文
|
||||
EOF
|
||||
|
||||
# 局部修补评论
|
||||
bun scripts/cli.ts gh issue comment patch <commentId> \
|
||||
--repo owner/name --body-patch-stdin <<'PATCH'
|
||||
*** Begin Patch
|
||||
*** Update File: comment.md
|
||||
@@
|
||||
-old text
|
||||
+new text
|
||||
*** End Patch
|
||||
PATCH
|
||||
|
||||
# 删除评论
|
||||
bun scripts/cli.ts gh issue comment delete <commentId>
|
||||
```
|
||||
|
||||
`edit` 是 `comment update` 的兼容别名。`--body <short-text>` 仅适合短单行。日常修正文案优先用 `update/edit` 保留评论 ID 和时间线;`delete` 只用于确实需要删除的评论。
|
||||
`edit` 是 `comment update` 的兼容别名。`--body <short-text>` 仅适合短单行。日常修正文案优先用 `patch` 或 `update/edit` 保留评论 ID 和时间线;`delete` 只用于确实需要删除的评论。`comment patch` 会先读取 comment id 对应的当前正文,把 envelope 应用到虚拟文件 `comment.md`,再 PATCH 单条评论;上下文不匹配时失败且不写入。
|
||||
|
||||
### 关闭/重开
|
||||
|
||||
|
||||
+430
-16
@@ -1,6 +1,7 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { ApplyPatchV2Error, deriveUpdatedContent, parseApplyPatchV2, type PatchHunk } from "./apply-patch-v2";
|
||||
import { coreInternalFetch } from "./microservices";
|
||||
|
||||
const DEFAULT_REPO = "pikasTech/unidesk";
|
||||
@@ -54,13 +55,13 @@ 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",
|
||||
"--body-file", "--body-patch-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", "--pr",
|
||||
"--search", "--inactive-hours", "--comment", "--comment-file", "--description",
|
||||
]);
|
||||
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 GH_FLAG_OPTIONS = new Set(["--dry-run", "--draft", "--notify-claudeqq-brief-diff", "--allow-short-body", "--body-stdin", "--body-patch-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 = {
|
||||
@@ -354,6 +355,7 @@ interface GitHubOptions {
|
||||
title?: string;
|
||||
body?: string;
|
||||
bodyFile?: string;
|
||||
bodyPatchFile?: string;
|
||||
comment?: string;
|
||||
commentFile?: string;
|
||||
repoDescription?: string;
|
||||
@@ -718,7 +720,7 @@ function isPrReadCommand(sub: string | undefined): boolean {
|
||||
function allowsNumberTargetAlias(top: string | undefined, sub: string | undefined, third: string | undefined): boolean {
|
||||
if (top === "preflight") return true;
|
||||
if (top === "issue") {
|
||||
if (sub === "read" || sub === "view" || sub === "edit" || sub === "update" || sub === "close" || sub === "reopen" || sub === "delete") return true;
|
||||
if (sub === "read" || sub === "view" || sub === "edit" || sub === "update" || sub === "patch" || sub === "close" || sub === "reopen" || sub === "delete") return true;
|
||||
if (sub === "comment") return true;
|
||||
if (sub === "board-row" && ["get", "update", "add", "move", "delete", "upsert"].includes(third ?? "")) return true;
|
||||
return false;
|
||||
@@ -904,6 +906,7 @@ function parseOptions(args: string[]): GitHubOptions {
|
||||
title: optionValue(args, "--title"),
|
||||
body: optionValue(args, "--body"),
|
||||
bodyFile: stdinAliasFileOption(args, "--body-file", "--body-stdin"),
|
||||
bodyPatchFile: stdinAliasFileOption(args, "--body-patch-file", "--body-patch-stdin"),
|
||||
comment: optionValue(args, "--comment"),
|
||||
commentFile: stdinAliasFileOption(args, "--comment-file", "--comment-stdin"),
|
||||
repoDescription: optionValue(args, "--description"),
|
||||
@@ -1307,6 +1310,140 @@ function readMarkdownBodyFileOrStdin(path: string): { body: string; bodySource:
|
||||
return { body: readFileSync(path, "utf8"), bodySource: { kind: "body-file", path } };
|
||||
}
|
||||
|
||||
function readBodyPatchFileOrStdin(path: string): { patch: string; patchSource: Record<string, unknown> } {
|
||||
if (path === "-") {
|
||||
return { patch: readFileSync(0, "utf8"), patchSource: { kind: "body-patch-stdin", path: "-" } };
|
||||
}
|
||||
if (!existsSync(path)) throw new Error(`body patch file not found: ${path}`);
|
||||
return { patch: readFileSync(path, "utf8"), patchSource: { kind: "body-patch-file", path } };
|
||||
}
|
||||
|
||||
function readBodyPatch(options: GitHubOptions, command: string): { patch: string; patchSource: Record<string, unknown> } {
|
||||
const conflictingSources: string[] = [];
|
||||
if (options.body !== undefined) conflictingSources.push("--body");
|
||||
if (options.bodyFile !== undefined) conflictingSources.push(options.bodyFile === "-" ? "--body-stdin" : "--body-file");
|
||||
if (options.comment !== undefined) conflictingSources.push("--comment");
|
||||
if (options.commentFile !== undefined) conflictingSources.push(options.commentFile === "-" ? "--comment-stdin" : "--comment-file");
|
||||
if (conflictingSources.length > 0) {
|
||||
throw new Error(`${command} accepts only a patch source; remove ${conflictingSources.join(",")} and use --body-patch-stdin or --body-patch-file <file|->`);
|
||||
}
|
||||
if (options.bodyPatchFile === undefined) throw new Error(`${command} requires --body-patch-stdin or --body-patch-file <file|->`);
|
||||
return readBodyPatchFileOrStdin(options.bodyPatchFile);
|
||||
}
|
||||
|
||||
function patchPathList(hunks: PatchHunk[]): string[] {
|
||||
return Array.from(new Set(hunks.map((hunk) => hunk.path)));
|
||||
}
|
||||
|
||||
function applyVirtualBodyPatch(virtualPath: "issue.md" | "comment.md", oldBody: string, patchText: string): { newBody: string; patchSummary: Record<string, unknown> } {
|
||||
const parsed = parseApplyPatchV2(patchText);
|
||||
let newBody = oldBody;
|
||||
let updateChunkCount = 0;
|
||||
for (let index = 0; index < parsed.hunks.length; index += 1) {
|
||||
const hunk = parsed.hunks[index] as PatchHunk;
|
||||
if (hunk.kind !== "update") {
|
||||
throw new ApplyPatchV2Error("GitHub body patch supports only Update File hunks", {
|
||||
hunk: index + 1,
|
||||
action: hunk.kind,
|
||||
path: hunk.path,
|
||||
expectedPath: virtualPath,
|
||||
});
|
||||
}
|
||||
if (hunk.path !== virtualPath) {
|
||||
throw new ApplyPatchV2Error("GitHub body patch target path mismatch", {
|
||||
hunk: index + 1,
|
||||
path: hunk.path,
|
||||
expectedPath: virtualPath,
|
||||
});
|
||||
}
|
||||
if (hunk.movePath !== null && hunk.movePath !== hunk.path) {
|
||||
throw new ApplyPatchV2Error("GitHub body patch does not support Move to", {
|
||||
hunk: index + 1,
|
||||
path: hunk.path,
|
||||
targetPath: hunk.movePath,
|
||||
expectedPath: virtualPath,
|
||||
});
|
||||
}
|
||||
updateChunkCount += hunk.chunks.length;
|
||||
newBody = deriveUpdatedContent(virtualPath, newBody, hunk.chunks).newContent;
|
||||
}
|
||||
if (newBody === oldBody) {
|
||||
throw new ApplyPatchV2Error("GitHub body patch did not change the body; no GitHub write will be performed", {
|
||||
path: virtualPath,
|
||||
hunkCount: parsed.hunks.length,
|
||||
});
|
||||
}
|
||||
return {
|
||||
newBody,
|
||||
patchSummary: {
|
||||
engine: "apply-patch-v2",
|
||||
virtualFile: virtualPath,
|
||||
patchBytes: Buffer.byteLength(patchText, "utf8"),
|
||||
hunkCount: parsed.hunks.length,
|
||||
updateChunkCount,
|
||||
paths: patchPathList(parsed.hunks),
|
||||
hints: parsed.hints,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizedPatchDiagnostics(value: unknown): Record<string, unknown> | null {
|
||||
if (!isRecord(value)) return null;
|
||||
const diagnostics: Record<string, unknown> = {};
|
||||
const candidateLines = value.firstExpectedLineCandidates;
|
||||
if (Array.isArray(candidateLines)) diagnostics.firstExpectedLineCandidates = candidateLines.filter((item): item is number => typeof item === "number");
|
||||
for (const key of ["firstExpectedLineCandidatesTruncated", "bestPrefixMatchedLines", "bestPrefixStartLine", "likelyMissingAddedPrefixes", "likelyStaleOrOversizedContext"]) {
|
||||
const item = value[key];
|
||||
if (typeof item === "string" || typeof item === "number" || typeof item === "boolean" || item === null) diagnostics[key] = item;
|
||||
}
|
||||
return Object.keys(diagnostics).length > 0 ? diagnostics : null;
|
||||
}
|
||||
|
||||
function sanitizedPatchDetails(value: unknown): Record<string, unknown> | null {
|
||||
if (!isRecord(value)) return null;
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const key of ["path", "targetPath", "expectedPath", "hunk", "chunk", "action", "status", "line", "hunkCount"]) {
|
||||
const item = value[key];
|
||||
if (typeof item === "string" || typeof item === "number" || typeof item === "boolean" || item === null) result[key] = item;
|
||||
}
|
||||
if (typeof value.expected === "string") {
|
||||
result.expectedLineCount = value.expected.length === 0 ? 0 : value.expected.split(/\r?\n/u).length;
|
||||
result.expectedChars = value.expected.length;
|
||||
}
|
||||
const diagnostics = sanitizedPatchDiagnostics(value.diagnostics);
|
||||
if (diagnostics !== null) result.diagnostics = diagnostics;
|
||||
if (Array.isArray(value.partialChanges)) result.partialChanges = value.partialChanges.filter((item): item is string => typeof item === "string");
|
||||
if (Array.isArray(value.outcomes)) {
|
||||
result.outcomes = value.outcomes
|
||||
.filter(isRecord)
|
||||
.map((outcome) => {
|
||||
const summary: Record<string, unknown> = {};
|
||||
for (const key of ["hunk", "action", "status", "path", "targetPath", "change"]) {
|
||||
const item = outcome[key];
|
||||
if (typeof item === "string" || typeof item === "number" || typeof item === "boolean" || item === null) summary[key] = item;
|
||||
}
|
||||
if (isRecord(outcome.error) && typeof outcome.error.message === "string") summary.errorMessage = outcome.error.message;
|
||||
return summary;
|
||||
});
|
||||
}
|
||||
if (isRecord(value.failed)) result.failed = sanitizedPatchDetails(value.failed);
|
||||
if (isRecord(value.cause)) result.cause = sanitizedPatchDetails(value.cause);
|
||||
return Object.keys(result).length > 0 ? result : null;
|
||||
}
|
||||
|
||||
function bodyPatchErrorSummary(error: unknown): Record<string, unknown> {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const summary: Record<string, unknown> = {
|
||||
name: error instanceof Error ? error.name : "Error",
|
||||
message,
|
||||
bodyTextRedacted: true,
|
||||
note: "Patch diagnostics omit expected/context text so GitHub body contents and credentials are not echoed.",
|
||||
};
|
||||
const details = error instanceof ApplyPatchV2Error ? sanitizedPatchDetails(error.details) : null;
|
||||
if (details !== null) summary.details = details;
|
||||
return summary;
|
||||
}
|
||||
|
||||
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/--body-stdin or --body`);
|
||||
if (options.bodyFile !== undefined) {
|
||||
@@ -4371,7 +4508,7 @@ function validationStringArray(record: Record<string, unknown>, key: string): st
|
||||
return Array.isArray(value) ? value.map((item) => String(item)) : [];
|
||||
}
|
||||
|
||||
function validateIssueBodyGuard(repo: string, issueNumber: number, body: string, options: GitHubOptions, profileContext: IssueProfileValidationContext = {}): GitHubCommandResult | null {
|
||||
function validateIssueBodyGuard(repo: string, issueNumber: number, body: string, options: GitHubOptions, profileContext: IssueProfileValidationContext = {}, commandName = "issue edit", bodySource: Record<string, unknown> = { kind: "body-file", path: options.bodyFile ?? null }): GitHubCommandResult | null {
|
||||
const trimmed = body.trim();
|
||||
const isLiteralNull = trimmed.toLowerCase() === "null";
|
||||
const isBlank = trimmed.length === 0;
|
||||
@@ -4393,9 +4530,9 @@ function validateIssueBodyGuard(repo: string, issueNumber: number, body: string,
|
||||
if (failures.length === 0) return null;
|
||||
const overrideAvailable = failures.every((failure) => failure === "short-body");
|
||||
const message = overrideAvailable
|
||||
? "issue edit body is shorter than the safe default; pass --allow-short-body only for intentional short writes"
|
||||
: "issue edit body failed safety guard; refusing to write possible body-only issue corruption";
|
||||
return validationError("issue edit", repo, message, {
|
||||
? `${commandName} body is shorter than the safe default; pass --allow-short-body only for intentional short writes`
|
||||
: `${commandName} body failed safety guard; refusing to write possible body-only issue corruption`;
|
||||
return validationError(commandName, repo, message, {
|
||||
issueNumber,
|
||||
guard: {
|
||||
ok: false,
|
||||
@@ -4403,7 +4540,7 @@ function validateIssueBodyGuard(repo: string, issueNumber: number, body: string,
|
||||
minSafeBodyChars: MIN_SAFE_ISSUE_BODY_CHARS,
|
||||
allowShortBody: options.allowShortBody,
|
||||
shortBodyOverrideAvailable: overrideAvailable,
|
||||
bodySource: { kind: "body-file", path: options.bodyFile ?? null },
|
||||
bodySource,
|
||||
bodyPreview: preview(body),
|
||||
bodyPreviewLines: previewLines(body),
|
||||
codeQueueBoardHint: boardBriefHint,
|
||||
@@ -4458,7 +4595,7 @@ function assertConcurrencyOptions(options: GitHubOptions, commandName: string):
|
||||
}
|
||||
}
|
||||
|
||||
function validateIssueConcurrency(repo: string, issueNumber: number, oldIssue: GitHubIssue, options: GitHubOptions): GitHubCommandResult | null {
|
||||
function validateIssueConcurrency(repo: string, issueNumber: number, oldIssue: GitHubIssue, options: GitHubOptions, commandName = "issue edit"): GitHubCommandResult | null {
|
||||
const checks: Record<string, unknown> = {
|
||||
issueNumber,
|
||||
expectUpdatedAt: options.expectUpdatedAt ?? null,
|
||||
@@ -4467,12 +4604,32 @@ function validateIssueConcurrency(repo: string, issueNumber: number, oldIssue: G
|
||||
actualBodySha: bodySha(oldIssue.body ?? ""),
|
||||
};
|
||||
if (options.expectUpdatedAt !== undefined && oldIssue.updated_at !== options.expectUpdatedAt) {
|
||||
return validationError("issue edit", repo, "--expect-updated-at did not match current GitHub issue updated_at; refusing stale body overwrite", checks);
|
||||
return validationError(commandName, repo, "--expect-updated-at did not match current GitHub issue updated_at; refusing stale body overwrite", checks);
|
||||
}
|
||||
if (options.expectBodySha !== undefined) {
|
||||
const expected = normalizeExpectedSha(options.expectBodySha);
|
||||
if (bodySha(oldIssue.body ?? "") !== expected) {
|
||||
return validationError("issue edit", repo, "--expect-body-sha did not match current GitHub issue body; refusing stale body overwrite", checks);
|
||||
return validationError(commandName, repo, "--expect-body-sha did not match current GitHub issue body; refusing stale body overwrite", checks);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateCommentConcurrency(repo: string, commentId: number, oldComment: GitHubComment, options: GitHubOptions, commandName: string): GitHubCommandResult | null {
|
||||
const checks: Record<string, unknown> = {
|
||||
commentId,
|
||||
expectUpdatedAt: options.expectUpdatedAt ?? null,
|
||||
actualUpdatedAt: oldComment.updated_at ?? null,
|
||||
expectBodySha: options.expectBodySha ?? null,
|
||||
actualBodySha: bodySha(oldComment.body ?? ""),
|
||||
};
|
||||
if (options.expectUpdatedAt !== undefined && oldComment.updated_at !== options.expectUpdatedAt) {
|
||||
return validationError(commandName, repo, "--expect-updated-at did not match current GitHub comment updated_at; refusing stale body overwrite", checks);
|
||||
}
|
||||
if (options.expectBodySha !== undefined) {
|
||||
const expected = normalizeExpectedSha(options.expectBodySha);
|
||||
if (bodySha(oldComment.body ?? "") !== expected) {
|
||||
return validationError(commandName, repo, "--expect-body-sha did not match current GitHub comment body; refusing stale body overwrite", checks);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -5477,6 +5634,11 @@ async function getIssue(token: string, repo: string, issueNumber: number): Promi
|
||||
return githubRequest<GitHubIssue>(token, "GET", `/repos/${owner}/${name}/issues/${issueNumber}`);
|
||||
}
|
||||
|
||||
async function getIssueComment(token: string, repo: string, commentId: number): Promise<GitHubComment | GitHubErrorPayload> {
|
||||
const { owner, name } = repoParts(repo);
|
||||
return githubRequest<GitHubComment>(token, "GET", `/repos/${owner}/${name}/issues/comments/${commentId}`);
|
||||
}
|
||||
|
||||
function selectedIssueJson(issue: GitHubIssue, comments: GitHubComment[] | null, fields: IssueViewJsonField[] | undefined): Record<string, unknown> | null {
|
||||
if (fields === undefined) return null;
|
||||
const summary = issueSummary(issue);
|
||||
@@ -5720,6 +5882,109 @@ async function issueCreate(repo: string, token: string, options: GitHubOptions):
|
||||
return { ok: true, command: "issue create", repo, issue: issueSummary(issue), labels, bodySource, rest: true };
|
||||
}
|
||||
|
||||
async function issuePatch(repo: string, token: string, issueNumber: number, options: GitHubOptions): Promise<GitHubCommandResult> {
|
||||
const commandName = "issue patch";
|
||||
const concurrencyOptionError = assertConcurrencyOptions(options, commandName);
|
||||
if (concurrencyOptionError !== null) return concurrencyOptionError;
|
||||
let patchInput: { patch: string; patchSource: Record<string, unknown> };
|
||||
try {
|
||||
patchInput = readBodyPatch(options, commandName);
|
||||
} catch (error) {
|
||||
return validationError(commandName, repo, error instanceof Error ? error.message : String(error), { issueNumber });
|
||||
}
|
||||
const { patch, patchSource } = patchInput;
|
||||
const oldIssue = await getIssue(token, repo, issueNumber);
|
||||
if (isGitHubError(oldIssue)) return commandError(commandName, repo, oldIssue, { issueNumber, phase: "read-before-patch" });
|
||||
const concurrencyError = validateIssueConcurrency(repo, issueNumber, oldIssue, options, commandName);
|
||||
if (concurrencyError !== null) return concurrencyError;
|
||||
const oldBody = oldIssue.body ?? "";
|
||||
let patched: { newBody: string; patchSummary: Record<string, unknown> };
|
||||
try {
|
||||
patched = applyVirtualBodyPatch("issue.md", oldBody, patch);
|
||||
} catch (error) {
|
||||
return validationError(commandName, repo, "body patch failed; no GitHub write performed", {
|
||||
issueNumber,
|
||||
patchSource,
|
||||
patchError: bodyPatchErrorSummary(error),
|
||||
oldBody: {
|
||||
bodyChars: oldBody.length,
|
||||
bodySha: bodySha(oldBody),
|
||||
updatedAt: oldIssue.updated_at ?? null,
|
||||
},
|
||||
noWrite: true,
|
||||
});
|
||||
}
|
||||
const profileContext: IssueProfileValidationContext = { issueTitle: oldIssue.title, issueBody: oldIssue.body, issueMetadataFetched: true };
|
||||
const guard = validateIssueBodyGuard(repo, issueNumber, patched.newBody, options, profileContext, commandName, patchSource);
|
||||
if (guard !== null) return guard;
|
||||
const guardSummary = issueEditGuardSummary(issueNumber, patched.newBody, options, profileContext);
|
||||
const bodyChange = {
|
||||
oldBodyChars: oldBody.length,
|
||||
newBodyChars: patched.newBody.length,
|
||||
deltaChars: patched.newBody.length - oldBody.length,
|
||||
oldBodySha: bodySha(oldBody),
|
||||
newBodySha: bodySha(patched.newBody),
|
||||
oldUpdatedAt: oldIssue.updated_at ?? null,
|
||||
};
|
||||
const concurrency = {
|
||||
checked: true,
|
||||
oldIssueUpdatedAt: oldIssue.updated_at ?? null,
|
||||
oldBodySha: bodySha(oldBody),
|
||||
expectUpdatedAt: options.expectUpdatedAt ?? null,
|
||||
expectBodySha: options.expectBodySha ?? null,
|
||||
};
|
||||
if (options.dryRun) {
|
||||
return {
|
||||
ok: true,
|
||||
command: commandName,
|
||||
repo,
|
||||
dryRun: true,
|
||||
planned: true,
|
||||
issueNumber,
|
||||
patchSource,
|
||||
patch: patched.patchSummary,
|
||||
...dryRunBody(repo, undefined, patched.newBody),
|
||||
disclosure: issueWriteDisclosure(options, repo, issueNumber, true),
|
||||
readCommands: issueBodyReadCommands(repo, issueNumber),
|
||||
guard: guardSummary,
|
||||
concurrency: { ...concurrency, dryRunNoWrite: true },
|
||||
bodyChange,
|
||||
wouldPatch: {
|
||||
issueNumber,
|
||||
bodyChars: patched.newBody.length,
|
||||
bodySha: bodySha(patched.newBody),
|
||||
method: "PATCH",
|
||||
path: `/repos/{owner}/{repo}/issues/${issueNumber}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
const { owner, name } = repoParts(repo);
|
||||
const issue = await githubRequest<GitHubIssue>(token, "PATCH", `/repos/${owner}/${name}/issues/${issueNumber}`, { body: patched.newBody });
|
||||
if (isGitHubError(issue)) return commandError(commandName, repo, issue, { issueNumber });
|
||||
return {
|
||||
ok: true,
|
||||
command: commandName,
|
||||
repo,
|
||||
issue: issueSummary(issue, { includeBody: options.raw || options.full }),
|
||||
patchSource,
|
||||
patch: patched.patchSummary,
|
||||
bodyChange: {
|
||||
...bodyChange,
|
||||
newUpdatedAt: issue.updated_at ?? null,
|
||||
},
|
||||
guard: guardSummary,
|
||||
concurrency,
|
||||
disclosure: issueWriteDisclosure(options, repo, issueNumber, false),
|
||||
readCommands: issueBodyReadCommands(repo, issueNumber),
|
||||
request: {
|
||||
method: "PATCH",
|
||||
path: `/repos/${owner}/${name}/issues/${issueNumber}`,
|
||||
bodyChars: patched.newBody.length,
|
||||
},
|
||||
rest: true,
|
||||
};
|
||||
}
|
||||
|
||||
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-stdin or --body-file <file|->`, { issueNumber });
|
||||
@@ -5955,6 +6220,116 @@ async function issueComment(repo: string, token: string, issueNumber: number, op
|
||||
};
|
||||
}
|
||||
|
||||
async function commentPatch(repo: string, token: string, ownerKind: "issue" | "pr", commentId: number, options: GitHubOptions, commandName?: string): Promise<GitHubCommandResult> {
|
||||
const command = commandName ?? `${ownerKind} comment patch`;
|
||||
const concurrencyOptionError = assertConcurrencyOptions(options, command);
|
||||
if (concurrencyOptionError !== null) return concurrencyOptionError;
|
||||
let patchInput: { patch: string; patchSource: Record<string, unknown> };
|
||||
try {
|
||||
patchInput = readBodyPatch(options, command);
|
||||
} catch (error) {
|
||||
return validationError(command, repo, error instanceof Error ? error.message : String(error), { commentId });
|
||||
}
|
||||
const { patch, patchSource } = patchInput;
|
||||
const readCommands = ownerKind === "issue"
|
||||
? {
|
||||
comments: `bun scripts/cli.ts gh issue view <issueNumber> --repo ${repo} --json comments`,
|
||||
full: `bun scripts/cli.ts gh issue view <issueNumber> --repo ${repo} --full`,
|
||||
note: "GitHub comment patch targets commentId directly; use the owning issue number to read the surrounding comments.",
|
||||
}
|
||||
: {
|
||||
comments: `bun scripts/cli.ts gh issue view <prNumber> --repo ${repo} --json comments`,
|
||||
full: `bun scripts/cli.ts gh issue view <prNumber> --repo ${repo} --full`,
|
||||
note: "GitHub PR comments are issue comments; use the owning PR number through gh issue view to read the surrounding comments.",
|
||||
};
|
||||
const oldComment = await getIssueComment(token, repo, commentId);
|
||||
if (isGitHubError(oldComment)) return commandError(command, repo, oldComment, { commentId, phase: "read-before-patch" });
|
||||
const concurrencyError = validateCommentConcurrency(repo, commentId, oldComment, options, command);
|
||||
if (concurrencyError !== null) return concurrencyError;
|
||||
const oldBody = oldComment.body ?? "";
|
||||
let patched: { newBody: string; patchSummary: Record<string, unknown> };
|
||||
try {
|
||||
patched = applyVirtualBodyPatch("comment.md", oldBody, patch);
|
||||
} catch (error) {
|
||||
return validationError(command, repo, "body patch failed; no GitHub write performed", {
|
||||
commentId,
|
||||
patchSource,
|
||||
patchError: bodyPatchErrorSummary(error),
|
||||
oldBody: {
|
||||
bodyChars: oldBody.length,
|
||||
bodySha: bodySha(oldBody),
|
||||
updatedAt: oldComment.updated_at ?? null,
|
||||
},
|
||||
noWrite: true,
|
||||
});
|
||||
}
|
||||
const bodyChange = {
|
||||
oldBodyChars: oldBody.length,
|
||||
newBodyChars: patched.newBody.length,
|
||||
deltaChars: patched.newBody.length - oldBody.length,
|
||||
oldBodySha: bodySha(oldBody),
|
||||
newBodySha: bodySha(patched.newBody),
|
||||
oldUpdatedAt: oldComment.updated_at ?? null,
|
||||
};
|
||||
const concurrency = {
|
||||
checked: true,
|
||||
oldCommentUpdatedAt: oldComment.updated_at ?? null,
|
||||
oldBodySha: bodySha(oldBody),
|
||||
expectUpdatedAt: options.expectUpdatedAt ?? null,
|
||||
expectBodySha: options.expectBodySha ?? null,
|
||||
};
|
||||
if (options.dryRun) {
|
||||
return {
|
||||
ok: true,
|
||||
command,
|
||||
repo,
|
||||
dryRun: true,
|
||||
planned: true,
|
||||
commentId,
|
||||
patchSource,
|
||||
patch: patched.patchSummary,
|
||||
bodyPreview: preview(patched.newBody),
|
||||
bodyPreviewLines: previewLines(patched.newBody, 4),
|
||||
bodyChars: patched.newBody.length,
|
||||
bodySha: bodySha(patched.newBody),
|
||||
bodyChange,
|
||||
concurrency: { ...concurrency, dryRunNoWrite: true },
|
||||
readCommands,
|
||||
wouldPatch: {
|
||||
commentId,
|
||||
bodyChars: patched.newBody.length,
|
||||
bodySha: bodySha(patched.newBody),
|
||||
method: "PATCH",
|
||||
path: `/repos/{owner}/{repo}/issues/comments/${commentId}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
const { owner, name } = repoParts(repo);
|
||||
const comment = await githubRequest<GitHubComment>(token, "PATCH", `/repos/${owner}/${name}/issues/comments/${commentId}`, { body: patched.newBody });
|
||||
if (isGitHubError(comment)) return commandError(command, repo, comment, { commentId });
|
||||
return {
|
||||
ok: true,
|
||||
command,
|
||||
repo,
|
||||
commentId,
|
||||
comment: compactCommentSummary(comment),
|
||||
patchSource,
|
||||
patch: patched.patchSummary,
|
||||
bodyChange: {
|
||||
...bodyChange,
|
||||
newUpdatedAt: comment.updated_at ?? null,
|
||||
},
|
||||
concurrency,
|
||||
readCommands,
|
||||
request: {
|
||||
method: "PATCH",
|
||||
path: `/repos/${owner}/${name}/issues/comments/${commentId}`,
|
||||
bodyChars: patched.newBody.length,
|
||||
},
|
||||
rest: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function commentUpdate(repo: string, token: string, ownerKind: "issue" | "pr", commentId: number, options: GitHubOptions, commandName?: string): Promise<GitHubCommandResult> {
|
||||
const command = commandName ?? `${ownerKind} comment update`;
|
||||
let bodyInput: { body: string; bodySource: Record<string, unknown> };
|
||||
@@ -6782,10 +7157,12 @@ export function ghHelp(): unknown {
|
||||
"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-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 patch <number|url|owner/repo#number> --body-patch-stdin [--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 update <commentId> (--body-stdin|--body-file <file|->|--body <short-text>) [--repo owner/name] [--number N compat] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue comment patch <commentId> --body-patch-stdin [--repo owner/name] [--number N compat] [--dry-run] [--expect-updated-at ts|--expect-body-sha sha256]",
|
||||
"bun scripts/cli.ts gh issue comment edit <commentId> (--body-stdin|--body-file <file|->|--body <short-text>) [--repo owner/name] [--number N compat] [--dry-run] [compatibility alias for issue comment update]",
|
||||
"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-stdin|--comment-file <file|->] [--dry-run]",
|
||||
@@ -6829,7 +7206,7 @@ export function ghHelp(): unknown {
|
||||
"issue list defaults to --state open and bounded --limit 30; it paginates GitHub REST/Search pages internally when --limit exceeds GitHub's per-page cap and discloses pagination/rawCount/hasMore so operators do not mistake a single page for the full repository. --search uses GitHub Search Issues API with repo/type/state qualifiers for low-friction dedupe lookup before creating a new issue. Supported --json fields are number,title,state,closed,closedAt,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 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.",
|
||||
"--raw and --full are explicit full-disclosure aliases for gh issue list/read/view/update/edit/patch 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-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.",
|
||||
@@ -6837,7 +7214,9 @@ export function ghHelp(): unknown {
|
||||
"issue edit is a compatibility alias for issue update --mode replace.",
|
||||
"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 patch reads the current GitHub issue body, applies a Codex apply_patch envelope against virtual file issue.md from --body-patch-stdin or --body-patch-file <file|->, then runs the same issue body guard before PATCH. It returns old/new bodySha, updatedAt, patch summary, and bounded previews; context mismatch fails with redacted diagnostics and no GitHub write.",
|
||||
"issue comment create/update/edit accept --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. Use comment update/edit to correct existing wording in place; delete remains for intentional removal.",
|
||||
"issue comment patch reads the current issue comment by commentId, applies a Codex apply_patch envelope against virtual file comment.md, then PATCHes only that comment. It returns comment id, old/new bodySha, updatedAt, patch summary, and redacted mismatch diagnostics without echoing the full comment body.",
|
||||
"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, 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.",
|
||||
@@ -6890,14 +7269,15 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
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, "--raw") || optionWasProvided(args, "--full")) && !((top === "issue" && (isIssueReadCommand(sub) || sub === "list" || sub === "update" || sub === "edit")) || top === "preflight" || (top === "pr" && (isPrReadCommand(sub) || sub === "list" || sub === "preflight" || sub === "closeout")))) {
|
||||
if ((optionWasProvided(args, "--raw") || optionWasProvided(args, "--full")) && !((top === "issue" && (isIssueReadCommand(sub) || sub === "list" || sub === "update" || sub === "edit" || sub === "patch")) || top === "preflight" || (top === "pr" && (isPrReadCommand(sub) || sub === "list" || sub === "preflight" || sub === "closeout")))) {
|
||||
const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh";
|
||||
return validationError(command, options.repo, "--raw and --full are explicit full-disclosure aliases only for gh issue list/read/view/update/edit, gh pr list/read/view, and gh pr preflight/closeout.", {
|
||||
return validationError(command, options.repo, "--raw and --full are explicit full-disclosure aliases only for gh issue list/read/view/update/edit/patch, gh pr list/read/view, and gh pr preflight/closeout.", {
|
||||
supportedCommands: [
|
||||
"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",
|
||||
"bun scripts/cli.ts gh issue update <number> --repo owner/name --body-stdin --full <<'EOF'\n<reviewed body>\nEOF",
|
||||
"bun scripts/cli.ts gh issue patch <number> --repo owner/name --body-patch-stdin --full <<'PATCH'\n*** Begin Patch\n*** Update File: issue.md\n@@\n-old\n+new\n*** End Patch\nPATCH",
|
||||
"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")}`,
|
||||
@@ -6931,9 +7311,24 @@ 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, "--mode is only supported by gh issue update/edit and gh pr update/edit");
|
||||
}
|
||||
if ((options.allowShortBody || options.expectUpdatedAt !== undefined || options.expectBodySha !== undefined || optionWasProvided(args, "--body-profile")) && !(top === "issue" && (sub === "edit" || sub === "update" || sub === "board-row" && ["update", "add", "move", "delete", "upsert"].includes(third ?? "")))) {
|
||||
const issueBodyGuardCommand = top === "issue" && (sub === "edit" || sub === "update" || sub === "patch" || sub === "board-row" && ["update", "add", "move", "delete", "upsert"].includes(third ?? ""));
|
||||
const issueCommentPatchCommand = top === "issue" && sub === "comment" && third === "patch";
|
||||
if ((options.allowShortBody || optionWasProvided(args, "--body-profile")) && !issueBodyGuardCommand) {
|
||||
const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh";
|
||||
return validationError(command, options.repo, "--allow-short-body, --expect-updated-at, --expect-body-sha, and --body-profile are only supported by gh issue update/edit and gh issue board-row update/add/move/delete/upsert");
|
||||
return validationError(command, options.repo, "--allow-short-body and --body-profile are only supported by gh issue update/edit/patch and gh issue board-row update/add/move/delete/upsert");
|
||||
}
|
||||
if ((options.expectUpdatedAt !== undefined || options.expectBodySha !== undefined) && !(issueBodyGuardCommand || issueCommentPatchCommand)) {
|
||||
const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh";
|
||||
return validationError(command, options.repo, "--expect-updated-at and --expect-body-sha are only supported by gh issue update/edit/patch, gh issue comment patch, and gh issue board-row update/add/move/delete/upsert");
|
||||
}
|
||||
if ((optionWasProvided(args, "--body-patch-file") || optionWasProvided(args, "--body-patch-stdin")) && !(top === "issue" && (sub === "patch" || sub === "comment" && third === "patch"))) {
|
||||
const command = [top, sub, third].filter((value): value is string => value !== undefined).join(" ") || "gh";
|
||||
return validationError(command, options.repo, "--body-patch-file and --body-patch-stdin are only supported by gh issue patch and gh issue comment patch", {
|
||||
supportedCommands: [
|
||||
"bun scripts/cli.ts gh issue patch <number> --repo owner/name --body-patch-stdin",
|
||||
"bun scripts/cli.ts gh issue comment patch <commentId> --repo owner/name --body-patch-stdin",
|
||||
],
|
||||
});
|
||||
}
|
||||
if ((optionWasProvided(args, "--board-issue") || optionWasProvided(args, "--known-meta-issue") || optionWasProvided(args, "--ignore-issue")) && !(top === "issue" && (sub === "board-audit" || sub === "board-row"))) {
|
||||
const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh";
|
||||
@@ -7027,6 +7422,17 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, "issue comment delete", { present: false, source: null, ghFallbackAttempted: true });
|
||||
return withNumberOptionHint(commentDelete(resolved.repo, token, "issue", commentId, false), resolved);
|
||||
}
|
||||
if (sub === "comment" && third === "patch") {
|
||||
const commandName = "issue comment patch";
|
||||
const resolved = resolvePositionalNumberReference("issue", args, 3, commandName, options);
|
||||
if (isGitHubCommandResult(resolved)) return resolved;
|
||||
const commentId = resolved.number;
|
||||
if (typeof commentId !== "number") return commentId;
|
||||
const { token, probe } = resolveToken(true);
|
||||
const missing = authRequired(resolved.repo, commandName, probe);
|
||||
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, commandName, { present: false, source: null, ghFallbackAttempted: true });
|
||||
return withNumberOptionHint(commentPatch(resolved.repo, token, "issue", commentId, { ...options, repo: resolved.repo }, commandName), resolved);
|
||||
}
|
||||
if (sub === "comment" && (third === "update" || third === "edit")) {
|
||||
const commandName = `issue comment ${third}`;
|
||||
const resolved = resolvePositionalNumberReference("issue", args, 3, commandName, options);
|
||||
@@ -7086,6 +7492,14 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
if (action === "move") return withNumberOptionHint(issueBoardRowMove(resolvedBoardRow.repo, token, issueNumber, boardRowOptions), resolvedBoardRow);
|
||||
return withNumberOptionHint(issueBoardRowDelete(resolvedBoardRow.repo, token, issueNumber, boardRowOptions), resolvedBoardRow);
|
||||
}
|
||||
if (sub === "patch") {
|
||||
const resolved = resolvePositionalIssueReference(args, 2, "issue patch", options);
|
||||
if (isGitHubCommandResult(resolved)) return resolved;
|
||||
const { token, probe } = resolveToken(true);
|
||||
const missing = authRequired(resolved.repo, "issue patch", probe);
|
||||
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, "issue patch", { present: false, source: null, ghFallbackAttempted: true });
|
||||
return withNumberOptionHint(issuePatch(resolved.repo, token, resolved.number, { ...options, repo: resolved.repo }), resolved);
|
||||
}
|
||||
if (options.dryRun) {
|
||||
if (sub === "create") return issueCreate(options.repo, "", options);
|
||||
if (sub === "edit") {
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ export function rootHelp(): unknown {
|
||||
{ command: "dev-env validate|prewarm-images", description: "Validate D601 unidesk-dev guardrails or prewarm dev foundation images into native k3s containerd through a bounded async job." },
|
||||
{ command: "artifact-registry plan|render|status|health|install|deploy-backend-core|deploy-service", description: "Manage the D601 host-managed CNCF Distribution registry and run pull-only artifact CD for supported services, including D601 direct, k3s-managed, and code-queue dev-only consumers." },
|
||||
{ command: "auth-broker contract|health --dry-run|credential-request --dry-run|pr-preflight --dry-run", description: "Inspect the P0 Rust auth broker and CLI adapter contract without reading token values, writing GitHub, or starting services." },
|
||||
{ command: "gh preflight|auth|issue|pr", description: "Run safe GitHub issue and PR CRUD/lifecycle operations through REST with body-file update replace/append, comment delete, token diagnostics, PR closeout preflight, hard delete unsupported, and guarded PR merge." },
|
||||
{ command: "gh preflight|auth|issue|pr", description: "Run safe GitHub issue and PR CRUD/lifecycle operations through REST with body-file update replace/append, issue/comment apply_patch body patching, comment delete, token diagnostics, PR closeout preflight, hard delete unsupported, and guarded PR merge." },
|
||||
{ command: "commander contract|plan --dry-run|smoke --dry-run|approval request --dry-run", description: "Host Codex commander skeleton contract, no-daemon smoke plan, and dry-run approval preview without live bridges or message sends." },
|
||||
{ command: "hwlab nodes control-plane|git-mirror|secret --node <node> --lane <lane>", description: "Manage HWLAB node/lane runtime prerequisites, including D601 YAML-declared infra/tools-image/Argo bootstrap and G14 v0.3+ runtime lanes, with the node identity passed as data." },
|
||||
{ command: "hwlab g14 monitor-prs | hwlab g14 control-plane status|apply|trigger-current|runtime-migration|cleanup-runs|cleanup-released-pvs | hwlab g14 git-mirror status|apply|sync|flush | hwlab g14 tools-image status|build", description: "Start the legacy G14 PR monitor, run bounded v0.2 Tekton/Argo control-plane, manual PipelineRun trigger, runtime migration, CI workspace retention, manual devops-infra git mirror/relay maintenance, or fixed HWLAB CI tools image actions; long confirmed trigger/sync/flush actions return async jobs by default." },
|
||||
|
||||
Reference in New Issue
Block a user