Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
"@effect/platform-node-shared": "catalog:",
"@effect/sql-sqlite-bun": "catalog:",
"@ff-labs/fff-node": "0.9.4",
"@opencode-ai/sdk": "^1.3.15",
"@pierre/diffs": "catalog:",
"effect": "catalog:",
"msgpackr-extract": "3.0.4",
Expand Down
112 changes: 35 additions & 77 deletions apps/server/src/provider/Drivers/OpenCodeDriver.ts
Original file line number Diff line number Diff line change
@@ -1,93 +1,60 @@
/**
* OpenCodeDriver — `ProviderDriver` for the OpenCode runtime.
*
* Mirrors the Codex / Claude drivers: a plain value whose `create()`
* bundles `snapshot` / `adapter` / `textGeneration` closures over the
* per-instance `OpenCodeSettings`.
*
* Two instances with different `serverUrl`s therefore talk to independent
* OpenCode servers; when no `serverUrl` is set, the adapter + text-generation
* shares spin up their own scoped child processes, and those child
* processes are released when the registry scope closes.
*
* @module provider/Drivers/OpenCodeDriver
*/
import { OpenCodeSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";
import * as Crypto from "effect/Crypto";
import type * as Crypto from "effect/Crypto";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import type * as FileSystem from "effect/FileSystem";
import type * as Path from "effect/Path";
import * as Schema from "effect/Schema";
import { HttpClient } from "effect/unstable/http";
import { ChildProcessSpawner } from "effect/unstable/process";

import { makeOpenCodeTextGeneration } from "../../textGeneration/OpenCodeTextGeneration.ts";
import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";
import { ServerConfig } from "../../config.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { makeOpenCodeTextGeneration } from "../../textGeneration/OpenCodeTextGeneration.ts";
import { ProviderDriverError } from "../Errors.ts";
import { makeOpenCodeAdapter } from "../Layers/OpenCodeAdapter.ts";
import {
buildInitialOpenCodeProviderSnapshot,
checkOpenCodeProviderStatus,
makePendingOpenCodeProvider,
} from "../Layers/OpenCodeProvider.ts";
import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
import { OpenCodeRuntime } from "../opencodeRuntime.ts";
import * as OpenCodeRuntime from "../opencodeRuntime.ts";
import {
defaultProviderContinuationIdentity,
type ProviderDriver,
type ProviderInstance,
} from "../ProviderDriver.ts";
import type { ServerProviderDraft } from "../providerSnapshot.ts";
import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
import {
enrichProviderSnapshotWithVersionAdvisory,
makePackageManagedProviderMaintenanceResolver,
normalizeCommandPath,
resolveProviderMaintenanceCapabilitiesEffect,
} from "../providerMaintenance.ts";
import { makeManualOnlyProviderMaintenanceCapabilities } from "../providerMaintenance.ts";
import {
haveProviderSnapshotSettingsChanged,
makeProviderSnapshotSettingsSource,
type ProviderSnapshotSettings,
} from "../providerUpdateSettings.ts";
const decodeOpenCodeSettings = Schema.decodeSync(OpenCodeSettings);

const decodeOpenCodeSettings = Schema.decodeSync(OpenCodeSettings);
const DRIVER_KIND = ProviderDriverKind.make("opencode");

function isOpenCodeNativeCommandPath(commandPath: string): boolean {
const normalized = normalizeCommandPath(commandPath);
return (
normalized.endsWith("/.opencode/bin/opencode") ||
normalized.endsWith("/.opencode/bin/opencode.exe")
);
}

const UPDATE = makePackageManagedProviderMaintenanceResolver({
const MAINTENANCE = makeManualOnlyProviderMaintenanceCapabilities({
provider: DRIVER_KIND,
npmPackageName: "opencode-ai",
homebrewFormula: "anomalyco/tap/opencode",
nativeUpdate: {
executable: "opencode",
args: ["upgrade"],
lockKey: "opencode-native",
isCommandPath: isOpenCodeNativeCommandPath,
},
packageName: null,
});

export type OpenCodeDriverEnv =
| BackgroundPolicy.BackgroundPolicy
| ChildProcessSpawner.ChildProcessSpawner
| Crypto.Crypto
| FileSystem.FileSystem
| HttpClient.HttpClient
| OpenCodeRuntime
| OpenCodeRuntime.OpenCodeRuntime
| Path.Path
| ProviderEventLoggers
| ServerConfig
| ServerSettingsService;

function nativeBinaryPath(binaryPath: string): string {
return /^opencode(?:\.exe)?$/iu.test(binaryPath)
? binaryPath.replace(/opencode(?=(?:\.exe)?$)/iu, "opencode2")
: binaryPath;
}

const withInstanceIdentity =
(input: {
readonly instanceId: ProviderInstance["instanceId"];
Expand All @@ -108,15 +75,13 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>
driverKind: DRIVER_KIND,
metadata: {
displayName: "OpenCode",
supportsMultipleInstances: true,
supportsMultipleInstances: false,
},
configSchema: OpenCodeSettings,
defaultConfig: (): OpenCodeSettings => decodeOpenCodeSettings({}),
create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
Effect.gen(function* () {
const openCodeRuntime = yield* OpenCodeRuntime;
const serverConfig = yield* ServerConfig;
const httpClient = yield* HttpClient.HttpClient;
const runtime = yield* OpenCodeRuntime.OpenCodeRuntime;
const serverSettings = yield* ServerSettingsService;
const eventLoggers = yield* ProviderEventLoggers;
const processEnv = mergeProviderInstanceEnvironment(environment);
Expand All @@ -130,50 +95,43 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>
accentColor,
continuationGroupKey: continuationIdentity.continuationKey,
});
const effectiveConfig = { ...config, enabled } satisfies OpenCodeSettings;
const maintenanceCapabilities = yield* resolveProviderMaintenanceCapabilitiesEffect(UPDATE, {
binaryPath: effectiveConfig.binaryPath,
env: processEnv,
});
const effectiveConfig = {
...config,
enabled,
binaryPath: nativeBinaryPath(config.binaryPath),
} satisfies OpenCodeSettings;

const adapter = yield* makeOpenCodeAdapter(effectiveConfig, {
instanceId,
environment: processEnv,
...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}),
});
const textGeneration = yield* makeOpenCodeTextGeneration(effectiveConfig, processEnv);

const checkProvider = checkOpenCodeProviderStatus(
effectiveConfig,
serverConfig.cwd,
processEnv,
).pipe(Effect.map(stampIdentity), Effect.provideService(OpenCodeRuntime, openCodeRuntime));
}).pipe(Effect.provideService(OpenCodeRuntime.OpenCodeRuntime, runtime));
const textGeneration = yield* makeOpenCodeTextGeneration(effectiveConfig, processEnv).pipe(
Effect.provideService(OpenCodeRuntime.OpenCodeRuntime, runtime),
);

const checkProvider = checkOpenCodeProviderStatus(effectiveConfig, processEnv).pipe(
Effect.map(stampIdentity),
Effect.provideService(OpenCodeRuntime.OpenCodeRuntime, runtime),
);
const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
const snapshot = yield* makeManagedServerProvider<ProviderSnapshotSettings<OpenCodeSettings>>(
{
maintenanceCapabilities,
maintenanceCapabilities: MAINTENANCE,
getSettings: snapshotSettings.getSettings,
streamSettings: snapshotSettings.streamSettings,
haveSettingsChanged: haveProviderSnapshotSettingsChanged,
initialSnapshot: (settings) =>
makePendingOpenCodeProvider(settings.provider).pipe(Effect.map(stampIdentity)),
buildInitialOpenCodeProviderSnapshot(settings.provider).pipe(Effect.map(stampIdentity)),
checkProvider,
enrichSnapshot: ({ settings, snapshot, publishSnapshot }) =>
enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities, {
enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
}).pipe(
Effect.provideService(HttpClient.HttpClient, httpClient),
Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)),
),
},
).pipe(
Effect.mapError(
(cause) =>
new ProviderDriverError({
driver: DRIVER_KIND,
instanceId,
detail: `Failed to build OpenCode snapshot: ${cause.message ?? String(cause)}`,
detail: "Failed to build the OpenCode provider snapshot.",
cause,
}),
),
Expand Down
Loading
Loading