fix(cli): compact gh view and web-probe failures

This commit is contained in:
Codex
2026-06-20 10:11:24 +00:00
parent a2ec41cc63
commit 6d0054c78c
3 changed files with 63 additions and 16 deletions
+43 -13
View File
@@ -2452,9 +2452,10 @@ function issueWriteDisclosure(options: GitHubOptions, repo: string, issueNumber:
};
}
function issueSummary(issue: GitHubIssue, options: { includeBody?: boolean; previewLineCount?: number } = {}): Record<string, unknown> {
function issueSummary(issue: GitHubIssue, options: { includeBody?: boolean; includePreview?: boolean; previewLineCount?: number } = {}): Record<string, unknown> {
const body = issue.body ?? "";
const includeBody = options.includeBody ?? true;
const includePreview = options.includePreview ?? true;
const summary: Record<string, unknown> = {
id: issue.id,
number: issue.number,
@@ -2475,8 +2476,10 @@ function issueSummary(issue: GitHubIssue, options: { includeBody?: boolean; prev
} else {
summary.bodyOmitted = true;
summary.fullBodyIncluded = false;
summary.bodyPreview = preview(body);
summary.bodyPreviewLines = previewLines(body, options.previewLineCount ?? 8);
if (includePreview) {
summary.bodyPreview = preview(body);
summary.bodyPreviewLines = previewLines(body, options.previewLineCount ?? 8);
}
}
return summary;
}
@@ -4705,16 +4708,18 @@ function prStateDetail(pr: GitHubPullRequest): "open" | "closed" | "merged" {
return pr.state === "closed" ? "closed" : "open";
}
function prSummary(pr: GitHubPullRequest): Record<string, unknown> {
function prSummary(pr: GitHubPullRequest, options: { includeBody?: boolean; includePreview?: boolean; previewLineCount?: number } = {}): Record<string, unknown> {
const stateDetail = prStateDetail(pr);
const closedAt = pr.closed_at ?? pr.merged_at ?? null;
const mergedAt = pr.merged_at ?? null;
const mergeCommitSha = pr.merge_commit_sha ?? null;
return {
const body = pr.body ?? "";
const includeBody = options.includeBody ?? true;
const includePreview = options.includePreview ?? true;
const summary: Record<string, unknown> = {
id: pr.id,
number: pr.number,
title: pr.title,
body: pr.body ?? "",
state: pr.state,
stateDetail,
draft: pr.draft ?? false,
@@ -4731,7 +4736,20 @@ function prSummary(pr: GitHubPullRequest): Record<string, unknown> {
merged: stateDetail === "merged",
mergedAt,
mergeCommit: stateDetail === "merged" && mergeCommitSha !== null ? { oid: mergeCommitSha } : null,
bodyChars: body.length,
bodySha: bodySha(body),
};
if (includeBody) {
summary.body = body;
} else {
summary.bodyOmitted = true;
summary.fullBodyIncluded = false;
if (includePreview) {
summary.bodyPreview = preview(body);
summary.bodyPreviewLines = previewLines(body, options.previewLineCount ?? 8);
}
}
return summary;
}
function numberOrNull(value: number | undefined): number | null {
@@ -5694,21 +5712,24 @@ async function issueRead(repo: string, token: string, issueNumber: number, jsonF
const issue = await getIssue(token, repo, issueNumber);
if (isGitHubError(issue)) return commandError(commandName, repo, issue, { issueNumber });
const needsComments = jsonFields === undefined || jsonFields.includes("comments");
const includeBody = jsonFields === undefined || jsonFields.includes("body");
const comments = needsComments ? await listIssueComments(token, repo, issueNumber) : null;
if (isGitHubError(comments)) return commandError(commandName, repo, comments, { issueNumber, issue: issueSummary(issue) });
if (isGitHubError(comments)) return commandError(commandName, repo, comments, { issueNumber, issue: issueSummary(issue, { includeBody, includePreview: false }) });
return {
ok: true,
command: commandName,
repo,
...(disclosure === null ? {} : { disclosure }),
issue: issueSummary(issue),
issue: issueSummary(issue, { includeBody, includePreview: false }),
codeQueueBoardHint: codeQueueBoardCommanderBriefHint(issueNumber, issue.body ?? ""),
...(comments === null ? {} : { comments: comments.map(commentSummary) }),
...(jsonFields === undefined ? {} : {
jsonFields,
json: selectedIssueJson(issue, comments, jsonFields),
compatibility: {
legacyJsonBodyPath: ".data.issue.body",
legacyJsonBodyPath: includeBody ? ".data.issue.body" : null,
bodyOmitted: !includeBody,
readCommands: includeBody ? null : issueBodyReadCommands(repo, issueNumber),
},
}),
};
@@ -7626,10 +7647,12 @@ async function prRead(repo: string, token: string, number: number, jsonFields: P
const { owner, name } = repoParts(repo);
const pr = await githubRequest<GitHubPullRequest>(token, "GET", `/repos/${owner}/${name}/pulls/${number}`);
if (isGitHubError(pr)) return commandError(commandName, repo, pr, { number });
const summary = prSummary(pr);
const fullSummary = prSummary(pr);
const includeBody = jsonFields === undefined || jsonFields.includes("body");
const summary = includeBody ? fullSummary : prSummary(pr, { includeBody: false, includePreview: false });
const metadata = needsPrGraphqlMetadata(jsonFields) ? await prGraphqlMetadata(repo, token, number) : null;
if (isGitHubError(metadata)) return commandError(commandName, repo, metadata, { number, phase: "fetch-pr-closeout-metadata", pullRequest: summary, requestedJsonFields: jsonFields ?? [], closeoutMetadata: prCloseoutMetadataError(metadata) });
const selectionSummary = metadata === null ? summary : { ...summary, ...prMetadataSummary(metadata) };
const selectionSummary = metadata === null ? fullSummary : { ...fullSummary, ...prMetadataSummary(metadata) };
return {
ok: true,
command: commandName,
@@ -7637,7 +7660,14 @@ async function prRead(repo: string, token: string, number: number, jsonFields: P
...(disclosure === null ? {} : { disclosure }),
pullRequest: summary,
...(metadata === null ? {} : { closeoutMetadata: prCloseoutMetadata(metadata) }),
...(jsonFields === undefined ? {} : { jsonFields, json: selectedPrJson(selectionSummary, jsonFields) }),
...(jsonFields === undefined ? {} : {
jsonFields,
json: selectedPrJson(selectionSummary, jsonFields),
compatibility: {
legacyJsonBodyPath: includeBody ? ".data.pullRequest.body" : null,
bodyOmitted: !includeBody,
},
}),
};
}
@@ -7708,7 +7738,7 @@ export function ghHelp(): unknown {
"issue list and pr list accept a single positional owner/repo as a compatibility alias for --repo owner/name. The positional repo and --repo must match if both are supplied; non-repo positionals fail structurally instead of falling back to the default repo.",
"issue list defaults to --state open and bounded --limit 30; 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. --title-prefix filters the bounded listed issues locally by exact title startsWith, useful for [FEEDBACK] dedupe, and reports titleFilter input/output counts. 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.",
"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; full body is included only when requested with --json body, --full, or --raw, and unsupported fields fail structurally.",
"issue attachment list/download scan issue body and comments for GitHub user attachment URLs (`https://github.com/user-attachments/assets/...`). list is read-only and returns bounded attachment metadata. download writes the selected attachment to --output or /tmp/unidesk-gh-attachments, returns bytes/SHA-256/content-type/path, redacts redirected signed URL query parameters, and never prints binary bytes.",
"--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.",
@@ -44,7 +44,7 @@ try {
if (fn === null) throw new Error("custom script must export default, run, or probe function");
const scriptResult = await fn(scriptHelpers());
const safeResult = sanitize(scriptResult);
const scriptOk = !(safeResult && typeof safeResult === "object" && safeResult.ok === false);
const scriptOk = scriptResultOk(safeResult);
const failure = scriptOk ? null : classifiedProbeError(assertionResultError(safeResult));
const lastScreenshot = scriptOk ? null : await captureFailureScreenshot("failure.png");
const lastUrl = currentPageUrl();
@@ -1399,15 +1399,31 @@ function stableProbeError(code, message, readiness) {
}
function assertionResultError(value) {
const message = extractFailureMessage(value) || "script returned ok:false";
const message = extractFailureMessage(value) || scriptFailureSignalMessage(value) || "script returned a failure result";
const error = new Error(message);
error.code = "assertion-failed";
return error;
}
function scriptResultOk(value) {
if (!value || typeof value !== "object") return true;
if (value.ok === false) return false;
if (value.pass === false) return false;
if (value.success === false) return false;
return true;
}
function scriptFailureSignalMessage(value) {
if (!value || typeof value !== "object") return null;
if (value.pass === false) return "script returned pass:false";
if (value.success === false) return "script returned success:false";
if (value.ok === false) return "script returned ok:false";
return null;
}
function extractFailureMessage(value) {
if (!value || typeof value !== "object") return null;
for (const key of ["errorMessage", "message", "error", "reason", "summary"]) {
for (const key of ["failedCondition", "errorMessage", "message", "error", "reason", "summary"]) {
const nested = value[key];
if (typeof nested === "string" && nested.length > 0) return nested;
}