168 lines
5.9 KiB
TypeScript
168 lines
5.9 KiB
TypeScript
import * as os from 'os';
|
|
|
|
import { redactBridgeDiagnosticText } from './OpenCodeBridgeCommandClient';
|
|
|
|
import type { OpenCodeBridgeFailure } from './OpenCodeBridgeCommandContract';
|
|
import type { TeamProvisioningSupportDiagnostic } from '@shared/types/team';
|
|
|
|
const NO_OUTPUT_TITLE = 'OpenCode runtime check returned no output';
|
|
const NO_OUTPUT_SUMMARY = 'OpenCode readiness bridge exited without returning diagnostic JSON.';
|
|
|
|
export function isOpenCodeBridgeNoOutputDiagnostic(value: string | null | undefined): boolean {
|
|
const lower = value?.trim().toLowerCase() ?? '';
|
|
return (
|
|
lower.includes('opencode runtime check returned no output') ||
|
|
lower.includes('bridge stdout was empty') ||
|
|
lower.includes('opencode_bridge_contract_violation') ||
|
|
(lower.includes('opencode readiness bridge failed') && lower.includes('contract_violation'))
|
|
);
|
|
}
|
|
|
|
export function buildOpenCodeBridgeSupportDiagnostic(input: {
|
|
result: OpenCodeBridgeFailure;
|
|
projectPath: string;
|
|
selectedModel: string | null;
|
|
appVersion?: string | null;
|
|
}): TeamProvisioningSupportDiagnostic | null {
|
|
const event =
|
|
input.result.diagnostics.find((diagnostic) =>
|
|
isOpenCodeBridgeNoOutputDiagnostic(`${diagnostic.type}: ${diagnostic.message}`)
|
|
) ?? input.result.diagnostics[0];
|
|
const visibleError = `OpenCode readiness bridge failed: ${input.result.error.kind}: ${input.result.error.message}`;
|
|
const eventText = event ? `${event.type}: ${event.message}` : '';
|
|
if (
|
|
!isOpenCodeBridgeNoOutputDiagnostic(visibleError) &&
|
|
!isOpenCodeBridgeNoOutputDiagnostic(eventText)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
const details = {
|
|
...(event?.data ?? {}),
|
|
...(input.result.error.details ?? {}),
|
|
};
|
|
const createdAt = event?.createdAt ?? input.result.completedAt;
|
|
const copyText = buildOpenCodeBridgeSupportCopyText({
|
|
createdAt,
|
|
severity: event?.severity ?? (input.result.error.retryable ? 'warning' : 'error'),
|
|
visibleError,
|
|
details,
|
|
result: input.result,
|
|
projectPath: input.projectPath,
|
|
selectedModel: input.selectedModel,
|
|
appVersion: input.appVersion ?? null,
|
|
});
|
|
|
|
return {
|
|
id: event?.id ?? `opencode-bridge-support-${input.result.requestId}`,
|
|
providerId: 'opencode',
|
|
kind: 'opencode_bridge_no_output',
|
|
severity: event?.severity ?? (input.result.error.retryable ? 'warning' : 'error'),
|
|
title: NO_OUTPUT_TITLE,
|
|
summary: NO_OUTPUT_SUMMARY,
|
|
copyText,
|
|
createdAt,
|
|
};
|
|
}
|
|
|
|
function buildOpenCodeBridgeSupportCopyText(input: {
|
|
createdAt: string;
|
|
severity: 'info' | 'warning' | 'error';
|
|
visibleError: string;
|
|
details: Record<string, unknown>;
|
|
result: OpenCodeBridgeFailure;
|
|
projectPath: string;
|
|
selectedModel: string | null;
|
|
appVersion: string | null;
|
|
}): string {
|
|
const command = formatDiagnosticValue(input.details.command, input.result.command);
|
|
const requestId = formatDiagnosticValue(input.details.requestId, input.result.requestId);
|
|
const stderrPreview = formatPreview(input.details.stderrPreview);
|
|
const likelyCause = formatLikelyCause(input.details);
|
|
|
|
const lines = [
|
|
'Agent Teams OpenCode diagnostics',
|
|
`Time: ${input.createdAt}`,
|
|
'Provider: opencode',
|
|
`Severity: ${input.severity}`,
|
|
`Title: ${NO_OUTPUT_TITLE}`,
|
|
`Summary: ${NO_OUTPUT_SUMMARY}`,
|
|
'',
|
|
'Visible error:',
|
|
redactBridgeDiagnosticText(input.visibleError),
|
|
'',
|
|
'Bridge command:',
|
|
`command: ${command}`,
|
|
`requestId: ${requestId}`,
|
|
`binaryPath: ${formatDiagnosticPathValue(input.details.binaryPath)}`,
|
|
`cwd: ${formatDiagnosticPathValue(input.details.cwd)}`,
|
|
`attempts: ${formatDiagnosticValue(input.details.attempts)}`,
|
|
`exitCode: ${formatDiagnosticValue(input.details.exitCode)}`,
|
|
`timedOut: ${formatDiagnosticValue(input.details.timedOut)}`,
|
|
`stdoutBytes: ${formatDiagnosticValue(input.details.stdoutBytes)}`,
|
|
`stderrBytes: ${formatDiagnosticValue(input.details.stderrBytes)}`,
|
|
`outputSource: ${formatDiagnosticValue(input.details.outputSource)}`,
|
|
`outputFileBytes: ${formatDiagnosticValue(input.details.outputFileBytes)}`,
|
|
`outputReadError: ${formatDiagnosticValue(input.details.outputReadError)}`,
|
|
'',
|
|
'Environment:',
|
|
`platform: ${process.platform}`,
|
|
`arch: ${process.arch}`,
|
|
`appVersion: ${formatDiagnosticValue(input.appVersion)}`,
|
|
`projectPath: ${redactDiagnosticPath(input.projectPath)}`,
|
|
`selectedModel: ${formatDiagnosticValue(input.selectedModel)}`,
|
|
];
|
|
|
|
if (likelyCause) {
|
|
lines.push('', 'Likely cause:', likelyCause);
|
|
}
|
|
|
|
lines.push('', 'stderrPreview:', stderrPreview);
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
function formatLikelyCause(details: Record<string, unknown>): string | null {
|
|
if (details.exitCode !== 9009) {
|
|
return null;
|
|
}
|
|
return 'Windows could not start the bridge launcher. Check that the runtime launcher and its dependencies, such as Bun for cli-dev.cmd, are available in PATH.';
|
|
}
|
|
|
|
function formatDiagnosticValue(value: unknown, fallback: unknown = undefined): string {
|
|
const resolved = value ?? fallback;
|
|
if (resolved === null || resolved === undefined || resolved === '') {
|
|
return '(none)';
|
|
}
|
|
if (typeof resolved === 'string') {
|
|
return redactBridgeDiagnosticText(resolved);
|
|
}
|
|
if (typeof resolved === 'number' || typeof resolved === 'boolean') {
|
|
return String(resolved);
|
|
}
|
|
return redactBridgeDiagnosticText(JSON.stringify(resolved));
|
|
}
|
|
|
|
function formatDiagnosticPathValue(value: unknown): string {
|
|
if (typeof value !== 'string') {
|
|
return formatDiagnosticValue(value);
|
|
}
|
|
return redactDiagnosticPath(value);
|
|
}
|
|
|
|
function formatPreview(value: unknown): string {
|
|
const formatted = formatDiagnosticValue(value);
|
|
return formatted === '(none)' ? '(empty)' : formatted;
|
|
}
|
|
|
|
function redactDiagnosticPath(value: string): string {
|
|
const home = os.homedir();
|
|
const trimmed = value.trim();
|
|
if (!trimmed) {
|
|
return '(none)';
|
|
}
|
|
if (home && trimmed.startsWith(home)) {
|
|
return `~${trimmed.slice(home.length)}`;
|
|
}
|
|
return redactBridgeDiagnosticText(trimmed);
|
|
}
|