From cd6f5fc9a29668be1cf4ee1d5bead2b5b254a9f4 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 12 Aug 2026 14:08:54 -0400 Subject: [PATCH 1/4] feat(server): add no-execution-plane environment fallback --- packages/core/src/environment/index.ts | 1 + packages/core/src/environment/unavailable.ts | 17 ++++++++++++ packages/core/src/session.ts | 4 ++- packages/core/src/shell.ts | 29 ++++++++++++-------- packages/core/test/environment.test.ts | 16 ++++++++++- packages/server/src/handlers/shell.ts | 4 ++- packages/server/src/workerd.ts | 26 ++++-------------- packages/workerd-spike/test/spike.test.ts | 19 +++++++++++++ 8 files changed, 81 insertions(+), 35 deletions(-) create mode 100644 packages/core/src/environment/unavailable.ts diff --git a/packages/core/src/environment/index.ts b/packages/core/src/environment/index.ts index 0f6564a33da4..7ad8cdb404a6 100644 --- a/packages/core/src/environment/index.ts +++ b/packages/core/src/environment/index.ts @@ -15,6 +15,7 @@ export { export { execDefaults } from "./exec-defaults.js" export { makeLocalDriver } from "./local.js" export { makeMemoryDriver, type MemoryDriver } from "./memory.js" +export { layer as noExecutionPlaneLayer, spawner as noExecutionPlaneSpawner } from "./unavailable.js" export { type Interface, node, Service } from "./environment.js" import type { Driver } from "./driver.js" diff --git a/packages/core/src/environment/unavailable.ts b/packages/core/src/environment/unavailable.ts new file mode 100644 index 000000000000..59db611a77fd --- /dev/null +++ b/packages/core/src/environment/unavailable.ts @@ -0,0 +1,17 @@ +import { Effect, Layer, PlatformError } from "effect" +import { ChildProcessSpawner, make } from "effect/unstable/process/ChildProcessSpawner" + +export const spawner = make(() => + Effect.fail( + PlatformError.systemError({ + _tag: "Unknown", + module: "Environment", + method: "spawn", + description: "This location has no execution plane: no workspace is attached and the host cannot spawn processes", + }), + ), +) + +export const layer = Layer.succeed(ChildProcessSpawner, spawner) + +export * as EnvironmentUnavailable from "./unavailable.js" diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index bae924f9f43c..f75ce2cd6e7a 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -640,7 +640,9 @@ const layer = Layer.effect( yield* execution.awaitIdle(input.sessionID) const started = yield* Effect.gen(function* () { const shell = yield* Shell.Service - return yield* shell.create({ command: input.command, cwd: session.location.directory, timeout: 0 }) + return yield* shell + .create({ command: input.command, cwd: session.location.directory, timeout: 0 }) + .pipe(Effect.orDie) }).pipe(Effect.provide(locations.get(session.location))) yield* bus.publish( SessionEvent.Shell.Started, diff --git a/packages/core/src/shell.ts b/packages/core/src/shell.ts index 38e8c1cc0dba..d1733732be00 100644 --- a/packages/core/src/shell.ts +++ b/packages/core/src/shell.ts @@ -5,6 +5,7 @@ import { Context, Deferred, Duration, Effect, Fiber, Layer, Schema, Stream } fro import { ChildProcess } from "effect/unstable/process" import { produce } from "immer" import { Shell } from "@opencode-ai/schema/shell" +import { AppProcess } from "@opencode-ai/util/process" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Config } from "./config.js" import { Bus } from "./bus.js" @@ -50,7 +51,7 @@ export interface Interface { readonly create: ( input: Shell.CreateInput, before?: (input: ShellCreateBefore) => Effect.Effect, - ) => Effect.Effect + ) => Effect.Effect // Currently running commands only; exited shells are retained for get/output but excluded here. readonly list: () => Effect.Effect readonly get: (id: Shell.ID) => Effect.Effect @@ -215,19 +216,23 @@ export const layer = (options?: ShellSelect.Options) => // Spawn through the Environment and stream combined output to the file. The handle is scope-bound, so // the managing fiber keeps its scope open until the command terminates (it awaits `done` at the // end). `create` returns once `ready` resolves with the registered session. - const ready = Deferred.makeUnsafe() + const ready = Deferred.makeUnsafe() runFork( Effect.scoped( Effect.gen(function* () { - const handle = yield* environment.spawner.spawn( - ChildProcess.make(invocation.shell, args, { - cwd: invocation.cwd, - env: invocation.env, - stdin: "ignore", - detached: process.platform !== "win32", - forceKillAfter: Duration.seconds(3), - }), - ) + const handle = yield* environment.spawner + .spawn( + ChildProcess.make(invocation.shell, args, { + cwd: invocation.cwd, + env: invocation.env, + stdin: "ignore", + detached: process.platform !== "win32", + forceKillAfter: Duration.seconds(3), + }), + ) + .pipe( + Effect.mapError((cause) => new AppProcess.AppProcessError({ command: invocation.command, cause })), + ) const session: Active = { info: produce(info, (draft) => { draft.pid = handle.pid @@ -329,7 +334,7 @@ export const layer = (options?: ShellSelect.Options) => // release (kill) the process before its exit is observed. yield* Deferred.await(session.done).pipe(Effect.catch(() => Effect.void)) }), - ).pipe(Effect.catch(() => Effect.void)), + ).pipe(Effect.catchTag("AppProcessError", (error) => Deferred.fail(ready, error))), ) const session = yield* Deferred.await(ready) diff --git a/packages/core/test/environment.test.ts b/packages/core/test/environment.test.ts index 90246bd02bec..e705900cf106 100644 --- a/packages/core/test/environment.test.ts +++ b/packages/core/test/environment.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises" import { describe, expect } from "bun:test" import { Effect } from "effect" -import { ChildProcessSpawner } from "effect/unstable/process" +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { CrossSpawnSpawner } from "@opencode-ai/util/cross-spawn-spawner" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { @@ -10,6 +10,7 @@ import { makeFiles, makeLocalDriver, makeMemoryDriver, + noExecutionPlaneSpawner, NotFound, typeFollowing, } from "../src/environment/index" @@ -35,6 +36,19 @@ describe("typeFollowing", () => { ) }) +describe("no execution plane", () => { + it.effect("fails spawn with a typed location error", () => + Effect.gen(function* () { + const error = yield* noExecutionPlaneSpawner + .spawn(ChildProcess.make("echo", ["hello"])) + .pipe(Effect.flip) + + expect(error._tag).toBe("PlatformError") + expect(error.message).toContain("location has no execution plane") + }), + ) +}) + environmentConformance("memory environment", () => Effect.sync(() => { const driver = makeMemoryDriver() diff --git a/packages/server/src/handlers/shell.ts b/packages/server/src/handlers/shell.ts index 4b0f83bed5e3..96d7530cd816 100644 --- a/packages/server/src/handlers/shell.ts +++ b/packages/server/src/handlers/shell.ts @@ -21,7 +21,9 @@ export const ShellHandler = HttpApiBuilder.group(Api, "server.shell", (handlers) Effect.fn(function* (ctx) { const shell = yield* Shell.Service const location = yield* Location.Service - return yield* response(shell.create({ ...ctx.payload, cwd: ctx.payload.cwd || location.directory })) + return yield* response( + shell.create({ ...ctx.payload, cwd: ctx.payload.cwd || location.directory }).pipe(Effect.orDie), + ) }), ) .handle( diff --git a/packages/server/src/workerd.ts b/packages/server/src/workerd.ts index 6ed7fefde717..a5e546c2de93 100644 --- a/packages/server/src/workerd.ts +++ b/packages/server/src/workerd.ts @@ -5,12 +5,13 @@ import { ConfigPluginSource } from "@opencode-ai/core/config/plugin/source" import { Database } from "@opencode-ai/core/database/database" import { sqliteLayer } from "@opencode-ai/core/database/sqlite.workerd" import type { DurableObjectStorage } from "@opencode-ai/core/database/sqlite.workerd" +import { EnvironmentUnavailable } from "@opencode-ai/core/environment/unavailable" import { FileSystem } from "@opencode-ai/core/filesystem" import { FileSystemSearch } from "@opencode-ai/core/filesystem/search" import { Pty } from "@opencode-ai/core/pty" -import { Shell } from "@opencode-ai/core/shell" import { Snapshot } from "@opencode-ai/core/snapshot" import { Vcs } from "@opencode-ai/core/vcs" +import { CrossSpawnSpawner } from "@opencode-ai/util/cross-spawn-spawner" import type { LayerNode } from "@opencode-ai/util/effect/layer-node" import { ServerFetch } from "./fetch" import type { ServerOptions } from "./options" @@ -24,8 +25,9 @@ import type { ServerOptions } from "./options" * - Watcher and fff are disabled through their existing option flags; pty, fff, * shell-parser, photon, and process-lock native modules resolve to inert * stubs under the `workerd` bundle condition. - * - Shell, FileSystem, FileSystemSearch, and Pty fail with a clear defect until - * a remote sandbox backs them; Snapshot and Vcs degrade to no-op results. + * - Bare locations use a typed no-execution-plane process spawner; FileSystem, + * FileSystemSearch, and Pty fail with a clear defect until a remote sandbox + * backs them; Snapshot and Vcs degrade to no-op results. * - Config is injected as a string (no filesystem); plugin discovery is * precompiled-only and MCP is restricted to remote transports. * @@ -77,9 +79,9 @@ export function serverOptions(options: Options): ServerOptions { export function replacements(options: Options): LayerNode.Replacements { return [ [Database.node, Database.configuredClient(sqliteLayer({ storage: options.storage }))], + [CrossSpawnSpawner.node, EnvironmentUnavailable.layer], [Snapshot.node, Snapshot.noopLayer], [Vcs.node, vcsLayer], - [Shell.node, shellLayer], [FileSystem.node, fileSystemLayer], [FileSystemSearch.node, fileSystemSearchLayer], [Pty.node, ptyLayer], @@ -102,22 +104,6 @@ const vcsLayer = Layer.succeed( }), ) -// Shell commands need a real process; queries for unknown IDs stay typed while -// creation is a defect until a remote sandbox backs them. -const shellLayer = Layer.succeed( - Shell.Service, - Shell.Service.of({ - name: () => Effect.succeed("unsupported"), - create: () => unavailable("Shell.create"), - list: () => Effect.succeed([]), - get: (id) => Effect.fail(new Shell.NotFoundError({ id })), - wait: (id) => Effect.fail(new Shell.NotFoundError({ id })), - timeout: (id) => Effect.fail(new Shell.NotFoundError({ id })), - output: (id) => Effect.fail(new Shell.NotFoundError({ id })), - remove: (id) => Effect.fail(new Shell.NotFoundError({ id })), - }), -) - // The Location-scoped filesystem has no local worktree to serve until a remote // sandbox backs it. const fileSystemLayer = Layer.succeed( diff --git a/packages/workerd-spike/test/spike.test.ts b/packages/workerd-spike/test/spike.test.ts index b4ae1ecfba99..871e4d089c92 100644 --- a/packages/workerd-spike/test/spike.test.ts +++ b/packages/workerd-spike/test/spike.test.ts @@ -184,6 +184,25 @@ it("runs a full prompt turn against a fake provider and reads the durable log", expect(eventTypes).toContain("log.synced") }) +it("fails a shell command through the no-execution-plane spawner and leaves the session usable", async () => { + const sessionID = await createSession() + const shell = await request("/api/shell?directory=/tmp/project", { + method: "POST", + body: JSON.stringify({ command: "pwd", timeout: 1_000 }), + }) + expect(shell.status).toBe(500) + + mockLLM() + const prompt = await request(`/api/session/${sessionID}/prompt`, { + method: "POST", + body: JSON.stringify({ text: "Say hello after the shell failure" }), + }) + expect(prompt.status).toBe(200) + const wait = await request(`/api/session/${sessionID}/wait`, { method: "POST" }) + expect(wait.status).toBe(204) + expect((await readLog(sessionID)).map((item) => item.type)).toContain("session.execution.succeeded") +}) + // A5 question 1 (ack-then-continue): the Slack flow returns the prompt request // immediately and lets the turn continue inside the DO with no request held // open. The turn runs on the coordinator's background fiber in the app layer From c0b6fbd2ded8402bcd2bc1a6d6b63f3b53fa340e Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 12 Aug 2026 14:40:56 -0400 Subject: [PATCH 2/4] refactor(core): retire the MCP stdio capability flag --- packages/core/src/environment/index.ts | 1 - packages/core/src/mcp/index.ts | 7 ------- packages/core/test/environment.test.ts | 4 ++-- packages/core/test/mcp.test.ts | 22 ++++++++++++++++++++++ packages/server/src/options.ts | 6 ------ packages/server/src/routes.ts | 1 - packages/server/src/workerd.ts | 5 +---- 7 files changed, 25 insertions(+), 21 deletions(-) diff --git a/packages/core/src/environment/index.ts b/packages/core/src/environment/index.ts index 7ad8cdb404a6..0f6564a33da4 100644 --- a/packages/core/src/environment/index.ts +++ b/packages/core/src/environment/index.ts @@ -15,7 +15,6 @@ export { export { execDefaults } from "./exec-defaults.js" export { makeLocalDriver } from "./local.js" export { makeMemoryDriver, type MemoryDriver } from "./memory.js" -export { layer as noExecutionPlaneLayer, spawner as noExecutionPlaneSpawner } from "./unavailable.js" export { type Interface, node, Service } from "./environment.js" import type { Driver } from "./driver.js" diff --git a/packages/core/src/mcp/index.ts b/packages/core/src/mcp/index.ts index 9504e79e89f5..4685bc31eb5a 100644 --- a/packages/core/src/mcp/index.ts +++ b/packages/core/src/mcp/index.ts @@ -165,8 +165,6 @@ export const Options = Schema.Struct({ version: Schema.String, }), ), - /** Set false on runtimes that cannot spawn child processes; local (stdio) servers report failed instead of connecting. */ - stdio: Schema.optional(Schema.Boolean), }) export type Options = typeof Options.Type @@ -500,11 +498,6 @@ export const layer = (options?: Options) => const startServer = (name: ServerName, entry: ServerEntry) => Effect.gen(function* () { - if (options?.stdio === false && entry.config.type === "local") { - entry.status = { status: "failed", error: "stdio MCP servers are unavailable in this runtime" } - yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) - return - } // Announce the handshake so connect() and credential reconnects don't show a stale // disabled/failed status for the duration of the connection attempt. entry.status = { status: "pending" } diff --git a/packages/core/test/environment.test.ts b/packages/core/test/environment.test.ts index e705900cf106..1571df10f534 100644 --- a/packages/core/test/environment.test.ts +++ b/packages/core/test/environment.test.ts @@ -4,13 +4,13 @@ import { Effect } from "effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { CrossSpawnSpawner } from "@opencode-ai/util/cross-spawn-spawner" import { LayerNode } from "@opencode-ai/util/effect/layer-node" +import { EnvironmentUnavailable } from "../src/environment/unavailable" import { execDefaults, Failed, makeFiles, makeLocalDriver, makeMemoryDriver, - noExecutionPlaneSpawner, NotFound, typeFollowing, } from "../src/environment/index" @@ -39,7 +39,7 @@ describe("typeFollowing", () => { describe("no execution plane", () => { it.effect("fails spawn with a typed location error", () => Effect.gen(function* () { - const error = yield* noExecutionPlaneSpawner + const error = yield* EnvironmentUnavailable.spawner .spawn(ChildProcess.make("echo", ["hello"])) .pipe(Effect.flip) diff --git a/packages/core/test/mcp.test.ts b/packages/core/test/mcp.test.ts index 9c2e49c628be..cc879b8d3887 100644 --- a/packages/core/test/mcp.test.ts +++ b/packages/core/test/mcp.test.ts @@ -23,6 +23,7 @@ import { ID, type Payload } from "@opencode-ai/schema/event" import { Form } from "@opencode-ai/core/form" import { Integration } from "@opencode-ai/core/integration" import { Environment } from "@opencode-ai/core/environment/index" +import { EnvironmentUnavailable } from "@opencode-ai/core/environment/unavailable" import { Location } from "@opencode-ai/core/location" import { MCP } from "@opencode-ai/core/mcp/index" import { MCPClient } from "@opencode-ai/core/mcp/client" @@ -478,6 +479,27 @@ test("spawns local MCP servers through the location environment", async () => { expect(command.options.env).toEqual({ MCP_LOCATION_TEST: "configured" }) }) +test("reports a local MCP server as failed when the location has no execution plane", async () => { + const config = new ConfigMCP.Local({ type: "local", command: ["example-mcp"] }) + const driver = Environment.makeMemoryDriver() + const environment = Layer.succeed( + Environment.Service, + Environment.Service.of({ files: Environment.makeFiles(driver), spawner: EnvironmentUnavailable.spawner }), + ) + + await Effect.runPromise( + Effect.gen(function* () { + const service = yield* MCP.Service + yield* service.tools() + const status = (yield* service.servers()).find((server) => server.name === "resources")?.status + expect(status).toEqual({ + status: "failed", + error: expect.stringContaining("location has no execution plane"), + }) + }).pipe(Effect.provide(resourceMcpLayer(config, undefined, undefined, { environment }))), + ) +}) + test("rejects sends before the stdio transport is started", async () => { await Effect.runPromise( Effect.scoped( diff --git a/packages/server/src/options.ts b/packages/server/src/options.ts index 5ed50164a3c9..96a70bd18008 100644 --- a/packages/server/src/options.ts +++ b/packages/server/src/options.ts @@ -40,11 +40,5 @@ export const ServerOptions = Schema.Struct({ fff: Schema.optional(Schema.Boolean), }), ), - mcp: Schema.optional( - Schema.Struct({ - /** Set false on runtimes that cannot spawn child processes; local (stdio) MCP servers report failed instead of connecting. */ - stdio: Schema.optional(Schema.Boolean), - }), - ), }) export type ServerOptions = typeof ServerOptions.Type diff --git a/packages/server/src/routes.ts b/packages/server/src/routes.ts index dd9272e3103e..b91ff0bd7d28 100644 --- a/packages/server/src/routes.ts +++ b/packages/server/src/routes.ts @@ -120,7 +120,6 @@ function makeRoutes( name: options.app?.name ?? "opencode", version: options.app?.version ?? "unknown", }, - stdio: options.mcp?.stdio, }), ], [PluginRuntime.node, PluginRuntime.layerWithCell(pluginRuntimeCell)], diff --git a/packages/server/src/workerd.ts b/packages/server/src/workerd.ts index a5e546c2de93..fa628f31d4fd 100644 --- a/packages/server/src/workerd.ts +++ b/packages/server/src/workerd.ts @@ -29,7 +29,7 @@ import type { ServerOptions } from "./options" * FileSystemSearch, and Pty fail with a clear defect until a remote sandbox * backs them; Snapshot and Vcs degrade to no-op results. * - Config is injected as a string (no filesystem); plugin discovery is - * precompiled-only and MCP is restricted to remote transports. + * precompiled-only, and stdio MCP reports the same no-plane failure as Shell. * * Bundle with the `workerd` condition, e.g. * `bun build src/workerd.ts --conditions=workerd --target=node` @@ -69,9 +69,6 @@ export function serverOptions(options: Options): ServerOptions { events: { persist: true }, config: { content: options.config?.content }, models: options.models, - // No child processes on workerd: local (stdio) MCP servers report failed - // instead of connecting; remote transports work unchanged. - mcp: { stdio: false }, } } From 839dc62f352a3824fdf869b6eb68fa3e56f46e18 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 12 Aug 2026 14:51:19 -0400 Subject: [PATCH 3/4] refactor(core): lazily initialize shell output storage --- packages/core/src/shell.ts | 20 ++++++++++++++------ packages/workerd-spike/test/spike.test.ts | 4 ++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/core/src/shell.ts b/packages/core/src/shell.ts index d1733732be00..df47ffed3fad 100644 --- a/packages/core/src/shell.ts +++ b/packages/core/src/shell.ts @@ -82,9 +82,14 @@ export const layer = (options?: ShellSelect.Options) => const exitOrder: string[] = [] const outputDir = path.join(global.data, "shell", location.project.id) - const { mkdir, unlink } = yield* Effect.promise(() => import("fs/promises")) - const { createWriteStream, createReadStream } = yield* Effect.promise(() => import("fs")) - yield* Effect.promise(() => mkdir(outputDir, { recursive: true })) + const storage = yield* Effect.cached( + Effect.gen(function* () { + const { mkdir, unlink } = yield* Effect.promise(() => import("fs/promises")) + const { createWriteStream, createReadStream } = yield* Effect.promise(() => import("fs")) + yield* Effect.promise(() => mkdir(outputDir, { recursive: true })) + return { createReadStream, createWriteStream, unlink } + }), + ) yield* Effect.addFinalizer(() => Effect.gen(function* () { @@ -113,7 +118,8 @@ export const layer = (options?: ShellSelect.Options) => if (session.timeoutFiber) yield* Fiber.interrupt(session.timeoutFiber) // Unblock any wait still pending when the command is removed before it terminated. yield* Deferred.fail(session.done, new NotFoundError({ id })) - yield* Effect.promise(() => unlink(session.file).catch(() => {})) + const files = yield* storage + yield* Effect.promise(() => files.unlink(session.file).catch(() => {})) yield* bus.publish(Shell.Event.Deleted, { id }) }) @@ -158,10 +164,11 @@ export const layer = (options?: ShellSelect.Options) => const start = Math.max(0, cursor) const length = Math.min(limit, session.size - start) const buffer = Buffer.alloc(length) + const files = yield* storage const bytesRead = yield* Effect.promise( () => new Promise((resolve) => { - const stream = createReadStream(session.file, { start, end: start + length - 1 }) + const stream = files.createReadStream(session.file, { start, end: start + length - 1 }) let offset = 0 stream.on("data", (chunk: string | Buffer) => { const bytes = Buffer.from(chunk) @@ -243,7 +250,8 @@ export const layer = (options?: ShellSelect.Options) => } sessions.set(id, session) - const stream = createWriteStream(file) + const files = yield* storage + const stream = files.createWriteStream(file) const outputDone = Deferred.makeUnsafe() const pump = handle.all.pipe( Stream.runForEach((chunk: Uint8Array) => diff --git a/packages/workerd-spike/test/spike.test.ts b/packages/workerd-spike/test/spike.test.ts index 871e4d089c92..f953acab2fdc 100644 --- a/packages/workerd-spike/test/spike.test.ts +++ b/packages/workerd-spike/test/spike.test.ts @@ -186,9 +186,9 @@ it("runs a full prompt turn against a fake provider and reads the durable log", it("fails a shell command through the no-execution-plane spawner and leaves the session usable", async () => { const sessionID = await createSession() - const shell = await request("/api/shell?directory=/tmp/project", { + const shell = await request(`/api/session/${sessionID}/shell`, { method: "POST", - body: JSON.stringify({ command: "pwd", timeout: 1_000 }), + body: JSON.stringify({ command: "pwd" }), }) expect(shell.status).toBe(500) From 242ac80a5434dd1247e99f1f5001d33d98cdc696 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 12 Aug 2026 15:04:41 -0400 Subject: [PATCH 4/4] fix(core): initialize shell output before spawning --- packages/core/src/shell.ts | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/packages/core/src/shell.ts b/packages/core/src/shell.ts index df47ffed3fad..d1733732be00 100644 --- a/packages/core/src/shell.ts +++ b/packages/core/src/shell.ts @@ -82,14 +82,9 @@ export const layer = (options?: ShellSelect.Options) => const exitOrder: string[] = [] const outputDir = path.join(global.data, "shell", location.project.id) - const storage = yield* Effect.cached( - Effect.gen(function* () { - const { mkdir, unlink } = yield* Effect.promise(() => import("fs/promises")) - const { createWriteStream, createReadStream } = yield* Effect.promise(() => import("fs")) - yield* Effect.promise(() => mkdir(outputDir, { recursive: true })) - return { createReadStream, createWriteStream, unlink } - }), - ) + const { mkdir, unlink } = yield* Effect.promise(() => import("fs/promises")) + const { createWriteStream, createReadStream } = yield* Effect.promise(() => import("fs")) + yield* Effect.promise(() => mkdir(outputDir, { recursive: true })) yield* Effect.addFinalizer(() => Effect.gen(function* () { @@ -118,8 +113,7 @@ export const layer = (options?: ShellSelect.Options) => if (session.timeoutFiber) yield* Fiber.interrupt(session.timeoutFiber) // Unblock any wait still pending when the command is removed before it terminated. yield* Deferred.fail(session.done, new NotFoundError({ id })) - const files = yield* storage - yield* Effect.promise(() => files.unlink(session.file).catch(() => {})) + yield* Effect.promise(() => unlink(session.file).catch(() => {})) yield* bus.publish(Shell.Event.Deleted, { id }) }) @@ -164,11 +158,10 @@ export const layer = (options?: ShellSelect.Options) => const start = Math.max(0, cursor) const length = Math.min(limit, session.size - start) const buffer = Buffer.alloc(length) - const files = yield* storage const bytesRead = yield* Effect.promise( () => new Promise((resolve) => { - const stream = files.createReadStream(session.file, { start, end: start + length - 1 }) + const stream = createReadStream(session.file, { start, end: start + length - 1 }) let offset = 0 stream.on("data", (chunk: string | Buffer) => { const bytes = Buffer.from(chunk) @@ -250,8 +243,7 @@ export const layer = (options?: ShellSelect.Options) => } sessions.set(id, session) - const files = yield* storage - const stream = files.createWriteStream(file) + const stream = createWriteStream(file) const outputDone = Deferred.makeUnsafe() const pump = handle.all.pipe( Stream.runForEach((chunk: Uint8Array) =>