| 1 | /** |
| 2 | * OpenCodeDriver — `ProviderDriver` for the OpenCode runtime. |
| 3 | * |
| 4 | * Mirrors the Codex / Claude drivers: a plain value whose `create()` |
| 5 | * bundles `snapshot` / `adapter` / `textGeneration` closures over the |
| 6 | * per-instance `OpenCodeSettings`. |
| 7 | * |
| 8 | * Two instances with different `serverUrl`s therefore talk to independent |
| 9 | * OpenCode servers; when no `serverUrl` is set, the adapter + text-generation |
| 10 | * shares spin up their own scoped child processes, and those child |
| 11 | * processes are released when the registry scope closes. |
| 12 | * |
| 13 | * @module provider/Drivers/OpenCodeDriver |
| 14 | */ |
| 15 | import { ProviderDriverKind } from "@t3tools/contracts"; |
| 16 | import { OpenCodeSettings } from "../settings.ts"; |
| 17 | import * as Crypto from "effect/Crypto"; |
| 18 | import * as Deferred from "effect/Deferred"; |
| 19 | import * as Effect from "effect/Effect"; |
| 20 | import * as Option from "effect/Option"; |
| 21 | import * as FileSystem from "effect/FileSystem"; |
| 22 | import * as Path from "effect/Path"; |
| 23 | import * as Schema from "effect/Schema"; |
| 24 | import * as Stream from "effect/Stream"; |
| 25 | import * as HttpClient from "effect/http/HttpClient"; |
| 26 | import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; |
| 27 | |
| 28 | import * as OpenCode2TextGeneration from "./v2/textGeneration.ts"; |
| 29 | import { makeOpenCodeTextGeneration } from "./textGeneration.ts"; |
| 30 | import * as ProviderLatestVersions from "@t3tools/provider-core/server/ProviderLatestVersions"; |
| 31 | import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost"; |
| 32 | import * as OpenCodeAdapterV2 from "./adapter.ts"; |
| 33 | import * as OpenCode2AdapterV2 from "./v2/adapter.ts"; |
| 34 | import type * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter"; |
| 35 | import type { ProviderTextGeneration } from "@t3tools/provider-core/server/textGeneration"; |
| 36 | import { ProviderDriverError } from "@t3tools/provider-core/server/errors"; |
| 37 | import { openCodeUsageReader, type OpenCodeUsageReaderEnv } from "./usage.ts"; |
| 38 | import { readOpenCodeGoUsageLimits } from "./usageLimits.ts"; |
| 39 | import { loadOpenCode2Catalog } from "./openCode2Catalog.ts"; |
| 40 | import { |
| 41 | checkOpenCodeProviderStatus, |
| 42 | loadOpenCode2Workspace, |
| 43 | makeOpenCode2ModelLoader, |
| 44 | makePendingOpenCodeProvider, |
| 45 | openCode2CommandsToServerProviderSlashCommands, |
| 46 | openCode2SkillsToServerProviderSkills, |
| 47 | openCodeSkillsToServerProviderSkills, |
| 48 | openCodeCommandsToServerProviderSlashCommands, |
| 49 | } from "./status.ts"; |
| 50 | import { makeManagedServerProvider } from "@t3tools/provider-core/server/managedProvider"; |
| 51 | import * as OpenCodeRuntime from "./OpenCodeRuntime.ts"; |
| 52 | import { |
| 53 | makeOpenCodeRuntimeProbe, |
| 54 | probeOpenCodeRuntime, |
| 55 | type ProbedOpenCode, |
| 56 | } from "./versionProbe.ts"; |
| 57 | import * as OpenCodeServerOwner from "./OpenCodeServerOwner.ts"; |
| 58 | import * as OpenCode2Client from "./v2/OpenCode2Client.ts"; |
| 59 | import * as OpenCode2Server from "./v2/OpenCode2Server.ts"; |
| 60 | import { |
| 61 | defaultProviderContinuationIdentity, |
| 62 | type ProviderDriver, |
| 63 | type ProviderInstance, |
| 64 | } from "@t3tools/provider-core/server/driver"; |
| 65 | import { withInstanceIdentity } from "@t3tools/provider-core/server/instanceIdentity"; |
| 66 | import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment"; |
| 67 | import { |
| 68 | enrichProviderSnapshotWithVersionAdvisory, |
| 69 | makeCachedProviderMaintenanceResolution, |
| 70 | makeManualOnlyProviderMaintenanceCapabilities, |
| 71 | makePackageManagedProviderMaintenanceResolver, |
| 72 | normalizeCommandPath, |
| 73 | resolveProviderMaintenanceCapabilitiesEffect, |
| 74 | } from "@t3tools/provider-core/server/maintenanceResolver"; |
| 75 | import { |
| 76 | haveProviderSnapshotSettingsChanged, |
| 77 | makeProviderSnapshotSettingsSource, |
| 78 | type ProviderSnapshotSettings, |
| 79 | } from "@t3tools/provider-core/server/snapshotSettings"; |
| 80 | const decodeOpenCodeSettings = Schema.decodeSync(OpenCodeSettings); |
| 81 | |
| 82 | const DRIVER_KIND = ProviderDriverKind.make("opencode"); |
| 83 | |
| 84 | function isOpenCodeNativeCommandPath(commandPath: string): boolean { |
| 85 | const normalized = normalizeCommandPath(commandPath); |
| 86 | return ( |
| 87 | normalized.endsWith("/.opencode/bin/opencode") || |
| 88 | normalized.endsWith("/.opencode/bin/opencode.exe") |
| 89 | ); |
| 90 | } |
| 91 | |
| 92 | /** |
| 93 | * OpenCode 1.x ships as `opencode-ai` and 2.x as `@opencode/cli`. An install is |
| 94 | * only ever updated within its own package: T3 never moves a 1.x install onto |
| 95 | * 2.x or back, since 2.x converts the shared database in place. |
| 96 | */ |
| 97 | export const openCodeUpdateFor = (generation: ProbedOpenCode["generation"]) => |
| 98 | makePackageManagedProviderMaintenanceResolver({ |
| 99 | provider: DRIVER_KIND, |
| 100 | npmPackageName: generation === "v2" ? "@opencode/cli" : "opencode-ai", |
| 101 | nativeUpdate: { |
| 102 | args: ["upgrade"], |
| 103 | isCommandPath: isOpenCodeNativeCommandPath, |
| 104 | }, |
| 105 | }); |
| 106 | |
| 107 | type OpenCodeRuntimeProbe = Effect.Success< |
| 108 | ReturnType<typeof makeOpenCodeRuntimeProbe<OpenCodeRuntime.OpenCodeRuntimeError>> |
| 109 | >; |
| 110 | |
| 111 | /** Runs `v2` for a 2.x instance and `v1` otherwise; a failed probe keeps 1.x, whose checks report it. */ |
| 112 | function byOpenCodeRuntime<A, E, R, PE>( |
| 113 | probed: Effect.Effect<ProbedOpenCode | undefined, PE>, |
| 114 | paths: { readonly v1: Effect.Effect<A, E, R>; readonly v2: Effect.Effect<A, E, R> }, |
| 115 | ): Effect.Effect<A, E, R> { |
| 116 | return probed.pipe( |
| 117 | Effect.orElseSucceed(() => undefined), |
| 118 | Effect.flatMap((result) => (result?.generation === "v2" ? paths.v2 : paths.v1)), |
| 119 | ); |
| 120 | } |
| 121 | |
| 122 | /** |
| 123 | * Routes each adapter call to the runtime the instance's probe detected. Capability and selection |
| 124 | * reads are hot, so they use the last successful probe (1.x before one lands) and never wait on a |
| 125 | * slow server. Opening a session waits for a probe, so each server is spoken to in its own protocol. |
| 126 | */ |
| 127 | function selectOpenCodeRuntimeAdapter(input: { |
| 128 | readonly probe: OpenCodeRuntimeProbe; |
| 129 | readonly v1: ProviderAdapter.ProviderAdapterV2["Service"]; |
| 130 | readonly v2: ProviderAdapter.ProviderAdapterV2["Service"]; |
| 131 | }): ProviderAdapter.ProviderAdapterV2["Service"] { |
| 132 | const pick = <PE>(probed: Effect.Effect<ProbedOpenCode | undefined, PE>) => |
| 133 | byOpenCodeRuntime(probed, { v1: Effect.succeed(input.v1), v2: Effect.succeed(input.v2) }); |
| 134 | const hot = pick(Effect.map(input.probe.lastSuccess, Option.getOrUndefined)); |
| 135 | return { |
| 136 | instanceId: input.v1.instanceId, |
| 137 | driver: DRIVER_KIND, |
| 138 | getCapabilities: () => Effect.flatMap(hot, (adapter) => adapter.getCapabilities()), |
| 139 | planSelectionTransition: (transition) => |
| 140 | Effect.flatMap(hot, (adapter) => adapter.planSelectionTransition(transition)), |
| 141 | openSession: (session) => |
| 142 | Effect.flatMap(pick(input.probe.get), (adapter) => adapter.openSession(session)), |
| 143 | }; |
| 144 | } |
| 145 | |
| 146 | /** Text generation runs on the server the instance's probe detected, each in its own protocol. */ |
| 147 | function selectOpenCodeRuntimeTextGeneration( |
| 148 | probe: OpenCodeRuntimeProbe, |
| 149 | v1: ProviderTextGeneration, |
| 150 | v2: ProviderTextGeneration, |
| 151 | ): ProviderTextGeneration { |
| 152 | return { |
| 153 | generateCommitMessage: (input) => |
| 154 | byOpenCodeRuntime(probe.get, { |
| 155 | v1: v1.generateCommitMessage(input), |
| 156 | v2: v2.generateCommitMessage(input), |
| 157 | }), |
| 158 | generatePrContent: (input) => |
| 159 | byOpenCodeRuntime(probe.get, { |
| 160 | v1: v1.generatePrContent(input), |
| 161 | v2: v2.generatePrContent(input), |
| 162 | }), |
| 163 | generateBranchName: (input) => |
| 164 | byOpenCodeRuntime(probe.get, { |
| 165 | v1: v1.generateBranchName(input), |
| 166 | v2: v2.generateBranchName(input), |
| 167 | }), |
| 168 | generateThreadTitle: (input) => |
| 169 | byOpenCodeRuntime(probe.get, { |
| 170 | v1: v1.generateThreadTitle(input), |
| 171 | v2: v2.generateThreadTitle(input), |
| 172 | }), |
| 173 | generateReviewGuide: (input) => |
| 174 | byOpenCodeRuntime(probe.get, { |
| 175 | v1: v1.generateReviewGuide(input), |
| 176 | v2: v2.generateReviewGuide(input), |
| 177 | }), |
| 178 | }; |
| 179 | } |
| 180 | |
| 181 | export type OpenCodeDriverEnv = |
| 182 | | OpenCodeAdapterV2.OpenCodeAdapterV2DriverEnv |
| 183 | | ProviderHost.ProviderHost |
| 184 | | ChildProcessSpawner.ChildProcessSpawner |
| 185 | | Crypto.Crypto |
| 186 | | FileSystem.FileSystem |
| 187 | | HttpClient.HttpClient |
| 188 | | ProviderLatestVersions.ProviderLatestVersions |
| 189 | | OpenCodeRuntime.OpenCodeRuntime |
| 190 | | Path.Path; |
| 191 | |
| 192 | export const OpenCodeDriver: ProviderDriver< |
| 193 | OpenCodeSettings, |
| 194 | OpenCodeDriverEnv, |
| 195 | OpenCodeUsageReaderEnv |
| 196 | > = { |
| 197 | driverKind: DRIVER_KIND, |
| 198 | metadata: { |
| 199 | displayName: "OpenCode", |
| 200 | supportsMultipleInstances: true, |
| 201 | }, |
| 202 | configSchema: OpenCodeSettings, |
| 203 | defaultConfig: (): OpenCodeSettings => decodeOpenCodeSettings({}), |
| 204 | usage: openCodeUsageReader, |
| 205 | create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => |
| 206 | Effect.gen(function* () { |
| 207 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 208 | const fileSystem = yield* FileSystem.FileSystem; |
| 209 | const pathService = yield* Path.Path; |
| 210 | const openCodeRuntime = yield* OpenCodeRuntime.OpenCodeRuntime; |
| 211 | const httpClient = yield* HttpClient.HttpClient; |
| 212 | const latestVersions = yield* ProviderLatestVersions.ProviderLatestVersions; |
| 213 | const crypto = yield* Crypto.Crypto; |
| 214 | const host = yield* ProviderHost.ProviderHost; |
| 215 | const processEnv = yield* mergeProviderInstanceEnvironment(environment); |
| 216 | const continuationIdentity = defaultProviderContinuationIdentity({ |
| 217 | driverKind: DRIVER_KIND, |
| 218 | instanceId, |
| 219 | }); |
| 220 | const stampIdentity = withInstanceIdentity({ |
| 221 | instanceId, |
| 222 | driverKind: DRIVER_KIND, |
| 223 | displayName, |
| 224 | accentColor, |
| 225 | continuationGroupKey: continuationIdentity.continuationKey, |
| 226 | }); |
| 227 | const effectiveConfig = { ...config, enabled } satisfies OpenCodeSettings; |
| 228 | const runtimeProbe = yield* makeOpenCodeRuntimeProbe( |
| 229 | probeOpenCodeRuntime(effectiveConfig, processEnv).pipe( |
| 230 | Effect.provideService(HttpClient.HttpClient, httpClient), |
| 231 | Effect.provideService(OpenCodeRuntime.OpenCodeRuntime, openCodeRuntime), |
| 232 | ), |
| 233 | ); |
| 234 | // Updates follow the installed package, which the version probe names. An |
| 235 | // unknown version offers no package update, since a guess could move a |
| 236 | // 2.x install onto 1.x's package or the reverse. A disabled instance never |
| 237 | // runs its binary, so it has no version and offers no update. |
| 238 | const noUpdate = makeManualOnlyProviderMaintenanceCapabilities({ |
| 239 | provider: DRIVER_KIND, |
| 240 | packageName: null, |
| 241 | }); |
| 242 | const maintenanceFor = (probed: typeof runtimeProbe.get) => |
| 243 | probed.pipe( |
| 244 | Effect.map((result) => result.generation), |
| 245 | Effect.option, |
| 246 | Effect.flatMap((generation) => |
| 247 | Option.isNone(generation) |
| 248 | ? Effect.succeed(noUpdate) |
| 249 | : resolveProviderMaintenanceCapabilitiesEffect(openCodeUpdateFor(generation.value), { |
| 250 | binaryPath: effectiveConfig.binaryPath, |
| 251 | env: processEnv, |
| 252 | }), |
| 253 | ), |
| 254 | Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), |
| 255 | Effect.provideService(FileSystem.FileSystem, fileSystem), |
| 256 | Effect.provideService(Path.Path, pathService), |
| 257 | ); |
| 258 | const cachedMaintenance = yield* makeCachedProviderMaintenanceResolution( |
| 259 | maintenanceFor(runtimeProbe.get), |
| 260 | ); |
| 261 | // A fresh read (an update about to run, or a manual refresh) re-probes, so |
| 262 | // a binary replaced by the other major version gets its own package. |
| 263 | const resolveMaintenance = (options?: { readonly fresh?: boolean }) => |
| 264 | !effectiveConfig.enabled |
| 265 | ? Effect.succeed(noUpdate) |
| 266 | : options?.fresh === true |
| 267 | ? runtimeProbe.refresh.pipe( |
| 268 | Effect.ignore, |
| 269 | Effect.andThen(cachedMaintenance({ fresh: true })), |
| 270 | ) |
| 271 | : cachedMaintenance(); |
| 272 | const openCodeV1Adapter = yield* OpenCodeAdapterV2.OpenCodeAdapterV2Driver.create({ |
| 273 | instanceId, |
| 274 | displayName, |
| 275 | accentColor, |
| 276 | environment, |
| 277 | enabled, |
| 278 | config, |
| 279 | }).pipe( |
| 280 | Effect.mapError( |
| 281 | (cause) => |
| 282 | new ProviderDriverError({ |
| 283 | driver: DRIVER_KIND, |
| 284 | instanceId, |
| 285 | detail: "Failed to build OpenCode orchestration adapter.", |
| 286 | cause, |
| 287 | }), |
| 288 | ), |
| 289 | ); |
| 290 | // One OpenCode 2 server per instance, spawned on first use or reached at `serverUrl`. |
| 291 | const openCode2Server = yield* OpenCode2Server.make({ |
| 292 | binaryPath: effectiveConfig.binaryPath, |
| 293 | serverUrl: effectiveConfig.serverUrl, |
| 294 | serverPassword: effectiveConfig.serverPassword, |
| 295 | directory: host.paths.cwd, |
| 296 | environment: processEnv, |
| 297 | }).pipe( |
| 298 | Effect.provideService(OpenCodeRuntime.OpenCodeRuntime, openCodeRuntime), |
| 299 | Effect.provideService( |
| 300 | OpenCode2Client.OpenCode2Client, |
| 301 | yield* OpenCode2Client.make.pipe( |
| 302 | Effect.provideService(HttpClient.HttpClient, httpClient), |
| 303 | ), |
| 304 | ), |
| 305 | ); |
| 306 | const orchestrationAdapter = selectOpenCodeRuntimeAdapter({ |
| 307 | probe: runtimeProbe, |
| 308 | v1: openCodeV1Adapter, |
| 309 | v2: yield* OpenCode2AdapterV2.make(instanceId).pipe( |
| 310 | Effect.provideService(OpenCode2Server.OpenCode2Server, openCode2Server), |
| 311 | ), |
| 312 | }); |
| 313 | const loadOpenCode2Models = yield* makeOpenCode2ModelLoader( |
| 314 | openCode2Server.withConnection(({ client }) => |
| 315 | loadOpenCode2Catalog(client, host.paths.cwd), |
| 316 | ), |
| 317 | ); |
| 318 | // A 2.x server lists skills and commands per directory, so one server |
| 319 | // answers every workspace. Its event stream says when a directory it had |
| 320 | // not served yet finished scanning. |
| 321 | const listOpenCode2Workspace = (cwd: string) => |
| 322 | openCode2Server.withConnection(({ client, events }) => |
| 323 | Effect.gen(function* () { |
| 324 | const location = { directory: cwd }; |
| 325 | const scanned = yield* Deferred.make<void>(); |
| 326 | const pending = new Set(["command.updated", "skill.updated"]); |
| 327 | const stream = yield* events.pipe(Effect.option); |
| 328 | if (stream._tag === "Some") { |
| 329 | yield* stream.value.pipe( |
| 330 | Stream.runForEach((event) => |
| 331 | "location" in event && |
| 332 | event.location?.directory === cwd && |
| 333 | pending.delete(event.type) && |
| 334 | pending.size === 0 |
| 335 | ? Deferred.succeed(scanned, undefined) |
| 336 | : Effect.void, |
| 337 | ), |
| 338 | Effect.ignore, |
| 339 | Effect.forkScoped, |
| 340 | ); |
| 341 | } |
| 342 | return yield* loadOpenCode2Workspace( |
| 343 | Effect.all( |
| 344 | { |
| 345 | skills: client.skill.list({ location }).pipe(Effect.map((list) => list.data)), |
| 346 | commands: client.command.list({ location }).pipe(Effect.map((list) => list.data)), |
| 347 | }, |
| 348 | { concurrency: "unbounded" }, |
| 349 | ), |
| 350 | Deferred.await(scanned), |
| 351 | ); |
| 352 | }).pipe(Effect.scoped), |
| 353 | ); |
| 354 | const serverOwner = yield* OpenCodeServerOwner.make({ |
| 355 | binaryPath: effectiveConfig.binaryPath, |
| 356 | directory: host.paths.cwd, |
| 357 | ...(effectiveConfig.serverPassword |
| 358 | ? { serverPassword: effectiveConfig.serverPassword } |
| 359 | : {}), |
| 360 | environment: processEnv, |
| 361 | }); |
| 362 | const textGeneration = selectOpenCodeRuntimeTextGeneration( |
| 363 | runtimeProbe, |
| 364 | yield* makeOpenCodeTextGeneration(effectiveConfig).pipe( |
| 365 | Effect.provideService(OpenCodeServerOwner.OpenCodeServerOwner, serverOwner), |
| 366 | ), |
| 367 | yield* OpenCode2TextGeneration.make().pipe( |
| 368 | Effect.provideService(OpenCode2Server.OpenCode2Server, openCode2Server), |
| 369 | ), |
| 370 | ); |
| 371 | |
| 372 | const checkProvider = Effect.all( |
| 373 | { |
| 374 | provider: checkOpenCodeProviderStatus( |
| 375 | effectiveConfig, |
| 376 | host.paths.cwd, |
| 377 | runtimeProbe.refresh, |
| 378 | loadOpenCode2Models, |
| 379 | ), |
| 380 | usageLimits: readOpenCodeGoUsageLimits({ |
| 381 | enabled: effectiveConfig.enabled, |
| 382 | serverUrl: effectiveConfig.serverUrl, |
| 383 | environment: processEnv, |
| 384 | }), |
| 385 | }, |
| 386 | { concurrency: "unbounded" }, |
| 387 | ).pipe( |
| 388 | Effect.map(({ provider, usageLimits }) => ({ ...provider, usageLimits })), |
| 389 | Effect.map(stampIdentity), |
| 390 | Effect.provideService(FileSystem.FileSystem, fileSystem), |
| 391 | Effect.provideService(Path.Path, pathService), |
| 392 | Effect.provideService(HttpClient.HttpClient, httpClient), |
| 393 | Effect.provideService(Crypto.Crypto, crypto), |
| 394 | Effect.provideService(OpenCodeServerOwner.OpenCodeServerOwner, serverOwner), |
| 395 | Effect.provideService(OpenCodeRuntime.OpenCodeRuntime, openCodeRuntime), |
| 396 | ); |
| 397 | // NOTE: the local branch intentionally uses the shared SDK server |
| 398 | // instead of `opencode debug skill` (loadSkillsFromCli). The CLI writes |
| 399 | // its full JSON inventory to stdout, but the Bun-compiled binary does |
| 400 | // not flush more than one 64KB pipe buffer to a non-TTY stdout, so the |
| 401 | // piped output arrives truncated and unparseable — which degrades to an |
| 402 | // empty skill list and poisons the workspace snapshot the `$` picker |
| 403 | // reads. The SDK `app.skills` endpoint honors the per-request directory |
| 404 | // and returns complete results regardless of size. |
| 405 | const loadWorkspaceInventory = ( |
| 406 | client: Parameters<typeof OpenCodeRuntime.loadOpenCodeCommands>[0], |
| 407 | ) => |
| 408 | Effect.all( |
| 409 | { |
| 410 | skills: openCodeRuntime.loadOpenCodeSkills(client), |
| 411 | commands: OpenCodeRuntime.loadOpenCodeCommands(client).pipe( |
| 412 | Effect.timeout("10 seconds"), |
| 413 | Effect.orElseSucceed(() => []), |
| 414 | ), |
| 415 | }, |
| 416 | { concurrency: "unbounded" }, |
| 417 | ); |
| 418 | const loadWorkspaceForCwd = (cwd: string) => |
| 419 | effectiveConfig.serverUrl.trim().length > 0 |
| 420 | ? Effect.scoped( |
| 421 | Effect.gen(function* () { |
| 422 | const server = yield* openCodeRuntime.connectToOpenCodeServer({ |
| 423 | binaryPath: effectiveConfig.binaryPath, |
| 424 | directory: cwd, |
| 425 | serverUrl: effectiveConfig.serverUrl, |
| 426 | ...(effectiveConfig.serverPassword |
| 427 | ? { serverPassword: effectiveConfig.serverPassword } |
| 428 | : {}), |
| 429 | environment: processEnv, |
| 430 | }); |
| 431 | const client = openCodeRuntime.createOpenCodeSdkClient({ |
| 432 | baseUrl: server.url, |
| 433 | directory: cwd, |
| 434 | ...(effectiveConfig.serverPassword |
| 435 | ? { serverPassword: effectiveConfig.serverPassword } |
| 436 | : {}), |
| 437 | }); |
| 438 | return yield* loadWorkspaceInventory(client); |
| 439 | }), |
| 440 | ) |
| 441 | : serverOwner.withServer((server) => |
| 442 | loadWorkspaceInventory( |
| 443 | openCodeRuntime.createOpenCodeSdkClient({ |
| 444 | baseUrl: server.url, |
| 445 | directory: cwd, |
| 446 | ...(server.serverPassword !== undefined |
| 447 | ? { serverPassword: server.serverPassword } |
| 448 | : {}), |
| 449 | }), |
| 450 | ), |
| 451 | ); |
| 452 | |
| 453 | const snapshotSettings = yield* makeProviderSnapshotSettingsSource(effectiveConfig); |
| 454 | const snapshot = yield* makeManagedServerProvider<ProviderSnapshotSettings<OpenCodeSettings>>( |
| 455 | { |
| 456 | resolveMaintenance, |
| 457 | getSettings: snapshotSettings.getSettings, |
| 458 | streamSettings: snapshotSettings.streamSettings, |
| 459 | haveSettingsChanged: haveProviderSnapshotSettingsChanged, |
| 460 | checkProviderOnSettingsChange: () => false, |
| 461 | refreshOnInterval: false, |
| 462 | initialSnapshot: (settings) => |
| 463 | makePendingOpenCodeProvider(settings.provider).pipe(Effect.map(stampIdentity)), |
| 464 | checkProvider, |
| 465 | enrichSnapshot: ({ settings, snapshot, publishSnapshot }) => |
| 466 | resolveMaintenance().pipe( |
| 467 | Effect.flatMap((maintenanceCapabilities) => |
| 468 | enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities, { |
| 469 | enableProviderUpdateChecks: settings.enableProviderUpdateChecks, |
| 470 | }), |
| 471 | ), |
| 472 | Effect.provideService(HttpClient.HttpClient, httpClient), |
| 473 | Effect.provideService(ProviderLatestVersions.ProviderLatestVersions, latestVersions), |
| 474 | Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)), |
| 475 | ), |
| 476 | }, |
| 477 | ).pipe( |
| 478 | Effect.mapError( |
| 479 | (cause) => |
| 480 | new ProviderDriverError({ |
| 481 | driver: DRIVER_KIND, |
| 482 | instanceId, |
| 483 | detail: `Failed to build OpenCode snapshot: ${cause.message ?? String(cause)}`, |
| 484 | cause, |
| 485 | }), |
| 486 | ), |
| 487 | ); |
| 488 | |
| 489 | return { |
| 490 | instanceId, |
| 491 | driverKind: DRIVER_KIND, |
| 492 | continuationIdentity, |
| 493 | displayName, |
| 494 | accentColor, |
| 495 | enabled, |
| 496 | snapshot, |
| 497 | snapshotForCwd: (cwd) => |
| 498 | !effectiveConfig.enabled |
| 499 | ? snapshot.getSnapshot |
| 500 | : byOpenCodeRuntime(runtimeProbe.get, { |
| 501 | v2: Effect.all([ |
| 502 | snapshot.getSnapshot, |
| 503 | listOpenCode2Workspace(cwd).pipe( |
| 504 | Effect.timeout("20 seconds"), |
| 505 | Effect.mapError( |
| 506 | (cause) => |
| 507 | new ProviderDriverError({ |
| 508 | driver: DRIVER_KIND, |
| 509 | instanceId, |
| 510 | detail: `Failed to list OpenCode commands and skills for '${cwd}'`, |
| 511 | cause, |
| 512 | }), |
| 513 | ), |
| 514 | ), |
| 515 | ]).pipe( |
| 516 | Effect.map(([machineSnapshot, { skills, commands }]) => ({ |
| 517 | ...machineSnapshot, |
| 518 | skills: openCode2SkillsToServerProviderSkills(skills), |
| 519 | slashCommands: openCode2CommandsToServerProviderSlashCommands(commands), |
| 520 | })), |
| 521 | ), |
| 522 | v1: Effect.all([ |
| 523 | snapshot.getSnapshot, |
| 524 | loadWorkspaceForCwd(cwd).pipe(Effect.timeout("20 seconds")), |
| 525 | ]).pipe( |
| 526 | Effect.map(([machineSnapshot, { skills, commands }]) => ({ |
| 527 | ...machineSnapshot, |
| 528 | skills: openCodeSkillsToServerProviderSkills(skills), |
| 529 | slashCommands: openCodeCommandsToServerProviderSlashCommands(commands), |
| 530 | })), |
| 531 | Effect.mapError( |
| 532 | (cause) => |
| 533 | new ProviderDriverError({ |
| 534 | driver: DRIVER_KIND, |
| 535 | instanceId, |
| 536 | detail: `Failed to probe OpenCode commands and skills for '${cwd}'`, |
| 537 | cause, |
| 538 | }), |
| 539 | ), |
| 540 | ), |
| 541 | }), |
| 542 | orchestrationAdapter, |
| 543 | textGeneration, |
| 544 | } satisfies ProviderInstance; |
| 545 | }), |
| 546 | }; |