497 lines · 15.0 KB · TypeScript
RawDownload
import {  ProviderDriverKind,  type ModelCapabilities,  type OpenCodeSettings,  type ServerProviderModel,} from "@t3tools/contracts";import * as Cause from "effect/Cause";import * as DateTime from "effect/DateTime";import * as Effect from "effect/Effect";import * as Schema from "effect/Schema"; import { createModelCapabilities } from "@t3tools/shared/model";import { compareSemverVersions } from "@t3tools/shared/semver";import {  buildServerProvider,  nonEmptyTrimmed,  parseGenericCliVersion,  providerModelsFromSettings,  type ServerProviderDraft,} from "../providerSnapshot.ts";import * as OpenCodeRuntime from "../opencodeRuntime.ts";import type { Agent, ProviderListResponse } from "@opencode-ai/sdk/v2"; const PROVIDER = ProviderDriverKind.make("opencode");const OPENCODE_PRESENTATION = {  displayName: "OpenCode",  showInteractionModeToggle: false,} as const;const MINIMUM_OPENCODE_VERSION = "1.14.19"; export class OpenCodeProbeError extends Schema.TaggedErrorClass<OpenCodeProbeError>()(  "OpenCodeProbeError",  {    operation: Schema.Literals(["probe-version", "load-inventory"]),    detail: Schema.String,    cause: Schema.Defect(),  },) {  override get message(): string {    return `OpenCode probe failed during ${this.operation}.`;  }   static fromCause(operation: "probe-version" | "load-inventory") {    return (cause: unknown): OpenCodeProbeError =>      new OpenCodeProbeError({        operation,        detail: OpenCodeRuntime.OpenCodeRuntimeError.detailFromCause(cause),        cause,      });  }} export const isOpenCodeProbeError = Schema.is(OpenCodeProbeError); function normalizeProbeMessage(message: string): string | undefined {  const trimmed = message.trim();  if (trimmed.length === 0) {    return undefined;  }  if (    trimmed === "An error occurred in Effect.tryPromise" ||    trimmed === "An error occurred in Effect.try"  ) {    return undefined;  }  return trimmed;} function normalizedErrorMessage(cause: unknown): string | undefined {  if (isOpenCodeProbeError(cause)) {    return normalizeProbeMessage(cause.detail);  }   if (!(cause instanceof Error)) {    return undefined;  }   return normalizeProbeMessage(cause.message);} function openCodeServerDisplayTarget(input: string): string | undefined {  try {    const url = new URL(input);    return `${url.protocol}//${url.host}`;  } catch {    return undefined;  }} function formatOpenCodeProbeError(input: {  readonly cause: unknown;  readonly isExternalServer: boolean;  readonly serverUrl: string;}): { readonly installed: boolean; readonly message: string } {  const detail = normalizedErrorMessage(input.cause);  const lower = detail?.toLowerCase() ?? "";   if (input.isExternalServer) {    if (      lower.includes("401") ||      lower.includes("403") ||      lower.includes("unauthorized") ||      lower.includes("forbidden")    ) {      return {        installed: true,        message: "OpenCode server rejected authentication. Check the server URL and password.",      };    }     if (      lower.includes("econnrefused") ||      lower.includes("enotfound") ||      lower.includes("fetch failed") ||      lower.includes("networkerror") ||      lower.includes("timed out") ||      lower.includes("timeout") ||      lower.includes("socket hang up")    ) {      const serverTarget = openCodeServerDisplayTarget(input.serverUrl);      return {        installed: true,        message:          serverTarget === undefined            ? "Couldn't reach the configured OpenCode server. Check that the server is running and the URL is correct."            : `Couldn't reach the configured OpenCode server at ${serverTarget}. Check that the server is running and the URL is correct.`,      };    }     return {      installed: true,      message: detail ?? "Failed to connect to the configured OpenCode server.",    };  }   if (lower.includes("enoent") || lower.includes("notfound")) {    return {      installed: false,      message: "OpenCode CLI (`opencode`) is not installed or not on PATH.",    };  }   if (lower.includes("quarantine")) {    return {      installed: true,      message:        "macOS is blocking the OpenCode binary (quarantine). Run `xattr -d com.apple.quarantine $(which opencode)` to fix this.",    };  }   if (lower.includes("invalid code signature") || lower.includes("corrupted")) {    return {      installed: true,      message:        "macOS killed the OpenCode process due to an invalid code signature. The binary may be corrupted — try reinstalling OpenCode.",    };  }   return {    installed: true,    message: detail      ? `Failed to execute OpenCode CLI health check: ${detail}`      : "Failed to execute OpenCode CLI health check.",  };} function titleCaseSlug(value: string): string {  const segments: Array<string> = [];  for (const segment of value.split(/[-_/]+/)) {    if (segment.length > 0) {      segments.push(segment.charAt(0).toUpperCase() + segment.slice(1));    }  }  return segments.join(" ");} function inferDefaultVariant(  providerID: string,  variants: ReadonlyArray<string>,): string | undefined {  if (variants.length === 1) {    return variants[0];  }  if (providerID === "anthropic" || providerID.startsWith("google")) {    return variants.includes("high") ? "high" : undefined;  }  if (providerID === "openai" || providerID === "opencode") {    return variants.includes("medium") ? "medium" : variants.includes("high") ? "high" : undefined;  }  return undefined;} function inferDefaultAgent(agents: ReadonlyArray<Agent>): string | undefined {  return agents.find((agent) => agent.name === "build")?.name ?? agents[0]?.name ?? undefined;} const DEFAULT_OPENCODE_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabilities({  optionDescriptors: [],}); function openCodeCapabilitiesForModel(input: {  readonly providerID: string;  readonly model: ProviderListResponse["all"][number]["models"][string];  readonly agents: ReadonlyArray<Agent>;}): ModelCapabilities {  const variantValues = Object.keys(input.model.variants ?? {});  const defaultVariant = inferDefaultVariant(input.providerID, variantValues);  const variantOptions = variantValues.map((value) =>    defaultVariant === value      ? { id: value, label: titleCaseSlug(value), isDefault: true as const }      : { id: value, label: titleCaseSlug(value) },  );  const primaryAgents = input.agents.filter(    (agent) => !agent.hidden && (agent.mode === "primary" || agent.mode === "all"),  );  const defaultAgent = inferDefaultAgent(primaryAgents);  const agentOptions = primaryAgents.map((agent) =>    defaultAgent === agent.name      ? { id: agent.name, label: titleCaseSlug(agent.name), isDefault: true as const }      : { id: agent.name, label: titleCaseSlug(agent.name) },  );  return createModelCapabilities({    optionDescriptors: [      ...(variantOptions.length > 0        ? [            {              id: "variant",              label: "Variant",              type: "select" as const,              options: variantOptions,              ...(defaultVariant ? { currentValue: defaultVariant } : {}),            },          ]        : []),      ...(agentOptions.length > 0        ? [            {              id: "agent",              label: "Agent",              type: "select" as const,              options: agentOptions,              ...(defaultAgent ? { currentValue: defaultAgent } : {}),            },          ]        : []),    ],  });} function flattenOpenCodeModels(  input: OpenCodeRuntime.OpenCodeInventory,): ReadonlyArray<ServerProviderModel> {  const connected = new Set(input.providerList.connected);  const models: Array<ServerProviderModel> = [];   for (const provider of input.providerList.all) {    if (!connected.has(provider.id)) {      continue;    }     for (const model of Object.values(provider.models)) {      const name = nonEmptyTrimmed(model.name);      if (!name) {        continue;      }       const subProvider = nonEmptyTrimmed(provider.name);      models.push({        slug: `${provider.id}/${model.id}`,        name,        ...(subProvider ? { subProvider } : {}),        isCustom: false,        capabilities: openCodeCapabilitiesForModel({          providerID: provider.id,          model,          agents: input.agents,        }),      });    }  }   return models.toSorted((left, right) => left.name.localeCompare(right.name));} export const makePendingOpenCodeProvider = (  openCodeSettings: OpenCodeSettings,): Effect.Effect<ServerProviderDraft> =>  Effect.gen(function* () {    const checkedAt = yield* Effect.map(DateTime.now, DateTime.formatIso);    const models = providerModelsFromSettings(      [],      PROVIDER,      openCodeSettings.customModels,      DEFAULT_OPENCODE_MODEL_CAPABILITIES,    );     if (!openCodeSettings.enabled) {      return buildServerProvider({        presentation: OPENCODE_PRESENTATION,        enabled: false,        checkedAt,        models,        probe: {          installed: false,          version: null,          status: "warning",          auth: { status: "unknown" },          message:            openCodeSettings.serverUrl.trim().length > 0              ? "OpenCode is disabled in T3 Code settings. A server URL is configured."              : "OpenCode is disabled in T3 Code settings.",        },      });    }     return buildServerProvider({      presentation: OPENCODE_PRESENTATION,      enabled: true,      checkedAt,      models,      probe: {        installed: false,        version: null,        status: "warning",        auth: { status: "unknown" },        message: "OpenCode provider status has not been checked in this session yet.",      },    });  }); export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatus")(function* (  openCodeSettings: OpenCodeSettings,  cwd: string,  environment?: NodeJS.ProcessEnv,): Effect.fn.Return<ServerProviderDraft, never, OpenCodeRuntime.OpenCodeRuntime> {  const openCodeRuntime = yield* OpenCodeRuntime.OpenCodeRuntime;  const resolvedEnvironment = environment ?? process.env;  const checkedAt = DateTime.formatIso(yield* DateTime.now);  const customModels = openCodeSettings.customModels;  const isExternalServer = openCodeSettings.serverUrl.trim().length > 0;   const buildFailureSnapshot = (    failure: { readonly installed: boolean; readonly message: string },    version: string | null,  ) =>    buildServerProvider({      presentation: OPENCODE_PRESENTATION,      enabled: openCodeSettings.enabled,      checkedAt,      models: providerModelsFromSettings(        [],        PROVIDER,        customModels,        DEFAULT_OPENCODE_MODEL_CAPABILITIES,      ),      probe: {        installed: failure.installed,        version,        status: "error",        auth: { status: "unknown" },        message: failure.message,      },    });   const fallback = (cause: unknown, version: string | null = null) =>    buildFailureSnapshot(      formatOpenCodeProbeError({        cause,        isExternalServer,        serverUrl: openCodeSettings.serverUrl,      }),      version,    );   if (!openCodeSettings.enabled) {    return buildServerProvider({      presentation: OPENCODE_PRESENTATION,      enabled: false,      checkedAt,      models: providerModelsFromSettings(        [],        PROVIDER,        customModels,        DEFAULT_OPENCODE_MODEL_CAPABILITIES,      ),      probe: {        installed: false,        version: null,        status: "warning",        auth: { status: "unknown" },        message: isExternalServer          ? "OpenCode is disabled in T3 Code settings. A server URL is configured."          : "OpenCode is disabled in T3 Code settings.",      },    });  }   let version: string | null = null;  if (!isExternalServer) {    const versionExit = yield* Effect.exit(      openCodeRuntime        .runOpenCodeCommand({          binaryPath: openCodeSettings.binaryPath,          args: ["--version"],          environment: resolvedEnvironment,        })        .pipe(Effect.mapError(OpenCodeProbeError.fromCause("probe-version"))),    );    if (versionExit._tag === "Failure") {      return fallback(Cause.squash(versionExit.cause));    }    version = parseGenericCliVersion(versionExit.value.stdout) ?? null;     if (!version) {      return buildFailureSnapshot(        {          installed: true,          message: `Unable to determine OpenCode version from \`opencode --version\` output. T3 Code requires OpenCode v${MINIMUM_OPENCODE_VERSION} or newer.`,        },        null,      );    }    if (compareSemverVersions(version, MINIMUM_OPENCODE_VERSION) < 0) {      return buildServerProvider({        presentation: OPENCODE_PRESENTATION,        enabled: openCodeSettings.enabled,        checkedAt,        models: providerModelsFromSettings(          [],          PROVIDER,          customModels,          DEFAULT_OPENCODE_MODEL_CAPABILITIES,        ),        probe: {          installed: true,          version,          status: "error",          auth: { status: "unknown" },          message: `OpenCode v${version} is too old. Upgrade to v${MINIMUM_OPENCODE_VERSION} or newer.`,        },      });    }  }   const inventoryExit = yield* Effect.exit(    Effect.scoped(      Effect.gen(function* () {        const server = yield* openCodeRuntime.connectToOpenCodeServer({          binaryPath: openCodeSettings.binaryPath,          serverUrl: openCodeSettings.serverUrl,          environment: resolvedEnvironment,        });        return yield* openCodeRuntime.loadOpenCodeInventory(          openCodeRuntime.createOpenCodeSdkClient({            baseUrl: server.url,            directory: cwd,            ...(isExternalServer && openCodeSettings.serverPassword              ? { serverPassword: openCodeSettings.serverPassword }              : {}),          }),        );      }).pipe(Effect.mapError(OpenCodeProbeError.fromCause("load-inventory"))),    ),  );  if (inventoryExit._tag === "Failure") {    return fallback(Cause.squash(inventoryExit.cause), version);  }   const models = providerModelsFromSettings(    flattenOpenCodeModels(inventoryExit.value),    PROVIDER,    customModels,    DEFAULT_OPENCODE_MODEL_CAPABILITIES,  );  const connectedCount = inventoryExit.value.providerList.connected.length;  return buildServerProvider({    presentation: OPENCODE_PRESENTATION,    enabled: true,    checkedAt,    models,    probe: {      installed: true,      version,      status: connectedCount > 0 ? "ready" : "warning",      auth: {        status: connectedCount > 0 ? "authenticated" : "unknown",        type: "opencode",      },      message:        connectedCount > 0          ? `${connectedCount} upstream provider${connectedCount === 1 ? "" : "s"} connected through ${isExternalServer ? "the configured OpenCode server" : "OpenCode"}.`          : isExternalServer            ? "Connected to the configured OpenCode server, but it did not report any connected upstream providers."            : "OpenCode is available, but it did not report any connected upstream providers.",    },  });});