fix(cli): compact gh view and web-probe failures
This commit is contained in:
+43
-13
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user