1684 lines
52 KiB
TypeScript
1684 lines
52 KiB
TypeScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
import { buildProviderAwareCliEnv } from '@main/services/runtime/providerAwareCliEnv';
|
|
import { ClaudeBinaryResolver } from '@main/services/team/ClaudeBinaryResolver';
|
|
import { execCli, killProcessTree, spawnCli } from '@main/utils/childProcess';
|
|
import { resolveInteractiveShellEnvBestEffort } from '@main/utils/shellEnv';
|
|
|
|
import {
|
|
ensureOpenCodeProfileNodeModulesJunction,
|
|
extractProfileIdFromSymlinkError,
|
|
isOpenCodeNodeModulesSymlinkError,
|
|
} from './openCodeWindowsNodeModulesJunction';
|
|
|
|
import type {
|
|
RuntimeProviderManagementApi,
|
|
RuntimeProviderManagementConnectApiKeyInput,
|
|
RuntimeProviderManagementConnectInput,
|
|
RuntimeProviderManagementDirectoryResponse,
|
|
RuntimeProviderManagementErrorDto,
|
|
RuntimeProviderManagementForgetInput,
|
|
RuntimeProviderManagementLoadDirectoryInput,
|
|
RuntimeProviderManagementLoadModelsInput,
|
|
RuntimeProviderManagementLoadSetupFormInput,
|
|
RuntimeProviderManagementLoadViewInput,
|
|
RuntimeProviderManagementModelsResponse,
|
|
RuntimeProviderManagementModelTestResponse,
|
|
RuntimeProviderManagementProviderResponse,
|
|
RuntimeProviderManagementRuntimeId,
|
|
RuntimeProviderManagementSetDefaultModelInput,
|
|
RuntimeProviderManagementSetupFormResponse,
|
|
RuntimeProviderManagementTestModelInput,
|
|
RuntimeProviderManagementViewResponse,
|
|
} from '@features/runtime-provider-management/contracts';
|
|
import type { ChildProcessWithoutNullStreams } from 'child_process';
|
|
|
|
const PROBE_COMMAND_TIMEOUT_MS = 90_000;
|
|
const COMMAND_TIMEOUT_MS = PROBE_COMMAND_TIMEOUT_MS;
|
|
const COMMAND_MAX_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
const COMMAND_ERROR_DETAIL_LIMIT = 1_600;
|
|
const COMMAND_OUTPUT_PREVIEW_LIMIT = 1_200;
|
|
const ESCAPE_CHARACTER = String.fromCharCode(27);
|
|
const BELL_CHARACTER = String.fromCharCode(7);
|
|
const ANSI_ESCAPE_PATTERN = new RegExp(`${ESCAPE_CHARACTER}\\[[0-?]*[ -/]*[@-~]`, 'g');
|
|
const OSC_ESCAPE_PATTERN = new RegExp(
|
|
`${ESCAPE_CHARACTER}\\][\\s\\S]*?(?:${BELL_CHARACTER}|${ESCAPE_CHARACTER}\\\\)`,
|
|
'g'
|
|
);
|
|
const OPENCODE_BINARY_BASENAMES = new Set([
|
|
'opencode',
|
|
'opencode.exe',
|
|
'opencode.cmd',
|
|
'opencode.ps1',
|
|
]);
|
|
const RUNTIME_PROVIDER_ERROR_CODES = new Set<RuntimeProviderManagementErrorDto['code']>([
|
|
'unsupported-runtime',
|
|
'unsupported-action',
|
|
'runtime-missing',
|
|
'runtime-misconfigured',
|
|
'runtime-unhealthy',
|
|
'provider-missing',
|
|
'auth-required',
|
|
'auth-failed',
|
|
'model-missing',
|
|
'model-test-failed',
|
|
'unsupported-auth-method',
|
|
]);
|
|
|
|
type RuntimeProviderManagementErrorResponse =
|
|
| RuntimeProviderManagementViewResponse
|
|
| RuntimeProviderManagementDirectoryResponse
|
|
| RuntimeProviderManagementProviderResponse
|
|
| RuntimeProviderManagementSetupFormResponse
|
|
| RuntimeProviderManagementModelsResponse
|
|
| RuntimeProviderManagementModelTestResponse;
|
|
|
|
interface RuntimeProviderCommandContext {
|
|
binaryPath: string;
|
|
args: readonly string[];
|
|
projectPath: string | null;
|
|
}
|
|
|
|
interface RuntimeProviderCommandFailure {
|
|
message: string;
|
|
diagnostics?: RuntimeProviderManagementErrorDto['diagnostics'];
|
|
}
|
|
|
|
class RuntimeProviderCommandOutputError extends Error {
|
|
readonly diagnostics: RuntimeProviderManagementErrorDto['diagnostics'];
|
|
|
|
constructor(failure: RuntimeProviderCommandFailure) {
|
|
super(failure.message);
|
|
this.name = 'RuntimeProviderCommandOutputError';
|
|
this.diagnostics = failure.diagnostics ?? null;
|
|
}
|
|
}
|
|
|
|
function errorResponse<T extends RuntimeProviderManagementErrorResponse>(
|
|
runtimeId: RuntimeProviderManagementRuntimeId,
|
|
message: string,
|
|
code: RuntimeProviderManagementErrorDto['code'] = 'runtime-unhealthy',
|
|
diagnostics: RuntimeProviderManagementErrorDto['diagnostics'] = null
|
|
): T {
|
|
return {
|
|
schemaVersion: 1,
|
|
runtimeId,
|
|
error: {
|
|
code,
|
|
message,
|
|
recoverable: true,
|
|
diagnostics: withRuntimeProviderErrorCode(code, diagnostics),
|
|
},
|
|
} as T;
|
|
}
|
|
|
|
function commandFailureResponse<T extends RuntimeProviderManagementErrorResponse>(
|
|
runtimeId: RuntimeProviderManagementRuntimeId,
|
|
failure: RuntimeProviderCommandFailure,
|
|
code: RuntimeProviderManagementErrorDto['code'] = 'runtime-unhealthy'
|
|
): T {
|
|
return errorResponse<T>(runtimeId, failure.message, code, failure.diagnostics ?? null);
|
|
}
|
|
|
|
function sanitizeRuntimeProviderResponse<T extends RuntimeProviderManagementErrorResponse>(
|
|
response: T
|
|
): T {
|
|
const sanitizedResponse = sanitizeRuntimeProviderOutputValue(response) as T;
|
|
const sanitizedError = (sanitizedResponse as { error?: unknown }).error;
|
|
if (sanitizedError === null) {
|
|
const responseWithoutNullError = { ...sanitizedResponse };
|
|
delete (responseWithoutNullError as { error?: unknown }).error;
|
|
return responseWithoutNullError;
|
|
}
|
|
if (!sanitizedError) {
|
|
return sanitizedResponse;
|
|
}
|
|
|
|
return {
|
|
...sanitizedResponse,
|
|
error: sanitizeRuntimeProviderError(sanitizedError),
|
|
};
|
|
}
|
|
|
|
function sanitizeRuntimeProviderError(error: unknown): RuntimeProviderManagementErrorDto {
|
|
if (!isRecord(error)) {
|
|
return {
|
|
code: 'runtime-unhealthy',
|
|
message: 'Runtime provider management command failed',
|
|
recoverable: true,
|
|
diagnostics: null,
|
|
};
|
|
}
|
|
const rawCode = error.code;
|
|
const code =
|
|
typeof rawCode === 'string' &&
|
|
RUNTIME_PROVIDER_ERROR_CODES.has(rawCode as RuntimeProviderManagementErrorDto['code'])
|
|
? (rawCode as RuntimeProviderManagementErrorDto['code'])
|
|
: 'runtime-unhealthy';
|
|
const diagnostics = sanitizeRuntimeProviderDiagnostics(error.diagnostics);
|
|
const message =
|
|
sanitizeNullableRuntimeProviderText(error.message) ??
|
|
'Runtime provider management command failed';
|
|
return {
|
|
code,
|
|
message,
|
|
recoverable: typeof error.recoverable === 'boolean' ? error.recoverable : true,
|
|
diagnostics: withRuntimeProviderErrorCode(
|
|
code,
|
|
diagnostics ?? buildOpenCodeProfileNodeModulesLinkDiagnostics(message)
|
|
),
|
|
};
|
|
}
|
|
|
|
function withRuntimeProviderErrorCode(
|
|
errorCode: RuntimeProviderManagementErrorDto['code'],
|
|
diagnostics: RuntimeProviderManagementErrorDto['diagnostics']
|
|
): RuntimeProviderManagementErrorDto['diagnostics'] {
|
|
return diagnostics ? { ...diagnostics, errorCode } : null;
|
|
}
|
|
|
|
function sanitizeRuntimeProviderOutputValue(value: unknown): unknown {
|
|
if (typeof value === 'string') {
|
|
return sanitizeRuntimeProviderText(value);
|
|
}
|
|
if (Array.isArray(value)) {
|
|
return value.map(sanitizeRuntimeProviderOutputValue);
|
|
}
|
|
if (!value || typeof value !== 'object') {
|
|
return value;
|
|
}
|
|
return Object.fromEntries(
|
|
Object.entries(value).map(([key, entry]) => [key, sanitizeRuntimeProviderOutputValue(entry)])
|
|
);
|
|
}
|
|
|
|
function sanitizeRuntimeProviderDiagnostics(
|
|
diagnostics: unknown
|
|
): RuntimeProviderManagementErrorDto['diagnostics'] {
|
|
if (!isRecord(diagnostics)) {
|
|
return null;
|
|
}
|
|
return {
|
|
errorCode:
|
|
typeof diagnostics.errorCode === 'string' &&
|
|
RUNTIME_PROVIDER_ERROR_CODES.has(
|
|
diagnostics.errorCode as RuntimeProviderManagementErrorDto['code']
|
|
)
|
|
? (diagnostics.errorCode as RuntimeProviderManagementErrorDto['code'])
|
|
: null,
|
|
summary: sanitizeNullableRuntimeProviderText(diagnostics.summary),
|
|
likelyCause: sanitizeNullableRuntimeProviderText(diagnostics.likelyCause),
|
|
binaryPath: sanitizeNullableRuntimeProviderText(diagnostics.binaryPath),
|
|
command: sanitizeNullableRuntimeProviderText(diagnostics.command),
|
|
projectPath: sanitizeNullableRuntimeProviderText(diagnostics.projectPath),
|
|
exitCode: typeof diagnostics.exitCode === 'number' ? diagnostics.exitCode : null,
|
|
stderrPreview: sanitizeNullableRuntimeProviderText(diagnostics.stderrPreview),
|
|
stdoutPreview: sanitizeNullableRuntimeProviderText(diagnostics.stdoutPreview),
|
|
hints: Array.isArray(diagnostics.hints)
|
|
? diagnostics.hints
|
|
.filter((hint): hint is string => typeof hint === 'string')
|
|
.map(sanitizeRuntimeProviderText)
|
|
: [],
|
|
};
|
|
}
|
|
|
|
function sanitizeNullableRuntimeProviderText(value: unknown): string | null {
|
|
return typeof value === 'string' ? sanitizeRuntimeProviderText(value) : null;
|
|
}
|
|
|
|
function buildOpenCodeProfileNodeModulesLinkDiagnostics(
|
|
message: string
|
|
): RuntimeProviderManagementErrorDto['diagnostics'] {
|
|
const normalized = message.toLowerCase();
|
|
const isAccessDeniedLinkFailure =
|
|
(normalized.includes('eperm') || normalized.includes('eacces')) &&
|
|
normalized.includes('symlink') &&
|
|
normalized.includes('opencode') &&
|
|
normalized.includes('node_modules');
|
|
if (!isAccessDeniedLinkFailure) {
|
|
return null;
|
|
}
|
|
|
|
const summary = 'OpenCode managed profile node_modules link was blocked.';
|
|
const likelyCause =
|
|
'Windows denied creating the managed OpenCode profile node_modules link. The app attempted automatic junction recovery when possible, but the link is still unavailable.';
|
|
return {
|
|
summary,
|
|
likelyCause,
|
|
binaryPath: null,
|
|
command: null,
|
|
projectPath: null,
|
|
exitCode: null,
|
|
stderrPreview: message,
|
|
stdoutPreview: null,
|
|
hints: [
|
|
'The app attempts automatic junction fallback for this Windows link failure before showing this error.',
|
|
'As a temporary workaround, enable Windows Developer Mode or run Agent Teams AI as Administrator.',
|
|
'After enabling Developer Mode, refresh the OpenCode provider catalog.',
|
|
],
|
|
};
|
|
}
|
|
|
|
function extractJsonObject<T>(raw: string): T {
|
|
const start = raw.indexOf('{');
|
|
if (start < 0) {
|
|
throw new Error('CLI did not return a JSON object');
|
|
}
|
|
|
|
for (let index = start; index >= 0 && index < raw.length; index = raw.indexOf('{', index + 1)) {
|
|
const end = findJsonObjectEnd(raw, index);
|
|
if (end === null) {
|
|
continue;
|
|
}
|
|
try {
|
|
const candidate = JSON.parse(raw.slice(index, end + 1)) as T;
|
|
if (isRuntimeProviderResponseCandidate(candidate)) {
|
|
return candidate;
|
|
}
|
|
} catch {
|
|
// Keep scanning. CLI output can contain brace-looking logs before the JSON response.
|
|
}
|
|
}
|
|
|
|
throw new Error('CLI did not return a JSON object');
|
|
}
|
|
|
|
function findJsonObjectEnd(raw: string, start: number): number | null {
|
|
let depth = 0;
|
|
let inString = false;
|
|
let escaped = false;
|
|
|
|
for (let index = start; index < raw.length; index += 1) {
|
|
const char = raw[index];
|
|
if (inString) {
|
|
if (escaped) {
|
|
escaped = false;
|
|
continue;
|
|
}
|
|
if (char === '\\') {
|
|
escaped = true;
|
|
continue;
|
|
}
|
|
if (char === '"') {
|
|
inString = false;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (char === '"') {
|
|
inString = true;
|
|
continue;
|
|
}
|
|
if (char === '{') {
|
|
depth += 1;
|
|
continue;
|
|
}
|
|
if (char !== '}') {
|
|
continue;
|
|
}
|
|
|
|
depth -= 1;
|
|
if (depth === 0) {
|
|
return index;
|
|
}
|
|
if (depth < 0) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function isRuntimeProviderResponseCandidate(value: unknown): boolean {
|
|
if (!isRecord(value)) {
|
|
return false;
|
|
}
|
|
return (
|
|
typeof value.schemaVersion === 'number' &&
|
|
typeof value.runtimeId === 'string' &&
|
|
hasRuntimeProviderResponsePayload(value)
|
|
);
|
|
}
|
|
|
|
function hasRuntimeProviderResponsePayload(record: Record<string, unknown>): boolean {
|
|
if (isRecord(record.error)) {
|
|
return isRuntimeProviderErrorPayload(record.error);
|
|
}
|
|
if ('view' in record) {
|
|
return isRuntimeProviderViewPayload(record.view);
|
|
}
|
|
if ('directory' in record) {
|
|
return isRuntimeProviderDirectoryPayload(record.directory);
|
|
}
|
|
if ('provider' in record) {
|
|
return isRuntimeProviderProviderPayload(record.provider);
|
|
}
|
|
if ('setupForm' in record) {
|
|
return isRuntimeProviderSetupFormPayload(record.setupForm);
|
|
}
|
|
if ('models' in record) {
|
|
return isRuntimeProviderModelsPayload(record.models);
|
|
}
|
|
if ('result' in record) {
|
|
return isRuntimeProviderModelTestResultPayload(record.result);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isRuntimeProviderErrorPayload(value: unknown): boolean {
|
|
return (
|
|
isRecord(value) &&
|
|
(typeof value.code === 'string' ||
|
|
typeof value.message === 'string' ||
|
|
typeof value.recoverable === 'boolean' ||
|
|
'diagnostics' in value)
|
|
);
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
}
|
|
|
|
function hasArrayField<K extends string>(
|
|
record: Record<string, unknown>,
|
|
key: K
|
|
): record is Record<string, unknown> & Record<K, unknown[]> {
|
|
return Array.isArray(record[key]);
|
|
}
|
|
|
|
function isRuntimeProviderViewPayload(value: unknown): boolean {
|
|
return (
|
|
isRecord(value) &&
|
|
hasArrayField(value, 'providers') &&
|
|
hasArrayField(value, 'diagnostics') &&
|
|
value.providers.every(isRuntimeProviderProviderPayload)
|
|
);
|
|
}
|
|
|
|
function isRuntimeProviderProviderPayload(value: unknown): boolean {
|
|
return (
|
|
isRecord(value) &&
|
|
hasArrayField(value, 'actions') &&
|
|
hasArrayField(value, 'authMethods') &&
|
|
hasArrayField(value, 'ownership')
|
|
);
|
|
}
|
|
|
|
function isRuntimeProviderDirectoryPayload(value: unknown): boolean {
|
|
return (
|
|
isRecord(value) &&
|
|
hasArrayField(value, 'entries') &&
|
|
hasArrayField(value, 'diagnostics') &&
|
|
value.entries.every(isRuntimeProviderDirectoryEntryPayload)
|
|
);
|
|
}
|
|
|
|
function isRuntimeProviderDirectoryEntryPayload(value: unknown): boolean {
|
|
return (
|
|
isRuntimeProviderProviderPayload(value) && isRecord(value) && hasArrayField(value, 'sources')
|
|
);
|
|
}
|
|
|
|
function isRuntimeProviderSetupFormPayload(value: unknown): boolean {
|
|
return (
|
|
isRecord(value) &&
|
|
hasArrayField(value, 'prompts') &&
|
|
value.prompts.every(isRuntimeProviderSetupPromptPayload)
|
|
);
|
|
}
|
|
|
|
function isRuntimeProviderSetupPromptPayload(value: unknown): boolean {
|
|
return isRecord(value) && hasArrayField(value, 'options');
|
|
}
|
|
|
|
function isRuntimeProviderModelsPayload(value: unknown): boolean {
|
|
return isRecord(value) && hasArrayField(value, 'models') && hasArrayField(value, 'diagnostics');
|
|
}
|
|
|
|
function isRuntimeProviderModelTestResultPayload(value: unknown): boolean {
|
|
return isRecord(value) && hasArrayField(value, 'diagnostics');
|
|
}
|
|
|
|
function stripTerminalFormatting(value: string): string {
|
|
return value.replace(OSC_ESCAPE_PATTERN, '').replace(ANSI_ESCAPE_PATTERN, '');
|
|
}
|
|
|
|
function sanitizeRuntimeProviderText(value: string): string {
|
|
return redactSensitiveText(stripTerminalFormatting(value));
|
|
}
|
|
|
|
function redactSensitiveText(value: string): string {
|
|
return value
|
|
.replace(/\b(sk-[A-Za-z0-9_-]{12,})\b/g, 'sk-...redacted')
|
|
.replace(/\b(or-[A-Za-z0-9_-]{12,})\b/g, 'or-...redacted')
|
|
.replace(/\b(AIza[A-Za-z0-9_-]{20,})\b/g, 'AIza...redacted')
|
|
.replace(
|
|
/\b([a-z0-9_.-]*(?:api[-_]?key|(?:access|auth)[-_]?token|token|secret|password|[-_]key)["'\s:=]+)([a-z0-9._~+/=-]{12,})/gi,
|
|
'$1...redacted'
|
|
)
|
|
.replace(/\b(key["'\s:=]+)([a-z0-9._~+/=-]{12,})/gi, '$1...redacted')
|
|
.replace(/\b(bearer\s+)([a-z0-9._~+/=-]{12,})/gi, '$1...redacted');
|
|
}
|
|
|
|
function formatCommandForDisplay(context: RuntimeProviderCommandContext): string {
|
|
return [context.binaryPath, ...context.args].map(formatCommandPartForDisplay).join(' ');
|
|
}
|
|
|
|
function formatCommandPartForDisplay(value: string): string {
|
|
if (/^[A-Za-z0-9_./:=@%+-]+$/.test(value)) {
|
|
return value;
|
|
}
|
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
}
|
|
|
|
function getOutputPreview(value: string | null): string | null {
|
|
const normalized = sanitizeRuntimeProviderText(value ?? '').trim();
|
|
if (!normalized) {
|
|
return null;
|
|
}
|
|
return truncateCommandErrorDetail(
|
|
normalized.length > COMMAND_OUTPUT_PREVIEW_LIMIT
|
|
? `${normalized.slice(0, COMMAND_OUTPUT_PREVIEW_LIMIT).trimEnd()}...`
|
|
: normalized
|
|
);
|
|
}
|
|
|
|
function sanitizeCommandErrorMessage(value: string): string {
|
|
return truncateCommandErrorDetail(sanitizeRuntimeProviderText(value.trim()));
|
|
}
|
|
|
|
function outputLooksLikeOpenCodeCliHelp(value: string | null): boolean {
|
|
const normalized = stripTerminalFormatting(value ?? '').toLowerCase();
|
|
return (
|
|
normalized.includes('opencode providers') ||
|
|
normalized.includes('opencode models') ||
|
|
(normalized.includes('commands:') && normalized.includes('opencode'))
|
|
);
|
|
}
|
|
|
|
function binaryLooksLikeOpenCode(binaryPath: string): boolean {
|
|
return getBinaryBasenameCandidates(binaryPath).some((basename) =>
|
|
OPENCODE_BINARY_BASENAMES.has(basename)
|
|
);
|
|
}
|
|
|
|
function getBinaryBasenameCandidates(binaryPath: string): string[] {
|
|
const basenames = new Set([path.basename(binaryPath).toLowerCase()]);
|
|
try {
|
|
basenames.add(path.basename(fs.realpathSync.native(binaryPath)).toLowerCase());
|
|
} catch {
|
|
// Nonexistent mocked paths are handled by the literal basename above.
|
|
}
|
|
return [...basenames];
|
|
}
|
|
|
|
function formatNonJsonCliOutputError(input: {
|
|
context: RuntimeProviderCommandContext;
|
|
stdout?: string | null;
|
|
stderr?: string | null;
|
|
exitCode?: number | null;
|
|
}): RuntimeProviderCommandFailure {
|
|
const stdoutPreview = getOutputPreview(input.stdout ?? null);
|
|
const stderrPreview = getOutputPreview(input.stderr ?? null);
|
|
const likelyWrongBinary =
|
|
binaryLooksLikeOpenCode(input.context.binaryPath) ||
|
|
outputLooksLikeOpenCodeCliHelp(input.stdout ?? null) ||
|
|
outputLooksLikeOpenCodeCliHelp(input.stderr ?? null);
|
|
const likelyCause = likelyWrongBinary
|
|
? 'The app is launching the OpenCode CLI itself instead of the Agent Teams runtime (claude-multimodel).'
|
|
: 'The runtime command printed logs, help text, or a crash message instead of JSON.';
|
|
const hints = likelyWrongBinary
|
|
? [
|
|
'Check CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH and CLAUDE_CLI_PATH.',
|
|
'Those environment variables must not point to opencode.',
|
|
'The expected binary is the Agent Teams runtime/orchestrator CLI, not the OpenCode CLI.',
|
|
]
|
|
: [
|
|
'Open stderr preview first. It usually contains the real crash or missing dependency.',
|
|
'Run the shown command from the same project path to reproduce the runtime output.',
|
|
];
|
|
const lines = [
|
|
'OpenCode provider settings could not read the runtime response.',
|
|
'Expected a JSON object from the Agent Teams runtime provider command.',
|
|
`Resolved runtime binary: ${input.context.binaryPath}`,
|
|
`Command: ${formatCommandForDisplay(input.context)}`,
|
|
];
|
|
|
|
if (input.context.projectPath) {
|
|
lines.push(`Project path: ${input.context.projectPath}`);
|
|
}
|
|
if (input.exitCode !== undefined) {
|
|
lines.push(`Exit code: ${String(input.exitCode ?? 'unknown')}`);
|
|
}
|
|
|
|
if (likelyWrongBinary) {
|
|
lines.push(`Likely cause: ${likelyCause}`, ...hints);
|
|
} else {
|
|
lines.push(`Likely cause: ${likelyCause}`);
|
|
}
|
|
|
|
if (stderrPreview) {
|
|
lines.push('stderr preview:', stderrPreview);
|
|
}
|
|
if (stdoutPreview) {
|
|
lines.push('stdout preview:', stdoutPreview);
|
|
}
|
|
if (!stderrPreview && !stdoutPreview) {
|
|
lines.push('No stdout or stderr was captured from the runtime command.');
|
|
}
|
|
|
|
return {
|
|
message: lines.join('\n'),
|
|
diagnostics: {
|
|
summary: 'OpenCode provider settings could not read the runtime response.',
|
|
likelyCause,
|
|
binaryPath: input.context.binaryPath,
|
|
command: formatCommandForDisplay(input.context),
|
|
projectPath: input.context.projectPath,
|
|
exitCode: input.exitCode ?? null,
|
|
stderrPreview,
|
|
stdoutPreview,
|
|
hints,
|
|
},
|
|
};
|
|
}
|
|
|
|
function formatWrongRuntimeBinaryError(
|
|
context: RuntimeProviderCommandContext
|
|
): RuntimeProviderCommandFailure {
|
|
const likelyCause = 'The app resolved the OpenCode CLI itself as the Agent Teams runtime binary.';
|
|
const hints = [
|
|
'Check CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH and CLAUDE_CLI_PATH.',
|
|
'Those environment variables must not point to opencode.',
|
|
'The expected binary is the Agent Teams runtime/orchestrator CLI, not the OpenCode CLI.',
|
|
];
|
|
const lines = [
|
|
'OpenCode provider settings are using the wrong runtime binary.',
|
|
`Resolved runtime binary: ${context.binaryPath}`,
|
|
`Command that was blocked: ${formatCommandForDisplay(context)}`,
|
|
];
|
|
|
|
if (context.projectPath) {
|
|
lines.push(`Project path: ${context.projectPath}`);
|
|
}
|
|
|
|
lines.push(`Likely cause: ${likelyCause}`, ...hints);
|
|
|
|
return {
|
|
message: lines.join('\n'),
|
|
diagnostics: {
|
|
summary: 'OpenCode provider settings are using the wrong runtime binary.',
|
|
likelyCause,
|
|
binaryPath: context.binaryPath,
|
|
command: formatCommandForDisplay(context),
|
|
projectPath: context.projectPath,
|
|
exitCode: null,
|
|
stderrPreview: null,
|
|
stdoutPreview: null,
|
|
hints,
|
|
},
|
|
};
|
|
}
|
|
|
|
function formatCommandExecutionError(input: {
|
|
context: RuntimeProviderCommandContext;
|
|
errorMessage: string;
|
|
}): RuntimeProviderCommandFailure {
|
|
const sanitizedError = sanitizeCommandErrorMessage(input.errorMessage);
|
|
const likelyCause = 'The runtime command failed before it returned JSON output.';
|
|
const hints = [
|
|
'Check whether the resolved runtime binary exists and is executable.',
|
|
'Run the shown command from the same project path to reproduce the failure.',
|
|
];
|
|
const lines = [
|
|
'OpenCode provider settings could not run the runtime command.',
|
|
`Resolved runtime binary: ${input.context.binaryPath}`,
|
|
`Command: ${formatCommandForDisplay(input.context)}`,
|
|
];
|
|
|
|
if (input.context.projectPath) {
|
|
lines.push(`Project path: ${input.context.projectPath}`);
|
|
}
|
|
|
|
lines.push(`Likely cause: ${likelyCause}`);
|
|
if (sanitizedError) {
|
|
lines.push('Error:', sanitizedError);
|
|
}
|
|
lines.push(...hints);
|
|
|
|
return {
|
|
message: lines.join('\n'),
|
|
diagnostics: {
|
|
summary: 'OpenCode provider settings could not run the runtime command.',
|
|
likelyCause,
|
|
binaryPath: input.context.binaryPath,
|
|
command: formatCommandForDisplay(input.context),
|
|
projectPath: input.context.projectPath,
|
|
exitCode: null,
|
|
stderrPreview: sanitizedError || null,
|
|
stdoutPreview: null,
|
|
hints,
|
|
},
|
|
};
|
|
}
|
|
|
|
function isCommandTimeoutMessage(value: string): boolean {
|
|
const normalized = value.toLowerCase();
|
|
return normalized.includes('timed out') || normalized.includes('timeout');
|
|
}
|
|
|
|
function formatCommandTimeoutError(input: {
|
|
context: RuntimeProviderCommandContext;
|
|
errorMessage: string;
|
|
stdout?: string | null;
|
|
stderr?: string | null;
|
|
}): RuntimeProviderCommandFailure {
|
|
const stdoutPreview = getOutputPreview(input.stdout ?? null);
|
|
const stderrPreview = getOutputPreview(input.stderr ?? null);
|
|
const sanitizedError = sanitizeCommandErrorMessage(input.errorMessage);
|
|
const likelyCause =
|
|
'The Agent Teams runtime command did not return JSON before the desktop timeout.';
|
|
const hints = [
|
|
'This is not enough evidence to conclude that OpenCode auth is missing.',
|
|
'Run the shown command from the same project path to see the runtime-side OpenCode diagnostics.',
|
|
'If the command hangs before printing JSON, check OpenCode CLI startup, provider/model listing, local OpenCode plugins, cache/profile corruption, and Windows security software delays.',
|
|
'If the runtime binary is stale, update Agent Teams so the runtime can return a degraded OpenCode diagnostic instead of timing out.',
|
|
];
|
|
const lines = [
|
|
'OpenCode provider settings timed out while waiting for the Agent Teams runtime.',
|
|
`Resolved runtime binary: ${input.context.binaryPath}`,
|
|
`Command: ${formatCommandForDisplay(input.context)}`,
|
|
];
|
|
|
|
if (input.context.projectPath) {
|
|
lines.push(`Project path: ${input.context.projectPath}`);
|
|
}
|
|
|
|
lines.push(`Likely cause: ${likelyCause}`);
|
|
if (sanitizedError) {
|
|
lines.push('Timeout detail:', sanitizedError);
|
|
}
|
|
if (stderrPreview) {
|
|
lines.push('stderr preview:', stderrPreview);
|
|
}
|
|
if (stdoutPreview) {
|
|
lines.push('stdout preview:', stdoutPreview);
|
|
}
|
|
lines.push(...hints);
|
|
|
|
return {
|
|
message: lines.join('\n'),
|
|
diagnostics: {
|
|
summary: 'OpenCode provider settings timed out while waiting for the Agent Teams runtime.',
|
|
likelyCause,
|
|
binaryPath: input.context.binaryPath,
|
|
command: formatCommandForDisplay(input.context),
|
|
projectPath: input.context.projectPath,
|
|
exitCode: null,
|
|
stderrPreview: stderrPreview ?? sanitizedError,
|
|
stdoutPreview,
|
|
hints,
|
|
},
|
|
};
|
|
}
|
|
|
|
function formatMissingRuntimeBinaryError(
|
|
projectPath: string | null
|
|
): RuntimeProviderCommandFailure {
|
|
const likelyCause =
|
|
'The Agent Teams runtime/orchestrator CLI could not be resolved from the current environment.';
|
|
const hints = [
|
|
'Check CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH and CLAUDE_CLI_PATH.',
|
|
'If you are developing locally, start the desktop app from a shell that can resolve the orchestrator CLI.',
|
|
'The expected binary is the Agent Teams runtime/orchestrator CLI, not the OpenCode CLI.',
|
|
];
|
|
const lines = [
|
|
'OpenCode provider settings could not find the Agent Teams runtime binary.',
|
|
`Likely cause: ${likelyCause}`,
|
|
...hints,
|
|
];
|
|
|
|
if (projectPath) {
|
|
lines.splice(1, 0, `Project path: ${projectPath}`);
|
|
}
|
|
|
|
return {
|
|
message: lines.join('\n'),
|
|
diagnostics: {
|
|
summary: 'OpenCode provider settings could not find the Agent Teams runtime binary.',
|
|
likelyCause,
|
|
binaryPath: null,
|
|
command: null,
|
|
projectPath,
|
|
exitCode: null,
|
|
stderrPreview: null,
|
|
stdoutPreview: null,
|
|
hints,
|
|
},
|
|
};
|
|
}
|
|
|
|
function missingRuntimeBinaryResponse<T extends RuntimeProviderManagementErrorResponse>(
|
|
runtimeId: RuntimeProviderManagementRuntimeId,
|
|
projectPath: string | null
|
|
): T {
|
|
return commandFailureResponse<T>(
|
|
runtimeId,
|
|
formatMissingRuntimeBinaryError(projectPath),
|
|
'runtime-missing'
|
|
);
|
|
}
|
|
|
|
function rejectWrongRuntimeBinary<T extends RuntimeProviderManagementErrorResponse>(
|
|
runtimeId: RuntimeProviderManagementRuntimeId,
|
|
context: RuntimeProviderCommandContext
|
|
): T | null {
|
|
if (!binaryLooksLikeOpenCode(context.binaryPath)) {
|
|
return null;
|
|
}
|
|
ClaudeBinaryResolver.clearCache();
|
|
return commandFailureResponse<T>(
|
|
runtimeId,
|
|
formatWrongRuntimeBinaryError(context),
|
|
'runtime-misconfigured'
|
|
);
|
|
}
|
|
|
|
function extractJsonObjectWithContext<T extends RuntimeProviderManagementErrorResponse>(
|
|
raw: string,
|
|
context: RuntimeProviderCommandContext,
|
|
stderr: string | null = null
|
|
): T {
|
|
try {
|
|
return sanitizeRuntimeProviderResponse(extractJsonObject<T>(raw));
|
|
} catch {
|
|
throw new RuntimeProviderCommandOutputError(
|
|
formatNonJsonCliOutputError({ context, stdout: raw, stderr })
|
|
);
|
|
}
|
|
}
|
|
|
|
function tryExtractJsonObject<T extends RuntimeProviderManagementErrorResponse>(
|
|
raw: string | null
|
|
): T | null {
|
|
if (!raw) {
|
|
return null;
|
|
}
|
|
try {
|
|
return sanitizeRuntimeProviderResponse(extractJsonObject<T>(raw));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function readErrorTextProperty(error: unknown, propertyName: 'stderr' | 'stdout'): string | null {
|
|
if (!error || typeof error !== 'object' || !(propertyName in error)) {
|
|
return null;
|
|
}
|
|
const value = (error as Record<string, unknown>)[propertyName];
|
|
if (typeof value === 'string') {
|
|
const trimmed = value.trim();
|
|
return trimmed ? trimmed : null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function extractJsonObjectFromError<T extends RuntimeProviderManagementErrorResponse>(
|
|
error: unknown
|
|
): T | null {
|
|
return (
|
|
tryExtractJsonObject<T>(readErrorTextProperty(error, 'stdout')) ??
|
|
tryExtractJsonObject<T>(readErrorTextProperty(error, 'stderr'))
|
|
);
|
|
}
|
|
|
|
function truncateCommandErrorDetail(message: string): string {
|
|
if (message.length <= COMMAND_ERROR_DETAIL_LIMIT) {
|
|
return message;
|
|
}
|
|
return `${message.slice(0, COMMAND_ERROR_DETAIL_LIMIT).trimEnd()}...`;
|
|
}
|
|
|
|
function normalizeCommandFailure(
|
|
error: unknown,
|
|
context?: RuntimeProviderCommandContext
|
|
): RuntimeProviderCommandFailure {
|
|
if (error instanceof RuntimeProviderCommandOutputError) {
|
|
return {
|
|
message: truncateCommandErrorDetail(error.message),
|
|
diagnostics: error.diagnostics,
|
|
};
|
|
}
|
|
const stderr = readErrorTextProperty(error, 'stderr');
|
|
const stdout = readErrorTextProperty(error, 'stdout');
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
if (context && isCommandTimeoutMessage(message)) {
|
|
return formatCommandTimeoutError({
|
|
context,
|
|
errorMessage: message,
|
|
stdout,
|
|
stderr,
|
|
});
|
|
}
|
|
if (
|
|
context &&
|
|
(outputLooksLikeOpenCodeCliHelp(stdout) ||
|
|
outputLooksLikeOpenCodeCliHelp(stderr) ||
|
|
(stdout && !stderr && binaryLooksLikeOpenCode(context.binaryPath)))
|
|
) {
|
|
return formatNonJsonCliOutputError({ context, stdout, stderr });
|
|
}
|
|
if (context && (stdout || stderr)) {
|
|
return formatNonJsonCliOutputError({ context, stdout, stderr });
|
|
}
|
|
if (stderr) {
|
|
return { message: sanitizeCommandErrorMessage(stderr) };
|
|
}
|
|
if (stdout) {
|
|
return { message: sanitizeCommandErrorMessage(stdout) };
|
|
}
|
|
if (error instanceof Error && error.message.trim()) {
|
|
if (context) {
|
|
return formatCommandExecutionError({ context, errorMessage: error.message });
|
|
}
|
|
return { message: sanitizeCommandErrorMessage(error.message) };
|
|
}
|
|
return { message: 'Runtime provider management command failed' };
|
|
}
|
|
|
|
function createCommandContext(
|
|
binaryPath: string,
|
|
args: readonly string[],
|
|
projectPath: string | null
|
|
): RuntimeProviderCommandContext {
|
|
return { binaryPath, args, projectPath };
|
|
}
|
|
|
|
function normalizeProjectPath(projectPath: string | null | undefined): string | null {
|
|
const normalized = projectPath?.trim();
|
|
return normalized ? normalized : null;
|
|
}
|
|
|
|
function appendProjectPathArgs(args: string[], projectPath: string | null): string[] {
|
|
return projectPath ? [...args, '--project-path', projectPath] : args;
|
|
}
|
|
|
|
function appendOptionalArg(args: string[], name: string, value: string | null | undefined): void {
|
|
const normalized = value?.trim();
|
|
if (normalized) {
|
|
args.push(name, normalized);
|
|
}
|
|
}
|
|
|
|
function runtimeProviderCommandOptions<T extends { env: NodeJS.ProcessEnv }>(
|
|
options: T,
|
|
projectPath: string | null
|
|
): T & { cwd?: string; maxBuffer: number } {
|
|
const commandOptions = {
|
|
...options,
|
|
maxBuffer: COMMAND_MAX_BUFFER_BYTES,
|
|
};
|
|
return projectPath ? { ...commandOptions, cwd: projectPath } : commandOptions;
|
|
}
|
|
|
|
async function resolveCliEnv(): Promise<{
|
|
binaryPath: string | null;
|
|
env: NodeJS.ProcessEnv;
|
|
}> {
|
|
const shellEnv = await resolveInteractiveShellEnvBestEffort({
|
|
timeoutMs: 1_500,
|
|
fallbackEnv: process.env,
|
|
background: false,
|
|
});
|
|
const binaryPath = await ClaudeBinaryResolver.resolve();
|
|
if (!binaryPath) {
|
|
return {
|
|
binaryPath: null,
|
|
env: {
|
|
...process.env,
|
|
...shellEnv,
|
|
},
|
|
};
|
|
}
|
|
if (binaryLooksLikeOpenCode(binaryPath)) {
|
|
return {
|
|
binaryPath,
|
|
env: {
|
|
...process.env,
|
|
...shellEnv,
|
|
},
|
|
};
|
|
}
|
|
|
|
const providerAware = await buildProviderAwareCliEnv({
|
|
binaryPath,
|
|
providerId: 'opencode',
|
|
shellEnv,
|
|
connectionMode: 'augment',
|
|
});
|
|
return {
|
|
binaryPath,
|
|
env: providerAware.env,
|
|
};
|
|
}
|
|
|
|
function collectSpawnOutput(
|
|
child: ChildProcessWithoutNullStreams,
|
|
stdinValue: string
|
|
): Promise<{ stdout: string; stderr: string; code: number | null; stdinError: string | null }> {
|
|
return new Promise((resolve, reject) => {
|
|
const stdout: Buffer[] = [];
|
|
const stderr: Buffer[] = [];
|
|
let stdinError: string | null = null;
|
|
let settled = false;
|
|
|
|
const timeout = setTimeout(() => {
|
|
if (settled) {
|
|
return;
|
|
}
|
|
settled = true;
|
|
killProcessTree(child, 'SIGKILL');
|
|
const error = new Error('Runtime provider management command timed out');
|
|
Object.assign(error, readSpawnOutputSnapshot(stdout, stderr));
|
|
reject(error);
|
|
}, COMMAND_TIMEOUT_MS);
|
|
|
|
child.stdout.on('data', (chunk: Buffer) => stdout.push(chunk));
|
|
child.stderr.on('data', (chunk: Buffer) => stderr.push(chunk));
|
|
child.stdin.once('error', (error: Error) => {
|
|
stdinError = error.message;
|
|
});
|
|
child.once('error', (error) => {
|
|
if (settled) {
|
|
return;
|
|
}
|
|
settled = true;
|
|
clearTimeout(timeout);
|
|
Object.assign(error, readSpawnOutputSnapshot(stdout, stderr));
|
|
reject(error);
|
|
});
|
|
child.once('close', (code) => {
|
|
if (settled) {
|
|
return;
|
|
}
|
|
settled = true;
|
|
clearTimeout(timeout);
|
|
resolve({
|
|
stdout: Buffer.concat(stdout).toString('utf8'),
|
|
stderr: Buffer.concat(stderr).toString('utf8'),
|
|
code,
|
|
stdinError,
|
|
});
|
|
});
|
|
|
|
try {
|
|
child.stdin.write(stdinValue);
|
|
child.stdin.end();
|
|
} catch (error) {
|
|
if (settled) {
|
|
return;
|
|
}
|
|
settled = true;
|
|
clearTimeout(timeout);
|
|
if (error instanceof Error) {
|
|
Object.assign(error, readSpawnOutputSnapshot(stdout, stderr));
|
|
reject(error);
|
|
return;
|
|
}
|
|
const fallbackError = new Error('Runtime provider management command stdin write failed');
|
|
Object.assign(fallbackError, readSpawnOutputSnapshot(stdout, stderr));
|
|
reject(fallbackError);
|
|
}
|
|
});
|
|
}
|
|
|
|
function mergeSpawnStderrWithStdinError(result: {
|
|
stderr: string;
|
|
stdinError: string | null;
|
|
}): string {
|
|
if (!result.stdinError?.trim()) {
|
|
return result.stderr;
|
|
}
|
|
const stdinErrorLine = `stdin error: ${result.stdinError.trim()}`;
|
|
return result.stderr.trim() ? `${result.stderr.trimEnd()}\n${stdinErrorLine}` : stdinErrorLine;
|
|
}
|
|
|
|
function readSpawnOutputSnapshot(
|
|
stdout: readonly Buffer[],
|
|
stderr: readonly Buffer[]
|
|
): { stdout: string; stderr: string } {
|
|
return {
|
|
stdout: Buffer.concat(stdout).toString('utf8'),
|
|
stderr: Buffer.concat(stderr).toString('utf8'),
|
|
};
|
|
}
|
|
|
|
export class AgentTeamsRuntimeProviderManagementCliClient implements RuntimeProviderManagementApi {
|
|
async loadView(
|
|
input: RuntimeProviderManagementLoadViewInput
|
|
): Promise<RuntimeProviderManagementViewResponse> {
|
|
const projectPath = normalizeProjectPath(input.projectPath);
|
|
const { binaryPath, env } = await resolveCliEnv();
|
|
if (!binaryPath) {
|
|
return missingRuntimeBinaryResponse<RuntimeProviderManagementViewResponse>(
|
|
input.runtimeId,
|
|
projectPath
|
|
);
|
|
}
|
|
|
|
const args = appendProjectPathArgs(
|
|
['runtime', 'providers', 'view', '--runtime', input.runtimeId, '--json', '--compact'],
|
|
projectPath
|
|
);
|
|
const context = createCommandContext(binaryPath, args, projectPath);
|
|
const misconfigured = rejectWrongRuntimeBinary<RuntimeProviderManagementViewResponse>(
|
|
input.runtimeId,
|
|
context
|
|
);
|
|
if (misconfigured) {
|
|
return misconfigured;
|
|
}
|
|
try {
|
|
const { stdout, stderr } = await execCli(
|
|
binaryPath,
|
|
args,
|
|
runtimeProviderCommandOptions({ env, timeout: COMMAND_TIMEOUT_MS }, projectPath)
|
|
);
|
|
return extractJsonObjectWithContext<RuntimeProviderManagementViewResponse>(
|
|
stdout,
|
|
context,
|
|
stderr
|
|
);
|
|
} catch (error) {
|
|
const failure = normalizeCommandFailure(error, context);
|
|
|
|
if (process.platform === 'win32' && isOpenCodeNodeModulesSymlinkError(failure.message)) {
|
|
const profileId = extractProfileIdFromSymlinkError(failure.message);
|
|
if (profileId) {
|
|
const junctionReady = ensureOpenCodeProfileNodeModulesJunction(
|
|
profileId,
|
|
failure.message
|
|
);
|
|
if (junctionReady) {
|
|
try {
|
|
const retryResult = await execCli(
|
|
binaryPath,
|
|
args,
|
|
runtimeProviderCommandOptions({ env, timeout: COMMAND_TIMEOUT_MS }, projectPath)
|
|
);
|
|
return extractJsonObjectWithContext<RuntimeProviderManagementViewResponse>(
|
|
retryResult.stdout,
|
|
context,
|
|
retryResult.stderr
|
|
);
|
|
} catch {
|
|
// Retry also failed; fall through to return the original error.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const retryResponse =
|
|
extractJsonObjectFromError<RuntimeProviderManagementViewResponse>(error);
|
|
if (retryResponse) {
|
|
return retryResponse;
|
|
}
|
|
return commandFailureResponse<RuntimeProviderManagementViewResponse>(
|
|
input.runtimeId,
|
|
failure
|
|
);
|
|
}
|
|
}
|
|
|
|
async loadProviderDirectory(
|
|
input: RuntimeProviderManagementLoadDirectoryInput
|
|
): Promise<RuntimeProviderManagementDirectoryResponse> {
|
|
const projectPath = normalizeProjectPath(input.projectPath);
|
|
const { binaryPath, env } = await resolveCliEnv();
|
|
if (!binaryPath) {
|
|
return missingRuntimeBinaryResponse<RuntimeProviderManagementDirectoryResponse>(
|
|
input.runtimeId,
|
|
projectPath
|
|
);
|
|
}
|
|
|
|
const args = ['runtime', 'providers', 'directory', '--runtime', input.runtimeId, '--json'];
|
|
appendOptionalArg(args, '--project-path', projectPath);
|
|
appendOptionalArg(args, '--query', input.query ?? null);
|
|
appendOptionalArg(args, '--filter', input.filter ?? null);
|
|
if (typeof input.limit === 'number' && Number.isFinite(input.limit) && input.limit > 0) {
|
|
args.push('--limit', String(Math.floor(input.limit)));
|
|
}
|
|
appendOptionalArg(args, '--cursor', input.cursor ?? null);
|
|
if (input.refresh) {
|
|
args.push('--refresh');
|
|
}
|
|
const context = createCommandContext(binaryPath, args, projectPath);
|
|
const misconfigured = rejectWrongRuntimeBinary<RuntimeProviderManagementDirectoryResponse>(
|
|
input.runtimeId,
|
|
context
|
|
);
|
|
if (misconfigured) {
|
|
return misconfigured;
|
|
}
|
|
|
|
try {
|
|
const { stdout, stderr } = await execCli(
|
|
binaryPath,
|
|
args,
|
|
runtimeProviderCommandOptions({ env, timeout: COMMAND_TIMEOUT_MS }, projectPath)
|
|
);
|
|
return extractJsonObjectWithContext<RuntimeProviderManagementDirectoryResponse>(
|
|
stdout,
|
|
context,
|
|
stderr
|
|
);
|
|
} catch (error) {
|
|
const failure = normalizeCommandFailure(error, context);
|
|
|
|
if (process.platform === 'win32' && isOpenCodeNodeModulesSymlinkError(failure.message)) {
|
|
const profileId = extractProfileIdFromSymlinkError(failure.message);
|
|
if (profileId) {
|
|
const junctionReady = ensureOpenCodeProfileNodeModulesJunction(
|
|
profileId,
|
|
failure.message
|
|
);
|
|
if (junctionReady) {
|
|
try {
|
|
const retryResult = await execCli(
|
|
binaryPath,
|
|
args,
|
|
runtimeProviderCommandOptions({ env, timeout: COMMAND_TIMEOUT_MS }, projectPath)
|
|
);
|
|
return extractJsonObjectWithContext<RuntimeProviderManagementDirectoryResponse>(
|
|
retryResult.stdout,
|
|
context,
|
|
retryResult.stderr
|
|
);
|
|
} catch {
|
|
// Retry also failed; fall through to return the original error.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const retryResponse =
|
|
extractJsonObjectFromError<RuntimeProviderManagementDirectoryResponse>(error);
|
|
if (retryResponse) {
|
|
return retryResponse;
|
|
}
|
|
return commandFailureResponse<RuntimeProviderManagementDirectoryResponse>(
|
|
input.runtimeId,
|
|
failure
|
|
);
|
|
}
|
|
}
|
|
|
|
async loadSetupForm(
|
|
input: RuntimeProviderManagementLoadSetupFormInput
|
|
): Promise<RuntimeProviderManagementSetupFormResponse> {
|
|
const projectPath = normalizeProjectPath(input.projectPath);
|
|
const { binaryPath, env } = await resolveCliEnv();
|
|
if (!binaryPath) {
|
|
return missingRuntimeBinaryResponse<RuntimeProviderManagementSetupFormResponse>(
|
|
input.runtimeId,
|
|
projectPath
|
|
);
|
|
}
|
|
|
|
const args = appendProjectPathArgs(
|
|
[
|
|
'runtime',
|
|
'providers',
|
|
'setup-form',
|
|
'--runtime',
|
|
input.runtimeId,
|
|
'--provider',
|
|
input.providerId,
|
|
'--json',
|
|
],
|
|
projectPath
|
|
);
|
|
const context = createCommandContext(binaryPath, args, projectPath);
|
|
const misconfigured = rejectWrongRuntimeBinary<RuntimeProviderManagementSetupFormResponse>(
|
|
input.runtimeId,
|
|
context
|
|
);
|
|
if (misconfigured) {
|
|
return misconfigured;
|
|
}
|
|
try {
|
|
const { stdout, stderr } = await execCli(
|
|
binaryPath,
|
|
args,
|
|
runtimeProviderCommandOptions({ env, timeout: COMMAND_TIMEOUT_MS }, projectPath)
|
|
);
|
|
return extractJsonObjectWithContext<RuntimeProviderManagementSetupFormResponse>(
|
|
stdout,
|
|
context,
|
|
stderr
|
|
);
|
|
} catch (error) {
|
|
const response =
|
|
extractJsonObjectFromError<RuntimeProviderManagementSetupFormResponse>(error);
|
|
if (response) {
|
|
return response;
|
|
}
|
|
return commandFailureResponse<RuntimeProviderManagementSetupFormResponse>(
|
|
input.runtimeId,
|
|
normalizeCommandFailure(error, context)
|
|
);
|
|
}
|
|
}
|
|
|
|
async connectProvider(
|
|
input: RuntimeProviderManagementConnectInput
|
|
): Promise<RuntimeProviderManagementProviderResponse> {
|
|
const projectPath = normalizeProjectPath(input.projectPath);
|
|
const { binaryPath, env } = await resolveCliEnv();
|
|
if (!binaryPath) {
|
|
return missingRuntimeBinaryResponse<RuntimeProviderManagementProviderResponse>(
|
|
input.runtimeId,
|
|
projectPath
|
|
);
|
|
}
|
|
|
|
const args = appendProjectPathArgs(
|
|
[
|
|
'runtime',
|
|
'providers',
|
|
'connect',
|
|
'--runtime',
|
|
input.runtimeId,
|
|
'--provider',
|
|
input.providerId,
|
|
'--stdin-json',
|
|
'--json',
|
|
],
|
|
projectPath
|
|
);
|
|
const context = createCommandContext(binaryPath, args, projectPath);
|
|
const misconfigured = rejectWrongRuntimeBinary<RuntimeProviderManagementProviderResponse>(
|
|
input.runtimeId,
|
|
context
|
|
);
|
|
if (misconfigured) {
|
|
return misconfigured;
|
|
}
|
|
try {
|
|
const child = spawnCli(
|
|
binaryPath,
|
|
args,
|
|
runtimeProviderCommandOptions(
|
|
{
|
|
env,
|
|
stdio: 'pipe' as const,
|
|
},
|
|
projectPath
|
|
)
|
|
) as ChildProcessWithoutNullStreams;
|
|
const result = await collectSpawnOutput(
|
|
child,
|
|
JSON.stringify({
|
|
method: input.method,
|
|
apiKey: input.apiKey ?? null,
|
|
metadata: input.metadata ?? {},
|
|
})
|
|
);
|
|
if (result.code === 0) {
|
|
return extractJsonObjectWithContext<RuntimeProviderManagementProviderResponse>(
|
|
result.stdout,
|
|
context,
|
|
mergeSpawnStderrWithStdinError(result)
|
|
);
|
|
}
|
|
|
|
try {
|
|
return sanitizeRuntimeProviderResponse(
|
|
extractJsonObject<RuntimeProviderManagementProviderResponse>(result.stdout)
|
|
);
|
|
} catch {
|
|
return commandFailureResponse<RuntimeProviderManagementProviderResponse>(
|
|
input.runtimeId,
|
|
formatNonJsonCliOutputError({
|
|
context,
|
|
stdout: result.stdout,
|
|
stderr: mergeSpawnStderrWithStdinError(result),
|
|
exitCode: result.code,
|
|
})
|
|
);
|
|
}
|
|
} catch (error) {
|
|
const response = extractJsonObjectFromError<RuntimeProviderManagementProviderResponse>(error);
|
|
if (response) {
|
|
return response;
|
|
}
|
|
return commandFailureResponse<RuntimeProviderManagementProviderResponse>(
|
|
input.runtimeId,
|
|
normalizeCommandFailure(error, context)
|
|
);
|
|
}
|
|
}
|
|
|
|
async connectWithApiKey(
|
|
input: RuntimeProviderManagementConnectApiKeyInput
|
|
): Promise<RuntimeProviderManagementProviderResponse> {
|
|
const projectPath = normalizeProjectPath(input.projectPath);
|
|
const { binaryPath, env } = await resolveCliEnv();
|
|
if (!binaryPath) {
|
|
return missingRuntimeBinaryResponse<RuntimeProviderManagementProviderResponse>(
|
|
input.runtimeId,
|
|
projectPath
|
|
);
|
|
}
|
|
|
|
const args = appendProjectPathArgs(
|
|
[
|
|
'runtime',
|
|
'providers',
|
|
'connect-api-key',
|
|
'--runtime',
|
|
input.runtimeId,
|
|
'--provider',
|
|
input.providerId,
|
|
'--stdin-key',
|
|
'--json',
|
|
],
|
|
projectPath
|
|
);
|
|
const context = createCommandContext(binaryPath, args, projectPath);
|
|
const misconfigured = rejectWrongRuntimeBinary<RuntimeProviderManagementProviderResponse>(
|
|
input.runtimeId,
|
|
context
|
|
);
|
|
if (misconfigured) {
|
|
return misconfigured;
|
|
}
|
|
try {
|
|
const child = spawnCli(
|
|
binaryPath,
|
|
args,
|
|
runtimeProviderCommandOptions(
|
|
{
|
|
env,
|
|
stdio: 'pipe' as const,
|
|
},
|
|
projectPath
|
|
)
|
|
) as ChildProcessWithoutNullStreams;
|
|
const result = await collectSpawnOutput(child, input.apiKey);
|
|
if (result.code === 0) {
|
|
return extractJsonObjectWithContext<RuntimeProviderManagementProviderResponse>(
|
|
result.stdout,
|
|
context,
|
|
mergeSpawnStderrWithStdinError(result)
|
|
);
|
|
}
|
|
|
|
try {
|
|
return sanitizeRuntimeProviderResponse(
|
|
extractJsonObject<RuntimeProviderManagementProviderResponse>(result.stdout)
|
|
);
|
|
} catch {
|
|
return commandFailureResponse<RuntimeProviderManagementProviderResponse>(
|
|
input.runtimeId,
|
|
formatNonJsonCliOutputError({
|
|
context,
|
|
stdout: result.stdout,
|
|
stderr: mergeSpawnStderrWithStdinError(result),
|
|
exitCode: result.code,
|
|
})
|
|
);
|
|
}
|
|
} catch (error) {
|
|
const response = extractJsonObjectFromError<RuntimeProviderManagementProviderResponse>(error);
|
|
if (response) {
|
|
return response;
|
|
}
|
|
return commandFailureResponse<RuntimeProviderManagementProviderResponse>(
|
|
input.runtimeId,
|
|
normalizeCommandFailure(error, context)
|
|
);
|
|
}
|
|
}
|
|
|
|
async forgetCredential(
|
|
input: RuntimeProviderManagementForgetInput
|
|
): Promise<RuntimeProviderManagementProviderResponse> {
|
|
const projectPath = normalizeProjectPath(input.projectPath);
|
|
const { binaryPath, env } = await resolveCliEnv();
|
|
if (!binaryPath) {
|
|
return missingRuntimeBinaryResponse<RuntimeProviderManagementProviderResponse>(
|
|
input.runtimeId,
|
|
projectPath
|
|
);
|
|
}
|
|
|
|
const args = appendProjectPathArgs(
|
|
[
|
|
'runtime',
|
|
'providers',
|
|
'forget',
|
|
'--runtime',
|
|
input.runtimeId,
|
|
'--provider',
|
|
input.providerId,
|
|
'--json',
|
|
],
|
|
projectPath
|
|
);
|
|
const context = createCommandContext(binaryPath, args, projectPath);
|
|
const misconfigured = rejectWrongRuntimeBinary<RuntimeProviderManagementProviderResponse>(
|
|
input.runtimeId,
|
|
context
|
|
);
|
|
if (misconfigured) {
|
|
return misconfigured;
|
|
}
|
|
try {
|
|
const { stdout, stderr } = await execCli(
|
|
binaryPath,
|
|
args,
|
|
runtimeProviderCommandOptions({ env, timeout: COMMAND_TIMEOUT_MS }, projectPath)
|
|
);
|
|
return extractJsonObjectWithContext<RuntimeProviderManagementProviderResponse>(
|
|
stdout,
|
|
context,
|
|
stderr
|
|
);
|
|
} catch (error) {
|
|
const response = extractJsonObjectFromError<RuntimeProviderManagementProviderResponse>(error);
|
|
if (response) {
|
|
return response;
|
|
}
|
|
return commandFailureResponse<RuntimeProviderManagementProviderResponse>(
|
|
input.runtimeId,
|
|
normalizeCommandFailure(error, context)
|
|
);
|
|
}
|
|
}
|
|
|
|
async loadModels(
|
|
input: RuntimeProviderManagementLoadModelsInput
|
|
): Promise<RuntimeProviderManagementModelsResponse> {
|
|
const projectPath = normalizeProjectPath(input.projectPath);
|
|
const { binaryPath, env } = await resolveCliEnv();
|
|
if (!binaryPath) {
|
|
return missingRuntimeBinaryResponse<RuntimeProviderManagementModelsResponse>(
|
|
input.runtimeId,
|
|
projectPath
|
|
);
|
|
}
|
|
|
|
let args = [
|
|
'runtime',
|
|
'providers',
|
|
'models',
|
|
'--runtime',
|
|
input.runtimeId,
|
|
'--provider',
|
|
input.providerId,
|
|
'--json',
|
|
];
|
|
if (input.query?.trim()) {
|
|
args.push('--query', input.query.trim());
|
|
}
|
|
if (typeof input.limit === 'number' && Number.isFinite(input.limit) && input.limit > 0) {
|
|
args.push('--limit', String(Math.floor(input.limit)));
|
|
}
|
|
args = appendProjectPathArgs(args, projectPath);
|
|
const context = createCommandContext(binaryPath, args, projectPath);
|
|
const misconfigured = rejectWrongRuntimeBinary<RuntimeProviderManagementModelsResponse>(
|
|
input.runtimeId,
|
|
context
|
|
);
|
|
if (misconfigured) {
|
|
return misconfigured;
|
|
}
|
|
|
|
try {
|
|
const { stdout, stderr } = await execCli(binaryPath, args, {
|
|
...runtimeProviderCommandOptions({ env }, projectPath),
|
|
timeout: COMMAND_TIMEOUT_MS,
|
|
});
|
|
return extractJsonObjectWithContext<RuntimeProviderManagementModelsResponse>(
|
|
stdout,
|
|
context,
|
|
stderr
|
|
);
|
|
} catch (error) {
|
|
const response = extractJsonObjectFromError<RuntimeProviderManagementModelsResponse>(error);
|
|
if (response) {
|
|
return response;
|
|
}
|
|
return commandFailureResponse<RuntimeProviderManagementModelsResponse>(
|
|
input.runtimeId,
|
|
normalizeCommandFailure(error, context)
|
|
);
|
|
}
|
|
}
|
|
|
|
async testModel(
|
|
input: RuntimeProviderManagementTestModelInput
|
|
): Promise<RuntimeProviderManagementModelTestResponse> {
|
|
const projectPath = normalizeProjectPath(input.projectPath);
|
|
const { binaryPath, env } = await resolveCliEnv();
|
|
if (!binaryPath) {
|
|
return missingRuntimeBinaryResponse<RuntimeProviderManagementModelTestResponse>(
|
|
input.runtimeId,
|
|
projectPath
|
|
);
|
|
}
|
|
|
|
const args = appendProjectPathArgs(
|
|
[
|
|
'runtime',
|
|
'providers',
|
|
'test-model',
|
|
'--runtime',
|
|
input.runtimeId,
|
|
'--provider',
|
|
input.providerId,
|
|
'--model',
|
|
input.modelId,
|
|
'--json',
|
|
],
|
|
projectPath
|
|
);
|
|
const context = createCommandContext(binaryPath, args, projectPath);
|
|
const misconfigured = rejectWrongRuntimeBinary<RuntimeProviderManagementModelTestResponse>(
|
|
input.runtimeId,
|
|
context
|
|
);
|
|
if (misconfigured) {
|
|
return misconfigured;
|
|
}
|
|
try {
|
|
const { stdout, stderr } = await execCli(
|
|
binaryPath,
|
|
args,
|
|
runtimeProviderCommandOptions({ env, timeout: PROBE_COMMAND_TIMEOUT_MS }, projectPath)
|
|
);
|
|
return extractJsonObjectWithContext<RuntimeProviderManagementModelTestResponse>(
|
|
stdout,
|
|
context,
|
|
stderr
|
|
);
|
|
} catch (error) {
|
|
const response =
|
|
extractJsonObjectFromError<RuntimeProviderManagementModelTestResponse>(error);
|
|
if (response) {
|
|
return response;
|
|
}
|
|
return commandFailureResponse<RuntimeProviderManagementModelTestResponse>(
|
|
input.runtimeId,
|
|
normalizeCommandFailure(error, context),
|
|
'model-test-failed'
|
|
);
|
|
}
|
|
}
|
|
|
|
async setDefaultModel(
|
|
input: RuntimeProviderManagementSetDefaultModelInput
|
|
): Promise<RuntimeProviderManagementViewResponse> {
|
|
const projectPath = normalizeProjectPath(input.projectPath);
|
|
const { binaryPath, env } = await resolveCliEnv();
|
|
if (!binaryPath) {
|
|
return missingRuntimeBinaryResponse<RuntimeProviderManagementViewResponse>(
|
|
input.runtimeId,
|
|
projectPath
|
|
);
|
|
}
|
|
|
|
const args = appendProjectPathArgs(
|
|
[
|
|
'runtime',
|
|
'providers',
|
|
'set-default',
|
|
'--runtime',
|
|
input.runtimeId,
|
|
'--provider',
|
|
input.providerId,
|
|
'--model',
|
|
input.modelId,
|
|
'--scope',
|
|
input.scope === 'all_projects' ? 'all-projects' : 'project',
|
|
'--probe',
|
|
'--compact',
|
|
'--json',
|
|
],
|
|
projectPath
|
|
);
|
|
const context = createCommandContext(binaryPath, args, projectPath);
|
|
const misconfigured = rejectWrongRuntimeBinary<RuntimeProviderManagementViewResponse>(
|
|
input.runtimeId,
|
|
context
|
|
);
|
|
if (misconfigured) {
|
|
return misconfigured;
|
|
}
|
|
try {
|
|
const { stdout, stderr } = await execCli(
|
|
binaryPath,
|
|
args,
|
|
runtimeProviderCommandOptions({ env, timeout: PROBE_COMMAND_TIMEOUT_MS }, projectPath)
|
|
);
|
|
return extractJsonObjectWithContext<RuntimeProviderManagementViewResponse>(
|
|
stdout,
|
|
context,
|
|
stderr
|
|
);
|
|
} catch (error) {
|
|
const response = extractJsonObjectFromError<RuntimeProviderManagementViewResponse>(error);
|
|
if (response) {
|
|
return response;
|
|
}
|
|
return commandFailureResponse<RuntimeProviderManagementViewResponse>(
|
|
input.runtimeId,
|
|
normalizeCommandFailure(error, context),
|
|
'model-test-failed'
|
|
);
|
|
}
|
|
}
|
|
}
|