fix: 修复 CLI repo 与状态别名解析
This commit is contained in:
+65
-12
@@ -59,6 +59,30 @@ const defaultSteerRetryAttempts = 2;
|
||||
const maxSteerRetryAttempts = 3;
|
||||
const defaultSteerRetryDelayMs = 750;
|
||||
const maxSteerRetryDelayMs = 5_000;
|
||||
const codexTaskStatuses = ["queued", "running", "judging", "retry_wait", "succeeded", "failed", "canceled"] as const;
|
||||
const codexTerminalTaskStatuses = ["succeeded", "failed", "canceled"] as const;
|
||||
const codexTaskStatusAliases: Record<string, string> = {
|
||||
completed: "succeeded",
|
||||
complete: "succeeded",
|
||||
success: "succeeded",
|
||||
successful: "succeeded",
|
||||
succeeded: "succeeded",
|
||||
cancelled: "canceled",
|
||||
canceled: "canceled",
|
||||
failed: "failed",
|
||||
failure: "failed",
|
||||
errored: "failed",
|
||||
error: "failed",
|
||||
running: "running",
|
||||
active: "running",
|
||||
judging: "judging",
|
||||
judge: "judging",
|
||||
queued: "queued",
|
||||
pending: "queued",
|
||||
retrying: "retry_wait",
|
||||
retry_wait: "retry_wait",
|
||||
"retry-wait": "retry_wait",
|
||||
};
|
||||
|
||||
interface CodexTaskOptions {
|
||||
detail: boolean;
|
||||
@@ -1176,6 +1200,45 @@ function assertKnownOptions(args: string[], spec: { flags?: string[]; valueOptio
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeCodexStatusFilter(raw: string, allowedValues: readonly string[], command: string): string[] {
|
||||
const requested = raw.split(/[,\s]+/u).map((item) => item.trim()).filter(Boolean);
|
||||
if (requested.length === 0) throw new Error(`${command} --status requires at least one status value`);
|
||||
const allowed = new Set(allowedValues);
|
||||
const normalized: string[] = [];
|
||||
const unsupported: string[] = [];
|
||||
for (const value of requested) {
|
||||
const key = value.toLowerCase();
|
||||
const canonical = codexTaskStatusAliases[key] ?? key;
|
||||
if (!allowed.has(canonical)) {
|
||||
unsupported.push(value);
|
||||
continue;
|
||||
}
|
||||
if (!normalized.includes(canonical)) normalized.push(canonical);
|
||||
}
|
||||
if (unsupported.length > 0) {
|
||||
const aliasHints = Object.entries(codexTaskStatusAliases)
|
||||
.filter(([alias, canonical]) => alias !== canonical && allowed.has(canonical))
|
||||
.map(([alias, canonical]) => `${alias}->${canonical}`);
|
||||
const detail = {
|
||||
ok: false,
|
||||
degradedReason: "validation-failed",
|
||||
command,
|
||||
option: "--status",
|
||||
unsupported,
|
||||
supported: allowedValues,
|
||||
aliases: aliasHints,
|
||||
examples: [
|
||||
`bun scripts/cli.ts ${command} --status ${allowedValues.join(",")}`,
|
||||
command === "codex tasks"
|
||||
? "bun scripts/cli.ts codex tasks --status completed,cancelled --view commander"
|
||||
: "bun scripts/cli.ts codex unread --status completed,cancelled",
|
||||
],
|
||||
};
|
||||
throw new Error(`${command} unsupported --status value: ${unsupported.join(", ")}; ${JSON.stringify(detail)}`);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function textView(text: string, full: boolean, maxChars: number): Record<string, unknown> {
|
||||
const truncated = !full && text.length > maxChars;
|
||||
return {
|
||||
@@ -2674,12 +2737,7 @@ function parseTasksOptions(args: string[]): CodexTasksOptions {
|
||||
const statusRaw = optionValue(args, ["--status"]);
|
||||
const statusFilter = statusRaw === undefined
|
||||
? null
|
||||
: statusRaw.split(/[,\s]+/u).map((item) => item.trim()).filter(Boolean);
|
||||
if (statusFilter !== null) {
|
||||
const supported = new Set(["queued", "running", "judging", "retry_wait", "succeeded", "failed", "canceled"]);
|
||||
const unsupported = statusFilter.filter((status) => !supported.has(status));
|
||||
if (unsupported.length > 0) throw new Error(`unsupported --status value: ${unsupported.join(", ")}; supported: ${Array.from(supported).join(", ")}`);
|
||||
}
|
||||
: normalizeCodexStatusFilter(statusRaw, codexTaskStatuses, "codex tasks");
|
||||
const requestedLimit = positiveIntegerOption(args, ["--limit"], defaultTasksLimit);
|
||||
return {
|
||||
queueId: optionValue(args, ["--queue", "--queue-id"]),
|
||||
@@ -2727,12 +2785,7 @@ function parseUnreadOptions(args: string[]): CodexUnreadOptions {
|
||||
const statusRaw = optionValue(optionArgs, ["--status"]);
|
||||
const statusFilter = statusRaw === undefined
|
||||
? null
|
||||
: statusRaw.split(/[,\s]+/u).map((item) => item.trim()).filter(Boolean);
|
||||
if (statusFilter !== null) {
|
||||
const supported = new Set(["succeeded", "failed", "canceled"]);
|
||||
const unsupported = statusFilter.filter((status) => !supported.has(status));
|
||||
if (unsupported.length > 0) throw new Error(`unsupported --status value for codex unread: ${unsupported.join(", ")}; supported terminal statuses: ${Array.from(supported).join(", ")}`);
|
||||
}
|
||||
: normalizeCodexStatusFilter(statusRaw, codexTerminalTaskStatuses, "codex unread");
|
||||
const requestedLimit = positiveIntegerOption(optionArgs, ["--limit"], defaultTasksLimit);
|
||||
const action: CodexUnreadAction = subcommand === "mark-read" || hasFlag(optionArgs, "--mark-read") ? "mark-read" : "summary";
|
||||
const explicitDryRun = hasFlag(optionArgs, "--dry-run");
|
||||
|
||||
+106
-10
@@ -28,6 +28,8 @@ const ISSUE_LIST_STATES = ["open", "closed", "all"] as const;
|
||||
const BODY_UPDATE_MODES = ["replace", "append"] as const;
|
||||
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", "--expect-updated-at", "--expect-body-sha", "--body-profile", "--label", "--field", "--value", "--section", "--to", "--status", "--row-file", "--category", "--branch", "--tasks", "--summary", "--focus", "--validation", "--progress", "--number"]);
|
||||
const GH_FLAG_OPTIONS = new Set(["--dry-run", "--draft", "--notify-claudeqq-brief-diff", "--allow-short-body", "--raw", "--full", "--stat"]);
|
||||
const MIN_SAFE_BODY_SCAN_CHARS = MIN_SAFE_ISSUE_BODY_CHARS;
|
||||
const ISSUE_SCAN_MAX_FINDINGS = 60;
|
||||
const ISSUE_BODY_PROFILES = {
|
||||
@@ -678,13 +680,11 @@ function parseBoardRowUpsertValues(args: string[]): BoardRowUpsertValues {
|
||||
}
|
||||
|
||||
function validateKnownOptions(args: string[]): void {
|
||||
const valueOptions = new Set(["--repo", "--limit", "--board-issue", "--known-meta-issue", "--ignore-issue", "--title", "--body-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"]);
|
||||
const flagOptions = new Set(["--dry-run", "--draft", "--notify-claudeqq-brief-diff", "--allow-short-body", "--raw", "--full", "--stat"]);
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (!arg.startsWith("--")) continue;
|
||||
if (flagOptions.has(arg)) continue;
|
||||
if (valueOptions.has(arg)) {
|
||||
if (GH_FLAG_OPTIONS.has(arg)) continue;
|
||||
if (GH_VALUE_OPTIONS.has(arg)) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
@@ -692,13 +692,52 @@ function validateKnownOptions(args: string[]): void {
|
||||
}
|
||||
}
|
||||
|
||||
function positionalArgs(args: string[]): string[] {
|
||||
const positionals: string[] = [];
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index] ?? "";
|
||||
if (arg.startsWith("--")) {
|
||||
if (GH_VALUE_OPTIONS.has(arg)) index += 1;
|
||||
continue;
|
||||
}
|
||||
positionals.push(arg);
|
||||
}
|
||||
return positionals;
|
||||
}
|
||||
|
||||
function isOwnerRepo(value: string): boolean {
|
||||
return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(value);
|
||||
}
|
||||
|
||||
function resolveRepoOption(args: string[]): string {
|
||||
const [top, sub] = args;
|
||||
const explicitRepo = optionValue(args, "--repo");
|
||||
if ((top === "issue" || top === "pr") && sub === "list") {
|
||||
const positionals = positionalArgs(args.slice(2));
|
||||
if (positionals.length > 1) {
|
||||
throw new Error(`gh ${top} list accepts at most one positional owner/repo; use --repo owner/name`);
|
||||
}
|
||||
const positionalRepo = positionals[0];
|
||||
if (positionalRepo !== undefined) {
|
||||
if (!isOwnerRepo(positionalRepo)) {
|
||||
throw new Error(`gh ${top} list positional argument must be owner/repo; use --repo owner/name`);
|
||||
}
|
||||
if (explicitRepo !== undefined && explicitRepo !== positionalRepo) {
|
||||
throw new Error(`gh ${top} list received positional repo ${positionalRepo} and --repo ${explicitRepo}; use one repo target, for example --repo ${positionalRepo}`);
|
||||
}
|
||||
return positionalRepo;
|
||||
}
|
||||
}
|
||||
return explicitRepo ?? DEFAULT_REPO;
|
||||
}
|
||||
|
||||
function parseOptions(args: string[]): GitHubOptions {
|
||||
validateKnownOptions(args);
|
||||
const [top, sub] = args;
|
||||
const requestedJsonFields = commaListOption(args, "--json");
|
||||
const limitMax = top === "pr" && (sub === "files" || sub === "diff") ? MAX_PR_FILES_LIMIT : 100;
|
||||
return {
|
||||
repo: optionValue(args, "--repo") ?? DEFAULT_REPO,
|
||||
repo: resolveRepoOption(args),
|
||||
dryRun: hasFlag(args, "--dry-run"),
|
||||
raw: hasFlag(args, "--raw"),
|
||||
full: hasFlag(args, "--full"),
|
||||
@@ -774,10 +813,32 @@ function readViewSupportedJsonFields(kind: "issue" | "pr"): string {
|
||||
: "body,title,state,stateDetail,closed,closedAt,merged,mergedAt,mergeCommit,head,base,draft,headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup";
|
||||
}
|
||||
|
||||
function resolveReadViewNumberReference(kind: "issue" | "pr", sub: "read" | "view", raw: string | undefined, options: GitHubOptions, args: string[]): GitHubResolvedNumberReference | GitHubCommandResult {
|
||||
function resolveReadViewNumberReference(kind: "issue" | "pr", sub: "read" | "view", _raw: string | undefined, options: GitHubOptions, args: string[]): GitHubResolvedNumberReference | GitHubCommandResult {
|
||||
const command = `${kind} ${sub}`;
|
||||
const targets = positionalArgs(args.slice(2));
|
||||
if (targets.length > 1) {
|
||||
return validationError(command, options.repo, `${command} accepts one number or owner/repo#number target; use --repo owner/name and --number N when passing options first`, {
|
||||
supportedCommands: [
|
||||
`bun scripts/cli.ts gh ${kind} read <number> --repo owner/name --json ${readViewSupportedJsonFields(kind)}`,
|
||||
`bun scripts/cli.ts gh ${kind} read owner/name#<number> --raw`,
|
||||
],
|
||||
});
|
||||
}
|
||||
const raw = targets[0];
|
||||
const numberAliasRaw = kind === "pr" ? optionValue(args, "--number") : undefined;
|
||||
const shorthand = parseOwnerRepoNumberShorthand(raw);
|
||||
if (shorthand !== null) {
|
||||
if (numberAliasRaw !== undefined) {
|
||||
const aliasNumber = parseNumberForCommand(shorthand.repo, numberAliasRaw, command);
|
||||
if (typeof aliasNumber !== "number") return aliasNumber;
|
||||
if (aliasNumber !== shorthand.number) {
|
||||
return validationError(command, shorthand.repo, `${command} target ${shorthand.input} conflicts with --number ${aliasNumber}; use one PR number target.`, {
|
||||
shorthand,
|
||||
numberAlias: aliasNumber,
|
||||
supportedCommands: readViewSupportedCommands(kind, shorthand.repo, shorthand.number),
|
||||
});
|
||||
}
|
||||
}
|
||||
const explicitRepo = optionValue(args, "--repo");
|
||||
if (explicitRepo !== undefined && explicitRepo !== shorthand.repo) {
|
||||
const message = `${command} target ${shorthand.input} resolves to repo ${shorthand.repo}, but --repo ${explicitRepo} was also provided. Use either the shorthand or a matching --repo, not both.`;
|
||||
@@ -790,6 +851,31 @@ function resolveReadViewNumberReference(kind: "issue" | "pr", sub: "read" | "vie
|
||||
}
|
||||
return { repo: shorthand.repo, number: shorthand.number, shorthand };
|
||||
}
|
||||
if (numberAliasRaw !== undefined) {
|
||||
const aliasNumber = parseNumberForCommand(options.repo, numberAliasRaw, command);
|
||||
if (typeof aliasNumber !== "number") return {
|
||||
...aliasNumber,
|
||||
supportedCommands: [
|
||||
`bun scripts/cli.ts gh pr read --repo owner/name --number <number> --json ${readViewSupportedJsonFields("pr")}`,
|
||||
"bun scripts/cli.ts gh pr read owner/name#<number> --raw",
|
||||
],
|
||||
};
|
||||
if (raw !== undefined) {
|
||||
const parsedRaw = parseNumberForCommand(options.repo, raw, command);
|
||||
if (typeof parsedRaw !== "number") return parsedRaw;
|
||||
if (parsedRaw !== aliasNumber) {
|
||||
return validationError(command, options.repo, `${command} positional number ${parsedRaw} conflicts with --number ${aliasNumber}; use one PR number target.`, {
|
||||
positionalNumber: parsedRaw,
|
||||
numberAlias: aliasNumber,
|
||||
supportedCommands: [
|
||||
`bun scripts/cli.ts gh pr read ${aliasNumber} --repo ${options.repo} --json ${readViewSupportedJsonFields("pr")}`,
|
||||
`bun scripts/cli.ts gh pr read --repo ${options.repo} --number ${aliasNumber} --full`,
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
return { repo: options.repo, number: aliasNumber };
|
||||
}
|
||||
const parsed = parseNumberForCommand(options.repo, raw, command);
|
||||
if (typeof parsed !== "number") {
|
||||
return {
|
||||
@@ -5521,7 +5607,7 @@ export function ghHelp(): unknown {
|
||||
output: "json",
|
||||
usage: [
|
||||
"bun scripts/cli.ts gh auth status [--repo owner/name]",
|
||||
"bun scripts/cli.ts gh issue list [--state open|closed|all] [--limit N] [--repo owner/name] [--json number,title,state,url,updatedAt,createdAt,author,labels]",
|
||||
"bun scripts/cli.ts gh issue list [owner/repo] [--state open|closed|all] [--limit N] [--repo owner/name] [--json number,title,state,url,updatedAt,createdAt,author,labels]",
|
||||
"bun scripts/cli.ts gh issue read <number|owner/repo#number> [--repo owner/name] [--json body,title,state,comments] [--raw|--full]",
|
||||
"bun scripts/cli.ts gh issue view <number|owner/repo#number> [--repo owner/name] [--raw|--full] [compatibility alias for issue read]",
|
||||
"bun scripts/cli.ts gh issue create --title <title> --body-file <file> [--label label[,label...]]... [--repo owner/name] [--dry-run]",
|
||||
@@ -5543,10 +5629,10 @@ export function ghHelp(): unknown {
|
||||
"bun scripts/cli.ts gh issue board-row move <issueNumber> [--repo owner/name] --board-issue 20 --to open|closed [--status OPEN|CLOSED] [--dry-run] [--expect-body-sha sha256]",
|
||||
"bun scripts/cli.ts gh issue board-row delete <issueNumber> [--repo owner/name] --board-issue 20 [--dry-run] [--expect-body-sha sha256]",
|
||||
"bun scripts/cli.ts gh preflight <prNumber> [--repo owner/name] [--full|--raw] [compatibility alias for gh pr preflight]",
|
||||
"bun scripts/cli.ts gh pr list [--repo owner/name] [--state open|closed|all] [--limit N] [--json number,title,state,url,updatedAt,createdAt,author,head,base,draft]",
|
||||
"bun scripts/cli.ts gh pr list [owner/repo] [--repo owner/name] [--state open|closed|all] [--limit N] [--json number,title,state,url,updatedAt,createdAt,author,head,base,draft]",
|
||||
"bun scripts/cli.ts gh pr files <number> [--repo owner/name] [--limit N]",
|
||||
"bun scripts/cli.ts gh pr diff <number> --stat [--repo owner/name] [--limit N] [compatibility alias for pr files; no raw diff]",
|
||||
"bun scripts/cli.ts gh pr read <number|owner/repo#number> [--repo owner/name] [--json body,title,state,stateDetail,closed,closedAt,merged,mergedAt,mergeCommit,head,base,draft,headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup] [--raw|--full]",
|
||||
"bun scripts/cli.ts gh pr read <number|owner/repo#number> [--repo owner/name] [--number N] [--json body,title,state,stateDetail,closed,closedAt,merged,mergedAt,mergeCommit,head,base,draft,headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup] [--raw|--full]",
|
||||
"bun scripts/cli.ts gh pr view <number|owner/repo#number> [--repo owner/name] [--raw|--full] [compatibility alias for pr read]",
|
||||
"bun scripts/cli.ts gh pr preflight <number> [--repo owner/name] [--full|--raw]",
|
||||
"bun scripts/cli.ts gh pr closeout <number> [--repo owner/name] [--full|--raw] [compatibility alias for pr preflight]",
|
||||
@@ -5562,6 +5648,7 @@ export function ghHelp(): unknown {
|
||||
notes: [
|
||||
"Issue and PR create/read/update/comment/close/reopen use GitHub REST and do not require the gh binary when GH_TOKEN or GITHUB_TOKEN is present.",
|
||||
"Token values are never printed; auth status reports only token source and presence.",
|
||||
"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; supported --json fields are number,title,state,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 read is the canonical read path; view remains a compatibility alias. Read/view accept owner/repo#number shorthand and derive --repo unless an explicit conflicting --repo is supplied, which fails structurally with suggested commands. Read supports legacy --json field selection such as --json body and still exposes .data.issue.body for compatibility; unsupported fields fail structurally.",
|
||||
@@ -5586,7 +5673,7 @@ export function ghHelp(): unknown {
|
||||
"comment delete is supported because GitHub supports deleting issue comments; issue/pr hard delete is unsupported and close is the lifecycle alternative.",
|
||||
"PR files is the canonical compact changed-file/stat summary. It uses GitHub REST, returns bounded file rows, additions/deletions/changes when available, truncation metadata, and a next command for full details. Raw diff patches are not emitted by default; gh pr diff <number> --stat is a compatibility alias for the same JSON summary.",
|
||||
"PR edit/update PATCHes /repos/{owner}/{repo}/pulls/{number} through REST only, never GitHub Projects Classic GraphQL/projectCards, and returns low-noise JSON with repo, PR number, changedFields, url, and body size/SHA metadata instead of echoing the full body.",
|
||||
"PR read is the canonical read path; view remains a compatibility alias. PR read/view accept owner/repo#number shorthand and derive --repo unless an explicit conflicting --repo is supplied, which fails structurally with suggested commands. PR read/view supports REST closeout fields stateDetail, closed, closedAt, merged, mergedAt, mergeCommit, headRefName, and baseRefName; mergeable, mergeStateStatus, and statusCheckRollup are fetched through GitHub GraphQL only when requested or when --raw/--full requests full disclosure, and closeoutMetadata makes GraphQL errors plus UNKNOWN/null metadata explicit.",
|
||||
"PR read is the canonical read path; view remains a compatibility alias. PR read/view accept owner/repo#number shorthand and --number N as a compatibility alias for the positional PR number; shorthand derives --repo unless an explicit conflicting --repo is supplied, which fails structurally with suggested commands. PR read/view supports REST closeout fields stateDetail, closed, closedAt, merged, mergedAt, mergeCommit, headRefName, and baseRefName; mergeable, mergeStateStatus, and statusCheckRollup are fetched through GitHub GraphQL only when requested or when --raw/--full requests full disclosure, and closeoutMetadata makes GraphQL errors plus UNKNOWN/null metadata explicit.",
|
||||
"PR list does not fetch mergeability or statusCheckRollup; request those closeout fields with gh pr view <number> --json headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup.",
|
||||
"PR preflight is a low-noise read-only closeout helper. It combines redacted auth capability, PR branch/state metadata, mergeability, mergeStateStatus, compact status check counts, and the explicit UniDesk REST CLI no-merge policy. Use --full or --raw to include all fetched status contexts.",
|
||||
"PR create/edit/update/comment are safe-write operations with dry-run planning; merge is intentionally unsupported in this phase.",
|
||||
@@ -5644,6 +5731,15 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
],
|
||||
});
|
||||
}
|
||||
if (optionWasProvided(args, "--number") && !(top === "pr" && isPrReadCommand(sub))) {
|
||||
const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh";
|
||||
return validationError(command, options.repo, "--number is only supported by gh pr read/view; use gh pr read --repo owner/name --number N.", {
|
||||
supportedCommands: [
|
||||
"bun scripts/cli.ts gh pr read --repo owner/name --number <number> --full",
|
||||
"bun scripts/cli.ts gh pr read <number> --repo owner/name --json body,title,state,head,base",
|
||||
],
|
||||
});
|
||||
}
|
||||
if (optionWasProvided(args, "--mode") && !((top === "issue" && (sub === "update" || sub === "edit")) || (top === "pr" && (sub === "update" || sub === "edit")))) {
|
||||
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");
|
||||
|
||||
+40
-3
@@ -34,13 +34,50 @@ export function emitJson<T>(command: string, data: T, ok = true): void {
|
||||
}
|
||||
|
||||
export function emitError(command: string, error: unknown): void {
|
||||
const payload = error instanceof Error
|
||||
? { name: error.name, message: error.message, stack: error.stack ?? null }
|
||||
: { message: String(error) };
|
||||
const payload = normalizeErrorPayload(command, error);
|
||||
const envelope: JsonEnvelope<never> = { ok: false, command, error: payload };
|
||||
safeStdoutWrite(renderEnvelope(command, envelope));
|
||||
}
|
||||
|
||||
function normalizeErrorPayload(command: string, error: unknown): Record<string, unknown> {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const parsed = parseStructuredErrorMessage(command, message);
|
||||
if (parsed !== null) return parsed;
|
||||
if (shouldSuppressStack(command) || shouldSuppressStack(commandPrefixFromMessage(message))) {
|
||||
return { message };
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
const debug = process.env.UNIDESK_CLI_DEBUG === "1" || process.env.UNIDESK_CLI_FULL_ERROR === "1" || process.env.UNIDESK_CLI_RAW_ERROR === "1";
|
||||
return { name: error.name, message, stack: error.stack ?? null, ...(debug ? { debug: true } : {}) };
|
||||
}
|
||||
return { message };
|
||||
}
|
||||
|
||||
function parseStructuredErrorMessage(command: string, message: string): Record<string, unknown> | null {
|
||||
if (!shouldSuppressStack(command) && !shouldSuppressStack(commandPrefixFromMessage(message))) return null;
|
||||
const jsonStart = message.indexOf("{");
|
||||
if (jsonStart === -1) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(message.slice(jsonStart)) as unknown;
|
||||
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
||||
const record = parsed as Record<string, unknown>;
|
||||
return { message: message.slice(0, jsonStart).trim().replace(/[;:]$/u, ""), ...record };
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function commandPrefixFromMessage(message: string): string {
|
||||
return message.split(/\s+/u).slice(0, 2).join(" ");
|
||||
}
|
||||
|
||||
function shouldSuppressStack(prefix: string): boolean {
|
||||
if (process.env.UNIDESK_CLI_DEBUG === "1" || process.env.UNIDESK_CLI_FULL_ERROR === "1" || process.env.UNIDESK_CLI_RAW_ERROR === "1") return false;
|
||||
return prefix === "codex tasks" || prefix.startsWith("codex tasks ") || prefix === "codex unread" || prefix.startsWith("codex unread ");
|
||||
}
|
||||
|
||||
function renderEnvelope<T>(command: string, envelope: JsonEnvelope<T>): string {
|
||||
const fullText = `${JSON.stringify(envelope, null, 2)}\n`;
|
||||
if (!shouldDumpLargeOutput(command, fullText)) return fullText;
|
||||
|
||||
Reference in New Issue
Block a user