Просмотр исходного кода

refactor(core): remove infrastructure layer exports (#34624)

James Long 2 месяцев назад
Родитель
Сommit
472d0f376e
34 измененных файлов с 46 добавлено и 104 удалено
  1. 3 2
      packages/cli/src/commands/handlers/serve.ts
  2. 2 1
      packages/cli/src/tui.ts
  3. 1 3
      packages/core/src/background-job.ts
  4. 1 3
      packages/core/src/credential.ts
  5. 1 2
      packages/core/src/cross-spawn-spawner.ts
  6. 1 7
      packages/core/src/database/database.ts
  7. 1 2
      packages/core/src/event.ts
  8. 0 4
      packages/core/src/filesystem.ts
  9. 1 3
      packages/core/src/filesystem/watcher.ts
  10. 1 2
      packages/core/src/fs-util.ts
  11. 1 2
      packages/core/src/git.ts
  12. 1 2
      packages/core/src/global.ts
  13. 1 6
      packages/core/src/models-dev.ts
  14. 1 7
      packages/core/src/npm.ts
  15. 1 3
      packages/core/src/permission/saved.ts
  16. 0 1
      packages/core/src/plugin.ts
  17. 0 2
      packages/core/src/plugin/internal.ts
  18. 1 2
      packages/core/src/process.ts
  19. 1 6
      packages/core/src/project.ts
  20. 1 2
      packages/core/src/project/directories.ts
  21. 1 8
      packages/core/src/repository-cache.ts
  22. 1 2
      packages/core/src/ripgrep.ts
  23. 1 7
      packages/core/src/ripgrep/binary.ts
  24. 0 2
      packages/core/src/skill.ts
  25. 1 7
      packages/core/src/skill/discovery.ts
  26. 1 2
      packages/core/src/util/effect-flock.ts
  27. 3 2
      packages/opencode/src/cli/cmd/run/variant.shared.ts
  28. 2 1
      packages/opencode/src/cli/tui/layer.ts
  29. 2 1
      packages/opencode/src/session/session.ts
  30. 2 1
      packages/opencode/test/fixture/fixture.ts
  31. 2 3
      packages/opencode/test/installation/installation.test.ts
  32. 3 1
      packages/opencode/test/lib/cli-process.ts
  33. 4 3
      packages/opencode/test/provider/provider.test.ts
  34. 3 2
      packages/tui/test/app-lifecycle.test.tsx

+ 3 - 2
packages/cli/src/commands/handlers/serve.ts

@@ -1,5 +1,7 @@
 import { NodeHttpServer } from "@effect/platform-node"
 import { Credential } from "@opencode-ai/core/credential"
+import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { PermissionSaved } from "@opencode-ai/core/permission/saved"
 import { Context, Layer, Option } from "effect"
 import * as Effect from "effect/Effect"
@@ -38,8 +40,7 @@ function bind(hostname: string, port: number, password: string) {
   return Layer.build(
     HttpRouter.serve(createRoutes(password), { disableListenLog: true, disableLogger: true }).pipe(
       Layer.provideMerge(NodeHttpServer.layer(() => createServer(), { port, host: hostname })),
-      Layer.provide(Credential.defaultLayer),
-      Layer.provide(PermissionSaved.defaultLayer),
+      Layer.provide(AppNodeBuilder.build(LayerNode.group([Credential.node, PermissionSaved.node]))),
     ),
   ).pipe(Effect.map((context) => Context.get(context, HttpServer.HttpServer).address))
 }

+ 2 - 1
packages/cli/src/tui.ts

@@ -1,6 +1,7 @@
 import { run } from "@opencode-ai/tui"
 import { TuiConfig } from "@opencode-ai/tui/config"
 import { Effect } from "effect"
+import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
 import { Global } from "@opencode-ai/core/global"
 
 export function runTui(transport: { url: string; headers: RequestInit["headers"] }) {
@@ -14,7 +15,7 @@ export function runTui(transport: { url: string; headers: RequestInit["headers"]
       async start() {},
       async dispose() {},
     },
-  }).pipe(Effect.provide(Global.defaultLayer))
+  }).pipe(Effect.provide(AppNodeBuilder.build(Global.node)))
 }
 
 const legacyDefaults: Record<string, unknown> = {

+ 1 - 3
packages/core/src/background-job.ts

@@ -360,8 +360,6 @@ export const make = Effect.gen(function* () {
   return Service.of({ list, get, start, extend, wait, waitForPromotion, promote, cancel })
 })
 
-export const layer = Layer.effect(Service, make)
-
-export const defaultLayer = layer
+const layer = Layer.effect(Service, make)
 
 export const node = makeGlobalNode({ service: Service, layer, deps: [] })

+ 1 - 3
packages/core/src/credential.ts

@@ -48,7 +48,7 @@ export interface Interface {
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Credential") {}
 
-export const layer = Layer.effect(
+const layer = Layer.effect(
   Service,
   Effect.gen(function* () {
     const { db } = yield* Database.Service
@@ -135,6 +135,4 @@ export const layer = Layer.effect(
   }),
 )
 
-export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))
-
 export const node = makeGlobalNode({ service: Service, layer, deps: [Database.node] })

+ 1 - 2
packages/core/src/cross-spawn-spawner.ts

@@ -497,12 +497,11 @@ export const make = Effect.gen(function* () {
   return makeSpawner(spawnCommand)
 })
 
-export const layer: Layer.Layer<ChildProcessSpawner, never, FileSystem.FileSystem | Path.Path> = Layer.effect(
+const layer: Layer.Layer<ChildProcessSpawner, never, FileSystem.FileSystem | Path.Path> = Layer.effect(
   ChildProcessSpawner,
   make,
 )
 
-export const defaultLayer = layer.pipe(Layer.provide(NodeFileSystem.layer), Layer.provide(NodePath.layer))
 export const node = makeGlobalNode({ service: ChildProcessSpawner, layer, deps: [filesystem, path] })
 
 export * as CrossSpawnSpawner from "./cross-spawn-spawner"

+ 1 - 7
packages/core/src/database/database.ts

@@ -19,7 +19,7 @@ export interface Interface {
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/v2/storage/Database") {}
 
-export const layer = Layer.effect(
+const layer = Layer.effect(
   Service,
   Effect.gen(function* () {
     const db = yield* makeDatabase
@@ -54,10 +54,4 @@ export function path() {
   return join(Global.Path.data, `opencode-${InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-")}.db`)
 }
 
-export const defaultLayer = Layer.unwrap(
-  Effect.gen(function* () {
-    return layerFromPath(path())
-  }),
-).pipe(Layer.provide(Global.defaultLayer))
-
 export const node = makeGlobalNode({ service: Service, layer: layerFromPath(path()), deps: [] })

+ 1 - 2
packages/core/src/event.ts

@@ -634,7 +634,6 @@ export const layerWith = (options?: LayerOptions) =>
     }),
   )
 
-export const layer = layerWith()
+const layer = layerWith()
 export const node = makeGlobalNode({ service: Service, layer: layer, deps: [Database.node] })
 
-export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))

+ 0 - 4
packages/core/src/filesystem.ts

@@ -111,10 +111,6 @@ const baseLayer = Layer.effect(
   }),
 )
 
-export const layer = baseLayer.pipe(Layer.provide(FileSystemSearch.locationLayer), Layer.provide(FSUtil.defaultLayer))
-
-export const locationLayer = layer
-
 export const node = makeLocationNode({
   service: Service,
   layer: baseLayer,

+ 1 - 3
packages/core/src/filesystem/watcher.ts

@@ -54,7 +54,7 @@ export interface Interface {}
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/v2/FileWatcher") {}
 
-export const layer = Layer.effect(
+const layer = Layer.effect(
   Service,
   Effect.gen(function* () {
     if (yield* Flag.OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER) return Service.of({})
@@ -133,8 +133,6 @@ export const layer = Layer.effect(
   ),
 )
 
-export const locationLayer = layer.pipe(Layer.provide(Config.locationLayer), Layer.provide(Git.defaultLayer))
-
 export const node = makeLocationNode({
   service: Service,
   layer,

+ 1 - 2
packages/core/src/fs-util.ts

@@ -49,7 +49,7 @@ export namespace FSUtil {
 
   export const use = serviceUse(Service)
 
-  export const layer = Layer.effect(
+  const layer = Layer.effect(
     Service,
     Effect.gen(function* () {
       const fs = yield* FileSystem.FileSystem
@@ -200,7 +200,6 @@ export namespace FSUtil {
     }),
   )
 
-  export const defaultLayer = layer.pipe(Layer.provide(NodeFileSystem.layer))
   export const node = makeGlobalNode({ service: Service, layer: layer, deps: [filesystem] })
 
   // Pure helpers that don't need Effect (path manipulation, sync operations)

+ 1 - 2
packages/core/src/git.ts

@@ -172,7 +172,7 @@ export interface Interface {
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/GitV2") {}
 
-export const layer = Layer.effect(
+const layer = Layer.effect(
   Service,
   Effect.gen(function* () {
     const fs = yield* FSUtil.Service
@@ -943,7 +943,6 @@ export const layer = Layer.effect(
   }),
 )
 
-export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer), Layer.provide(AppProcess.defaultLayer))
 export const node = makeGlobalNode({ service: Service, layer: layer, deps: [FSUtil.node, AppProcess.node] })
 
 interface Result {

+ 1 - 2
packages/core/src/global.ts

@@ -71,12 +71,11 @@ export function make(input: Partial<Interface> = {}): Interface {
   }
 }
 
-export const layer = Layer.effect(
+const layer = Layer.effect(
   Service,
   Effect.sync(() => Service.of(make())),
 )
 
-export const defaultLayer = layer
 export const node = makeGlobalNode({ service: Service, layer: layer, deps: [] })
 
 export const layerWith = (input: Partial<Interface>) =>

+ 1 - 6
packages/core/src/models-dev.ts

@@ -120,7 +120,7 @@ export interface Interface {
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/ModelsDev") {}
 
-export const layer = Layer.effect(
+const layer = Layer.effect(
   Service,
   Effect.gen(function* () {
     const fs = yield* FSUtil.Service
@@ -239,11 +239,6 @@ export const layer = Layer.effect(
   }),
 )
 
-export const defaultLayer = layer.pipe(
-  Layer.provide(FetchHttpClient.layer),
-  Layer.provide(FSUtil.defaultLayer),
-  Layer.provide(EventV2.defaultLayer),
-)
 export const node = makeGlobalNode({ service: Service, layer: layer, deps: [FSUtil.node, EventV2.node, httpClient] })
 
 export * as ModelsDev from "./models-dev"

+ 1 - 7
packages/core/src/npm.ts

@@ -69,7 +69,7 @@ interface ArboristTree {
   edgesOut: Map<string, { to?: ArboristNode }>
 }
 
-export const layer = Layer.effect(
+const layer = Layer.effect(
   Service,
   Effect.gen(function* () {
     const afs = yield* FSUtil.Service
@@ -248,12 +248,6 @@ export const layer = Layer.effect(
   }),
 )
 
-export const defaultLayer = layer.pipe(
-  Layer.provide(EffectFlock.layer),
-  Layer.provide(FSUtil.layer),
-  Layer.provide(Global.layer),
-  Layer.provide(NodeFileSystem.layer),
-)
 export const node = makeGlobalNode({
   service: Service,
   layer: layer,

+ 1 - 3
packages/core/src/permission/saved.ts

@@ -34,7 +34,7 @@ export interface Interface {
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/v2/PermissionSaved") {}
 
-export const layer = Layer.effect(
+const layer = Layer.effect(
   Service,
   Effect.gen(function* () {
     const { db } = yield* Database.Service
@@ -76,6 +76,4 @@ export const layer = Layer.effect(
   }),
 )
 
-export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))
-
 export const node = makeGlobalNode({ service: Service, layer, deps: [Database.node] })

+ 0 - 1
packages/core/src/plugin.ts

@@ -149,7 +149,6 @@ export const locationLayer = layer.pipe(
   Layer.provideMerge(CommandV2.locationLayer),
   Layer.provideMerge(Integration.locationLayer),
   Layer.provideMerge(Reference.locationLayer),
-  Layer.provideMerge(SkillV2.locationLayer),
 )
 
 export const node = makeLocationNode({

+ 0 - 2
packages/core/src/plugin/internal.ts

@@ -125,9 +125,7 @@ const layer = Layer.effectDiscard(
 )
 
 export const locationLayer = layer.pipe(
-  Layer.provideMerge(PluginV2.locationLayer),
   Layer.provideMerge(Config.locationLayer),
-  Layer.provideMerge(FileSystem.locationLayer),
   Layer.provideMerge(FetchHttpClient.layer),
 )
 

+ 1 - 2
packages/core/src/process.ts

@@ -136,7 +136,7 @@ export const collectStream = (stream: Stream.Stream<Uint8Array, PlatformError>,
     },
   ).pipe(Effect.map((x) => ({ buffer: Buffer.concat(x.chunks), truncated: x.truncated })))
 
-export const layer = Layer.effect(
+const layer = Layer.effect(
   Service,
   Effect.gen(function* () {
     const spawner = yield* ChildProcessSpawner
@@ -256,7 +256,6 @@ export const layer = Layer.effect(
   }),
 )
 
-export const defaultLayer = layer.pipe(Layer.provide(CrossSpawnSpawner.defaultLayer))
 export const node = makeGlobalNode({ service: Service, layer: layer, deps: [CrossSpawnSpawner.node] })
 
 export * as AppProcess from "./process"

+ 1 - 6
packages/core/src/project.ts

@@ -51,7 +51,7 @@ export interface Interface {
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/ProjectV2") {}
 
-export const layer = Layer.effect(
+const layer = Layer.effect(
   Service,
   Effect.gen(function* () {
     const fs = yield* FSUtil.Service
@@ -129,11 +129,6 @@ export const layer = Layer.effect(
   }),
 )
 
-export const defaultLayer = layer.pipe(
-  Layer.provide(FSUtil.defaultLayer),
-  Layer.provide(Git.defaultLayer),
-  Layer.provideMerge(ProjectDirectories.defaultLayer),
-)
 export const node = makeGlobalNode({
   service: Service,
   layer: layer,

+ 1 - 2
packages/core/src/project/directories.ts

@@ -57,7 +57,7 @@ export interface Interface {
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/ProjectDirectories") {}
 
-export const layer = Layer.effect(
+const layer = Layer.effect(
   Service,
   Effect.gen(function* () {
     const db = (yield* Database.Service).db
@@ -155,5 +155,4 @@ export const layer = Layer.effect(
   }),
 )
 
-export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))
 export const node = makeGlobalNode({ service: Service, layer: layer, deps: [Database.node] })

+ 1 - 8
packages/core/src/repository-cache.ts

@@ -121,7 +121,7 @@ export const validateBranch = Effect.fn("RepositoryCache.validateBranch")(functi
   })
 })
 
-export const layer: Layer.Layer<Service, never, FSUtil.Service | Git.Service | EffectFlock.Service | Global.Service> =
+const layer: Layer.Layer<Service, never, FSUtil.Service | Git.Service | EffectFlock.Service | Global.Service> =
   Layer.effect(
     Service,
     Effect.gen(function* () {
@@ -223,13 +223,6 @@ export const layer: Layer.Layer<Service, never, FSUtil.Service | Git.Service | E
     }),
   )
 
-export const defaultLayer: Layer.Layer<Service> = layer.pipe(
-  Layer.provide(EffectFlock.defaultLayer),
-  Layer.provide(FSUtil.defaultLayer),
-  Layer.provide(Git.defaultLayer),
-  Layer.provide(Global.defaultLayer),
-)
-
 export const node = makeGlobalNode({
   service: Service,
   layer,

+ 1 - 2
packages/core/src/ripgrep.ts

@@ -89,7 +89,7 @@ const failure = (message: string, cause?: unknown) => new Error({ message, cause
 const isInvalidPattern = (stderr: string) =>
   stderr.includes("regex parse error") || stderr.includes("error parsing regex")
 
-export const layer = Layer.effect(
+const layer = Layer.effect(
   Service,
   Effect.gen(function* () {
     const process = yield* AppProcess.Service
@@ -278,5 +278,4 @@ export const layer = Layer.effect(
   }),
 )
 
-export const defaultLayer = layer.pipe(Layer.provide(Layer.merge(RipgrepBinary.defaultLayer, AppProcess.defaultLayer)))
 export const node = makeGlobalNode({ service: Service, layer: layer, deps: [RipgrepBinary.node, AppProcess.node] })

+ 1 - 7
packages/core/src/ripgrep/binary.ts

@@ -28,7 +28,7 @@ export namespace RipgrepBinary {
 
   export class Service extends Context.Service<Service, Interface>()("@opencode/RipgrepBinary") {}
 
-  export const layer = Layer.effect(
+  const layer = Layer.effect(
     Service,
     Effect.gen(function* () {
       const fs = yield* FSUtil.Service
@@ -124,12 +124,6 @@ export namespace RipgrepBinary {
     }),
   )
 
-  export const defaultLayer = layer.pipe(
-    Layer.provide(FetchHttpClient.layer),
-    Layer.provide(FSUtil.defaultLayer),
-    Layer.provide(CrossSpawnSpawner.defaultLayer),
-  )
-
   export const node = makeGlobalNode({
     service: Service,
     layer: layer,

+ 0 - 2
packages/core/src/skill.ts

@@ -129,6 +129,4 @@ export const layer = Layer.effect(
   }),
 )
 
-export const locationLayer = layer.pipe(Layer.provide(SkillDiscovery.defaultLayer))
-
 export const node = makeLocationNode({ service: Service, layer, deps: [SkillDiscovery.node, FSUtil.node] })

+ 1 - 7
packages/core/src/skill/discovery.ts

@@ -68,7 +68,7 @@ export interface Interface {
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SkillDiscovery") {}
 
-export const layer = Layer.effect(
+const layer = Layer.effect(
   Service,
   Effect.gen(function* () {
     const fs = yield* FSUtil.Service
@@ -210,10 +210,4 @@ export const layer = Layer.effect(
   }),
 )
 
-export const defaultLayer = layer.pipe(
-  Layer.provide(FetchHttpClient.layer),
-  Layer.provide(FSUtil.defaultLayer),
-  Layer.provide(Global.defaultLayer),
-)
-
 export const node = makeGlobalNode({ service: Service, layer, deps: [httpClient, FSUtil.node, Global.node] })

+ 1 - 2
packages/core/src/util/effect-flock.ts

@@ -94,7 +94,7 @@ export namespace EffectFlock {
 
   const isPathGone = (e: PlatformError) => e.reason._tag === "NotFound" || e.reason._tag === "Unknown"
 
-  export const layer: Layer.Layer<Service, never, Global.Service | FSUtil.Service> = Layer.effect(
+  const layer: Layer.Layer<Service, never, Global.Service | FSUtil.Service> = Layer.effect(
     Service,
     Effect.gen(function* () {
       const global = yield* Global.Service
@@ -280,6 +280,5 @@ export namespace EffectFlock {
     }),
   )
 
-  export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer), Layer.provide(Global.layer))
   export const node = makeGlobalNode({ service: Service, layer: layer, deps: [Global.node, FSUtil.node] })
 }

+ 3 - 2
packages/opencode/src/cli/cmd/run/variant.shared.ts

@@ -8,6 +8,7 @@
 // variant and the persisted file.
 import path from "path"
 import { FSUtil } from "@opencode-ai/core/fs-util"
+import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
 import { Context, Effect, Layer } from "effect"
 import { makeRuntime } from "@/effect/run-service"
 import { Global } from "@opencode-ai/core/global"
@@ -135,7 +136,7 @@ function state(value: unknown): ModelState {
   }
 }
 
-function createLayer(fs = FSUtil.defaultLayer) {
+function createLayer(fs = AppNodeBuilder.build(FSUtil.node)) {
   return Layer.fresh(
     Layer.effect(
       Service,
@@ -196,7 +197,7 @@ function createLayer(fs = FSUtil.defaultLayer) {
 }
 
 /** @internal Exported for testing. */
-export function createVariantRuntime(fs = FSUtil.defaultLayer): VariantRuntime {
+export function createVariantRuntime(fs = AppNodeBuilder.build(FSUtil.node)): VariantRuntime {
   const runtime = makeRuntime(Service, createLayer(fs))
   return {
     resolveSavedVariant: (model) => runtime.runPromise((svc) => svc.resolveSavedVariant(model)).catch(() => undefined),

+ 2 - 1
packages/opencode/src/cli/tui/layer.ts

@@ -1,7 +1,8 @@
 import { run as runTui, type TuiInput } from "@opencode-ai/tui"
 import { Global } from "@opencode-ai/core/global"
+import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
 import { Effect } from "effect"
 
 export function run(input: TuiInput) {
-  return runTui(input).pipe(Effect.provide(Global.defaultLayer))
+  return runTui(input).pipe(Effect.provide(AppNodeBuilder.build(Global.node)))
 }

+ 2 - 1
packages/opencode/src/session/session.ts

@@ -1,4 +1,5 @@
 import { LayerNode } from "@opencode-ai/core/effect/layer-node"
+import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
 import { PermissionV1 } from "@opencode-ai/core/v1/permission"
 import { Slug } from "@opencode-ai/core/util/slug"
 import { SessionV1 } from "@opencode-ai/core/v1/session"
@@ -46,7 +47,7 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { SessionMessage } from "@opencode-ai/schema/session-message"
 
-const runtime = makeRuntime(Database.Service, Database.defaultLayer)
+const runtime = makeRuntime(Database.Service, AppNodeBuilder.build(Database.node))
 
 const parentTitlePrefix = "New session - "
 const childTitlePrefix = "Child session - "

+ 2 - 1
packages/opencode/test/fixture/fixture.ts

@@ -7,6 +7,7 @@ import { Effect, Context, Layer } from "effect"
 import type * as PlatformError from "effect/PlatformError"
 import type * as Scope from "effect/Scope"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
+import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
 import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
 import type { Config } from "@/config/config"
 import { LayerNode } from "@opencode-ai/core/effect/layer-node"
@@ -207,7 +208,7 @@ export const withTmpdirInstance =
     Effect.gen(function* () {
       const directory = yield* tmpdirScoped(options)
       return yield* self.pipe(Effect.provideService(TestInstance, { directory }), provideInstanceEffect(directory))
-    }).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer))
+    }).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(AppNodeBuilder.build(CrossSpawnSpawner.node)))
 
 export function provideTmpdirServer<A, E, R>(
   self: (input: { dir: string; llm: TestLLMServer["Service"] }) => Effect.Effect<A, E, R>,

+ 2 - 3
packages/opencode/test/installation/installation.test.ts

@@ -7,7 +7,7 @@ import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstab
 import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
 import { Installation } from "../../src/installation"
 import { InstallationChannel } from "@opencode-ai/core/installation/version"
-import { AppProcess } from "@opencode-ai/core/process"
+import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { testEffect } from "../lib/effect"
 
 const encoder = new TextEncoder()
@@ -60,10 +60,9 @@ function testLayer(
     layer: mockSpawner(spawnHandler),
     deps: [],
   })
-  const appProcessNode = makeGlobalNode({ service: AppProcess.Service, layer: AppProcess.layer, deps: [spawnerNode] })
   return LayerNode.compile(Installation.node, [
     [httpClient, mockHttpClient(httpHandler)],
-    [AppProcess.node, appProcessNode],
+    [CrossSpawnSpawner.node, spawnerNode],
   ])
 }
 

+ 3 - 1
packages/opencode/test/lib/cli-process.ts

@@ -19,6 +19,8 @@
 // different return shape — see the TODO at the bottom of OpencodeCli.
 import { test, type TestOptions } from "bun:test"
 import { FSUtil } from "@opencode-ai/core/fs-util"
+import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { AppProcess } from "@opencode-ai/core/process"
 import { Deferred, Duration, Effect, Layer, Queue, Schedule, Scope, Stream } from "effect"
 import { FetchHttpClient, HttpClient } from "effect/unstable/http"
@@ -469,7 +471,7 @@ export function withCliFixture<A, E>(
     // and hit endpoints on `opencode.serve()` without rolling their own fetch.
   }).pipe(
     Effect.provide(
-      Layer.mergeAll(TestLLMServer.layer, FetchHttpClient.layer, FSUtil.defaultLayer, AppProcess.defaultLayer),
+      Layer.mergeAll(TestLLMServer.layer, FetchHttpClient.layer, AppNodeBuilder.build(LayerNode.group([FSUtil.node, AppProcess.node]))),
     ),
   )
 }

+ 4 - 3
packages/opencode/test/provider/provider.test.ts

@@ -2,6 +2,7 @@ import { afterEach, expect, test } from "bun:test"
 import { mkdir, unlink } from "fs/promises"
 import path from "path"
 import { LayerNode } from "@opencode-ai/core/effect/layer-node"
+import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
 import { Effect, Layer } from "effect"
 import { ModelsDev } from "@opencode-ai/core/models-dev"
 import { FSUtil } from "@opencode-ai/core/fs-util"
@@ -1765,7 +1766,7 @@ const instanceStoreLayer = LayerNode.compile(InstanceStore.node, [
   [InstanceStore.bootstrapNode, InstanceBootstrap.node],
 ])
 const provideMultiInstance = <A, E, R>(eff: Effect.Effect<A, E, R>) =>
-  eff.pipe(Effect.provide(instanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer))
+  eff.pipe(Effect.provide(instanceStoreLayer), Effect.provide(AppNodeBuilder.build(CrossSpawnSpawner.node)))
 
 it.effect("plugin config providers persist after instance dispose", () =>
   Effect.gen(function* () {
@@ -1868,7 +1869,7 @@ it.effect("opencode loader keeps paid models when config apiKey is present", ()
       Provider.use
         .list()
         .pipe(provideInstanceEffect(directory))
-        .pipe(Effect.provide(instanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer))
+        .pipe(Effect.provide(instanceStoreLayer), Effect.provide(AppNodeBuilder.build(CrossSpawnSpawner.node)))
 
     const none = paid(yield* listIn(noneDir))
     const keyedCount = paid(yield* listIn(keyedDir))
@@ -1887,7 +1888,7 @@ it.effect("opencode loader keeps paid models when auth exists", () =>
       Provider.use
         .list()
         .pipe(provideInstanceEffect(directory))
-        .pipe(Effect.provide(instanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer))
+        .pipe(Effect.provide(instanceStoreLayer), Effect.provide(AppNodeBuilder.build(CrossSpawnSpawner.node)))
 
     const none = paid(yield* listIn(noneDir))
 

+ 3 - 2
packages/tui/test/app-lifecycle.test.tsx

@@ -2,6 +2,7 @@ import { expect, mock, test } from "bun:test"
 import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
 import { createTestRenderer } from "@opentui/core/testing"
 import { Effect } from "effect"
+import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
 import { Global } from "@opencode-ai/core/global"
 import { createTuiResolvedConfig } from "./fixture/tui-runtime"
 import { createEventSource, createFetch, directory, json } from "./fixture/tui-sdk"
@@ -43,7 +44,7 @@ test("SIGHUP clears title and disposes scoped resources once", async () => {
             disposes++
           },
         },
-      }).pipe(Effect.provide(Global.defaultLayer)),
+      }).pipe(Effect.provide(AppNodeBuilder.build(Global.node))),
     )
     await ready
     process.emit("SIGHUP")
@@ -108,7 +109,7 @@ test("app.exit prints the session epilogue after scoped cleanup", async () => {
           },
           async dispose() {},
         },
-      }).pipe(Effect.provide(Global.defaultLayer)),
+      }).pipe(Effect.provide(AppNodeBuilder.build(Global.node))),
     )
 
     await ready