feat(opencode): add typed application layer graph (#31531)
This commit is contained in:
parent
7a54a2c49c
commit
07e5ea9367
@ -24,6 +24,8 @@ import {
|
||||
import * as NodeChildProcess from "node:child_process"
|
||||
import { PassThrough } from "node:stream"
|
||||
import launch from "cross-spawn"
|
||||
import { LayerNode } from "./effect/layer-node"
|
||||
import { filesystem, path } from "./effect/layer-node-platform"
|
||||
|
||||
const toError = (err: unknown): Error => (err instanceof globalThis.Error ? err : new globalThis.Error(String(err)))
|
||||
|
||||
@ -501,5 +503,6 @@ export const layer: Layer.Layer<ChildProcessSpawner, never, FileSystem.FileSyste
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(NodeFileSystem.layer), Layer.provide(NodePath.layer))
|
||||
export const node = LayerNode.make(layer, [filesystem, path])
|
||||
|
||||
export * as CrossSpawnSpawner from "./cross-spawn-spawner"
|
||||
|
||||
@ -8,6 +8,7 @@ import { Flag } from "../flag/flag"
|
||||
import { isAbsolute, join } from "path"
|
||||
import { DatabaseMigration } from "./migration"
|
||||
import { InstallationChannel } from "../installation/version"
|
||||
import { LayerNode } from "../effect/layer-node"
|
||||
|
||||
const makeDatabase = EffectDrizzleSqlite.makeWithDefaults()
|
||||
type DatabaseShape = Effect.Success<typeof makeDatabase>
|
||||
@ -58,3 +59,5 @@ export const defaultLayer = Layer.unwrap(
|
||||
return layerFromPath(path())
|
||||
}),
|
||||
).pipe(Layer.provide(Global.defaultLayer))
|
||||
|
||||
export const node = LayerNode.make(layerFromPath(path()), [])
|
||||
|
||||
12
packages/core/src/effect/layer-node-platform.ts
Normal file
12
packages/core/src/effect/layer-node-platform.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { NodeFileSystem, NodePath } from "@effect/platform-node"
|
||||
import { LLMClient, RequestExecutor } from "@opencode-ai/llm/route"
|
||||
import { FetchHttpClient } from "effect/unstable/http"
|
||||
import { LayerNode } from "./layer-node"
|
||||
|
||||
export const filesystem = LayerNode.make(NodeFileSystem.layer, [])
|
||||
export const path = LayerNode.make(NodePath.layer, [])
|
||||
export const httpClient = LayerNode.make(FetchHttpClient.layer, [])
|
||||
export const requestExecutor = LayerNode.make(RequestExecutor.layer, [httpClient])
|
||||
export const llmClient = LayerNode.make(LLMClient.layer, [requestExecutor])
|
||||
|
||||
export * as LayerNodePlatform from "./layer-node-platform"
|
||||
95
packages/core/src/effect/layer-node.ts
Normal file
95
packages/core/src/effect/layer-node.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { Layer } from "effect"
|
||||
|
||||
type RuntimeLayer = Layer.Layer<never, unknown, unknown>
|
||||
type AnyNode = Node<unknown, unknown>
|
||||
type NodeList = readonly [] | readonly [AnyNode, ...AnyNode[]]
|
||||
type Output<Item> = [Item] extends [never] ? never : Item extends Node<infer A, unknown> ? A : never
|
||||
type Error<Item> = [Item] extends [never] ? never : Item extends Node<unknown, infer E> ? E : never
|
||||
type Missing<Required, Dependencies extends NodeList> = Exclude<Required, Output<Dependencies[number]>>
|
||||
type CheckDependencies<Implementation extends Layer.Any, Dependencies extends NodeList> = [
|
||||
Missing<Layer.Services<Implementation>, Dependencies>,
|
||||
] extends [never]
|
||||
? unknown
|
||||
: { readonly "Missing dependencies": Missing<Layer.Services<Implementation>, Dependencies> }
|
||||
declare const $OutputType: unique symbol
|
||||
declare const $ErrorType: unique symbol
|
||||
|
||||
export type Node<A, E = never> = {
|
||||
readonly kind: "layer" | "group"
|
||||
readonly implementation?: Layer.Any
|
||||
readonly dependencies: readonly AnyNode[]
|
||||
readonly [$OutputType]?: () => A
|
||||
readonly [$ErrorType]?: () => E
|
||||
}
|
||||
|
||||
export function make<const Implementation extends Layer.Any, const Items extends NodeList>(
|
||||
implementation: Implementation,
|
||||
dependencies: Items & CheckDependencies<Implementation, NoInfer<Items>>,
|
||||
): Node<Layer.Success<Implementation>, Layer.Error<Implementation> | Error<Items[number]>> {
|
||||
return { kind: "layer", implementation: implementation as Layer.Any, dependencies }
|
||||
}
|
||||
|
||||
export function group<const Items extends NodeList>(
|
||||
dependencies: Items,
|
||||
): Node<Output<Items[number]>, Error<Items[number]>> {
|
||||
return { kind: "group", dependencies }
|
||||
}
|
||||
|
||||
export type Replacement<A = unknown> = {
|
||||
readonly source: Node<A, unknown>
|
||||
readonly replacement: Node<A, unknown>
|
||||
}
|
||||
|
||||
type CheckReplacementErrors<SourceError, ReplacementError> = [Exclude<ReplacementError, SourceError>] extends [never]
|
||||
? unknown
|
||||
: { readonly "New replacement errors": Exclude<ReplacementError, SourceError> }
|
||||
|
||||
export function replace<A, E, E2>(
|
||||
source: Node<A, E>,
|
||||
replacement: Node<NoInfer<A>, E2> & CheckReplacementErrors<E, NoInfer<E2>>,
|
||||
): Replacement<A> {
|
||||
return { source, replacement }
|
||||
}
|
||||
|
||||
export function buildLayer<A, E>(node: Node<A, E>, options?: { readonly replacements?: readonly Replacement[] }) {
|
||||
const replacements = new Map(options?.replacements?.map((item) => [item.source, item.replacement]))
|
||||
const cache = new Map<AnyNode, RuntimeLayer>()
|
||||
const visiting = new Set<AnyNode>()
|
||||
const stack: AnyNode[] = []
|
||||
const ids = new Map<AnyNode, number>()
|
||||
|
||||
const visit = (input: AnyNode): RuntimeLayer => {
|
||||
const node = replacements.get(input) ?? input
|
||||
const cached = cache.get(node)
|
||||
if (cached) return cached
|
||||
if (visiting.has(node)) {
|
||||
const start = stack.indexOf(node)
|
||||
const cycle = [...stack.slice(start), node].map((item) => `${item.kind}#${ids.get(item)}`).join(" -> ")
|
||||
throw new Error(`Cycle detected in app graph: ${cycle}`)
|
||||
}
|
||||
if (!ids.has(node)) ids.set(node, ids.size + 1)
|
||||
visiting.add(node)
|
||||
stack.push(node)
|
||||
try {
|
||||
const dependencies = node.dependencies.map(visit)
|
||||
const nonEmpty = dependencies as [RuntimeLayer, ...RuntimeLayer[]]
|
||||
const result =
|
||||
node.kind === "group"
|
||||
? dependencies.length === 0
|
||||
? Layer.empty
|
||||
: Layer.mergeAll(...nonEmpty)
|
||||
: dependencies.length === 0
|
||||
? (node.implementation as RuntimeLayer)
|
||||
: Layer.provide(node.implementation as RuntimeLayer, nonEmpty)
|
||||
cache.set(node, result)
|
||||
return result
|
||||
} finally {
|
||||
stack.pop()
|
||||
visiting.delete(node)
|
||||
}
|
||||
}
|
||||
|
||||
return visit(node) as unknown as Layer.Layer<A, E, never>
|
||||
}
|
||||
|
||||
export * as LayerNode from "./layer-node"
|
||||
@ -7,6 +7,7 @@ import { EventSequenceTable, EventTable } from "./event/sql"
|
||||
import { Location } from "./location"
|
||||
import { externalID, type ExternalID, NonNegativeInt, withStatics } from "./schema"
|
||||
import { Identifier } from "./util/identifier"
|
||||
import { LayerNode } from "./effect/layer-node"
|
||||
import { isDeepStrictEqual } from "node:util"
|
||||
|
||||
export const ID = Schema.String.check(Schema.isStartsWith("evt_")).pipe(
|
||||
@ -674,5 +675,6 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
)
|
||||
|
||||
export const layer = layerWith()
|
||||
export const node = LayerNode.make(layer, [Database.node])
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))
|
||||
|
||||
@ -11,6 +11,8 @@ import { CrossSpawnSpawner } from "../cross-spawn-spawner"
|
||||
import { Global } from "../global"
|
||||
import { NonNegativeInt } from "../schema"
|
||||
import { which } from "../util/which"
|
||||
import { LayerNode } from "../effect/layer-node"
|
||||
import { httpClient } from "../effect/layer-node-platform"
|
||||
|
||||
const VERSION = "15.1.0"
|
||||
const PLATFORM = {
|
||||
@ -480,5 +482,6 @@ export const defaultLayer = layer.pipe(
|
||||
Layer.provide(FSUtil.defaultLayer),
|
||||
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
||||
)
|
||||
export const node = LayerNode.make(layer, [FSUtil.node, CrossSpawnSpawner.node, httpClient])
|
||||
|
||||
export * as Ripgrep from "./ripgrep"
|
||||
|
||||
@ -8,6 +8,7 @@ import { serviceUse } from "../effect/service-use"
|
||||
import { makeRuntime } from "../effect/runtime"
|
||||
import { Fff } from "#fff"
|
||||
import { Ripgrep } from "./ripgrep"
|
||||
import { LayerNode } from "../effect/layer-node"
|
||||
|
||||
const root = path.join(Global.Path.cache, "fff")
|
||||
|
||||
@ -539,6 +540,7 @@ export const defaultLayer: Layer.Layer<Service> = layer.pipe(
|
||||
Layer.provide(Ripgrep.defaultLayer),
|
||||
Layer.provide(FSUtil.defaultLayer),
|
||||
)
|
||||
export const node = LayerNode.make(layer, [FSUtil.node, Ripgrep.node])
|
||||
|
||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||
|
||||
|
||||
@ -7,6 +7,8 @@ import { Context, Effect, FileSystem, Layer, Schema } from "effect"
|
||||
import type { PlatformError } from "effect/PlatformError"
|
||||
import { Glob } from "./util/glob"
|
||||
import { serviceUse } from "./effect/service-use"
|
||||
import { LayerNode } from "./effect/layer-node"
|
||||
import { filesystem } from "./effect/layer-node-platform"
|
||||
|
||||
export namespace FSUtil {
|
||||
export class FileSystemError extends Schema.TaggedErrorClass<FileSystemError>()("FileSystemError", {
|
||||
@ -194,6 +196,7 @@ export namespace FSUtil {
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(NodeFileSystem.layer))
|
||||
export const node = LayerNode.make(layer, [filesystem])
|
||||
|
||||
// Pure helpers that don't need Effect (path manipulation, sync operations)
|
||||
export function mimeType(p: string): string {
|
||||
|
||||
@ -6,6 +6,7 @@ import { ChildProcess } from "effect/unstable/process"
|
||||
import { AbsolutePath } from "./schema"
|
||||
import { FSUtil } from "./fs-util"
|
||||
import { AppProcess } from "./process"
|
||||
import { LayerNode } from "./effect/layer-node"
|
||||
|
||||
export interface Repo {
|
||||
/**
|
||||
@ -400,6 +401,7 @@ export const layer = Layer.effect(
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer), Layer.provide(AppProcess.defaultLayer))
|
||||
export const node = LayerNode.make(layer, [FSUtil.node, AppProcess.node])
|
||||
|
||||
export interface Result {
|
||||
readonly exitCode: number
|
||||
|
||||
@ -5,6 +5,7 @@ import os from "os"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { Flock } from "./util/flock"
|
||||
import { Flag } from "./flag/flag"
|
||||
import { LayerNode } from "./effect/layer-node"
|
||||
|
||||
const app = "opencode"
|
||||
const data = path.join(xdgData!, app)
|
||||
@ -76,6 +77,7 @@ export const layer = Layer.effect(
|
||||
)
|
||||
|
||||
export const defaultLayer = layer
|
||||
export const node = LayerNode.make(layer, [])
|
||||
|
||||
export const layerWith = (input: Partial<Interface>) =>
|
||||
Layer.effect(
|
||||
|
||||
@ -8,6 +8,8 @@ import { Hash } from "./util/hash"
|
||||
import { FSUtil } from "./fs-util"
|
||||
import { InstallationChannel, InstallationVersion } from "./installation/version"
|
||||
import { EventV2 } from "./event"
|
||||
import { LayerNode } from "./effect/layer-node"
|
||||
import { httpClient } from "./effect/layer-node-platform"
|
||||
|
||||
export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"])
|
||||
export type CatalogModelStatus = typeof CatalogModelStatus.Type
|
||||
@ -246,5 +248,6 @@ export const defaultLayer = layer.pipe(
|
||||
Layer.provide(FSUtil.defaultLayer),
|
||||
Layer.provide(EventV2.defaultLayer),
|
||||
)
|
||||
export const node = LayerNode.make(layer, [FSUtil.node, EventV2.node, httpClient])
|
||||
|
||||
export * as ModelsDev from "./models-dev"
|
||||
|
||||
@ -7,6 +7,8 @@ import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { FSUtil } from "./fs-util"
|
||||
import { Global } from "./global"
|
||||
import { EffectFlock } from "./util/effect-flock"
|
||||
import { LayerNode } from "./effect/layer-node"
|
||||
import { filesystem } from "./effect/layer-node-platform"
|
||||
import { makeRuntime } from "./effect/runtime"
|
||||
import { NpmConfig } from "./npm-config"
|
||||
|
||||
@ -250,6 +252,7 @@ export const defaultLayer = layer.pipe(
|
||||
Layer.provide(Global.layer),
|
||||
Layer.provide(NodeFileSystem.layer),
|
||||
)
|
||||
export const node = LayerNode.make(layer, [FSUtil.node, Global.node, filesystem, EffectFlock.node])
|
||||
|
||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import type { PlatformError } from "effect/PlatformError"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
|
||||
import { CrossSpawnSpawner } from "./cross-spawn-spawner"
|
||||
import { LayerNode } from "./effect/layer-node"
|
||||
|
||||
export class AppProcessError extends Schema.TaggedErrorClass<AppProcessError>()("AppProcessError", {
|
||||
command: Schema.String,
|
||||
@ -230,5 +231,6 @@ export const layer = Layer.effect(
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(CrossSpawnSpawner.defaultLayer))
|
||||
export const node = LayerNode.make(layer, [CrossSpawnSpawner.node])
|
||||
|
||||
export * as AppProcess from "./process"
|
||||
|
||||
@ -8,6 +8,7 @@ import { AbsolutePath, withStatics } from "./schema"
|
||||
import { FSUtil } from "./fs-util"
|
||||
import { Database } from "./database/database"
|
||||
import { Git } from "./git"
|
||||
import { LayerNode } from "./effect/layer-node"
|
||||
import { Hash } from "./util/hash"
|
||||
import { ProjectDirectoryTable } from "./project/sql"
|
||||
|
||||
@ -159,3 +160,4 @@ export const defaultLayer = layer.pipe(
|
||||
Layer.provide(FSUtil.defaultLayer),
|
||||
Layer.provide(Git.defaultLayer),
|
||||
)
|
||||
export const node = LayerNode.make(layer, [Database.node, FSUtil.node, Git.node])
|
||||
|
||||
@ -8,6 +8,7 @@ import { FSUtil } from "../fs-util"
|
||||
import { Git } from "../git"
|
||||
import { Database } from "../database/database"
|
||||
import { EventV2 } from "../event"
|
||||
import { LayerNode } from "../effect/layer-node"
|
||||
import { Project } from "../project"
|
||||
import { ProjectDirectoryTable } from "./sql"
|
||||
import { makeStrategies } from "./copy-strategies"
|
||||
@ -275,3 +276,4 @@ export const defaultLayer = layer.pipe(
|
||||
Layer.provide(Git.defaultLayer),
|
||||
Layer.provide(EventV2.defaultLayer),
|
||||
)
|
||||
export const node = LayerNode.make(layer, [FSUtil.node, Git.node, EventV2.node, Database.node])
|
||||
|
||||
@ -4,6 +4,7 @@ import { WorkspaceV2 } from "../workspace"
|
||||
import { PositiveInt } from "../schema"
|
||||
import { PtyID } from "./schema"
|
||||
import { Cache, Context, Duration, Effect, Layer, Schema } from "effect"
|
||||
import { LayerNode } from "../effect/layer-node"
|
||||
|
||||
const DEFAULT_TTL = Duration.seconds(60)
|
||||
const CAPACITY = 10_000
|
||||
@ -56,3 +57,4 @@ export const make = (ttl: Duration.Input = DEFAULT_TTL) =>
|
||||
export const layer = Layer.effect(Service, make())
|
||||
|
||||
export const defaultLayer = layer
|
||||
export const node = LayerNode.make(layer, [])
|
||||
|
||||
@ -4,6 +4,7 @@ import { and, desc, eq, sql } from "drizzle-orm"
|
||||
import { DateTime, Effect, Layer, Schema } from "effect"
|
||||
import { Database } from "../database/database"
|
||||
import { EventV2 } from "../event"
|
||||
import { LayerNode } from "../effect/layer-node"
|
||||
import { SessionEvent } from "./event"
|
||||
import { SessionV1 } from "../v1/session"
|
||||
import { WorkspaceTable } from "../control-plane/workspace.sql"
|
||||
@ -447,3 +448,4 @@ export const layer = Layer.effectDiscard(
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(EventV2.defaultLayer), Layer.provide(Database.defaultLayer))
|
||||
export const node = LayerNode.make(layer, [EventV2.node, Database.node])
|
||||
|
||||
@ -6,6 +6,7 @@ import type { FileSystem, Scope } from "effect"
|
||||
import type { PlatformError } from "effect/PlatformError"
|
||||
import { FSUtil } from "../fs-util"
|
||||
import { Global } from "../global"
|
||||
import { LayerNode } from "../effect/layer-node"
|
||||
import { Hash } from "./hash"
|
||||
|
||||
export namespace EffectFlock {
|
||||
@ -280,4 +281,5 @@ export namespace EffectFlock {
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer), Layer.provide(Global.layer))
|
||||
export const node = LayerNode.make(layer, [Global.node, FSUtil.node])
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
|
||||
import { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, Context } from "effect"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import {
|
||||
@ -456,4 +458,6 @@ export const layer: Layer.Layer<Service, never, AccountRepo.Service | HttpClient
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(AccountRepo.defaultLayer), Layer.provide(FetchHttpClient.layer))
|
||||
|
||||
export const node = LayerNode.make(layer, [AccountRepo.node, httpClient])
|
||||
|
||||
export * as Account from "./account"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import { Effect, Layer, Option, Schema, Context } from "effect"
|
||||
@ -167,4 +168,6 @@ export const layer = Layer.effect(
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))
|
||||
|
||||
export const node = LayerNode.make(layer, [Database.node])
|
||||
|
||||
export * as AccountRepo from "./repo"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
|
||||
import { Config } from "@/config/config"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
@ -430,4 +431,6 @@ export const defaultLayer = layer.pipe(
|
||||
Layer.provide(Skill.defaultLayer),
|
||||
)
|
||||
|
||||
export const node = LayerNode.make(layer, [Config.node, Auth.node, Plugin.node, Skill.node, Provider.node])
|
||||
|
||||
export * as Agent from "./agent"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import path from "path"
|
||||
import { Effect, Layer, Record, Result, Schema, Context } from "effect"
|
||||
import { NonNegativeInt } from "@opencode-ai/core/schema"
|
||||
@ -93,4 +94,6 @@ export const layer = Layer.effect(
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer))
|
||||
|
||||
export const node = LayerNode.make(layer, [FSUtil.node])
|
||||
|
||||
export * as Auth from "."
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { BackgroundJob as CoreBackgroundJob } from "@opencode-ai/core/background-job"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { Effect, Layer } from "effect"
|
||||
@ -33,4 +34,6 @@ export const layer = Layer.effect(
|
||||
|
||||
export const defaultLayer = layer
|
||||
|
||||
export const node = LayerNode.make(layer, [])
|
||||
|
||||
export * as BackgroundJob from "./job"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { EffectBridge } from "@/effect/bridge"
|
||||
import type { InstanceContext } from "@/project/instance-context"
|
||||
@ -178,4 +179,6 @@ export const defaultLayer = layer.pipe(
|
||||
Layer.provide(Skill.defaultLayer),
|
||||
)
|
||||
|
||||
export const node = LayerNode.make(layer, [Config.node, MCP.node, Skill.node])
|
||||
|
||||
export * as Command from "."
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
@ -669,4 +671,6 @@ export const defaultLayer = layer.pipe(
|
||||
Layer.provide(FetchHttpClient.layer),
|
||||
)
|
||||
|
||||
export const node = LayerNode.make(layer, [FSUtil.node, Auth.node, Account.node, Env.node, Npm.node, httpClient])
|
||||
|
||||
export * as Config from "./config"
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
|
||||
import { Context, Effect, FiberMap, Iterable, Layer, Schema, Stream } from "effect"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import { FetchHttpClient, HttpBody, HttpClient, HttpClientError, HttpClientRequest } from "effect/unstable/http"
|
||||
@ -972,4 +974,16 @@ function route(url: string | URL, path: string) {
|
||||
return next
|
||||
}
|
||||
|
||||
export const node = LayerNode.make(layer, [
|
||||
Auth.node,
|
||||
Session.node,
|
||||
SessionPrompt.node,
|
||||
httpClient,
|
||||
EventV2Bridge.node,
|
||||
Vcs.node,
|
||||
RuntimeFlags.node,
|
||||
FSUtil.node,
|
||||
Database.node,
|
||||
])
|
||||
|
||||
export * as Workspace from "./workspace"
|
||||
|
||||
@ -73,4 +73,7 @@ export const layer = (overrides: Partial<Info> = {}) =>
|
||||
|
||||
export const defaultLayer = Service.defaultLayer.pipe(Layer.orDie)
|
||||
|
||||
export const node = LayerNode.make(defaultLayer, [])
|
||||
|
||||
export * as RuntimeFlags from "./runtime-flags"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
|
||||
3
packages/opencode/src/env/index.ts
vendored
3
packages/opencode/src/env/index.ts
vendored
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
@ -37,4 +38,6 @@ export const layer = Layer.effect(
|
||||
|
||||
export const defaultLayer = layer
|
||||
|
||||
export const node = LayerNode.make(layer, [])
|
||||
|
||||
export * as Env from "."
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
// Opencode publish boundary for core events. Attach routed instance location
|
||||
// so direct EventV2 consumers can isolate directory/workspace streams.
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
@ -73,4 +74,6 @@ export const layer = Layer.effect(
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(EventV2.defaultLayer))
|
||||
|
||||
export const node = LayerNode.make(layer, [EventV2.node])
|
||||
|
||||
export * as EventV2Bridge from "./event-v2-bridge"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Effect, Layer, Context, Schema } from "effect"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
@ -199,4 +200,6 @@ export const defaultLayer = layer.pipe(
|
||||
Layer.provide(RuntimeFlags.defaultLayer),
|
||||
)
|
||||
|
||||
export const node = LayerNode.make(layer, [Config.node, AppProcess.node, RuntimeFlags.node])
|
||||
|
||||
export * as Format from "."
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { AppProcess } from "@opencode-ai/core/process"
|
||||
import { Effect, Layer, Context, Stream } from "effect"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
@ -344,4 +345,6 @@ export const layer = Layer.effect(
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(AppProcess.defaultLayer))
|
||||
|
||||
export const node = LayerNode.make(layer, [AppProcess.node])
|
||||
|
||||
export * as Git from "."
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Config } from "@/config/config"
|
||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||
import type { MessageV2 } from "@/session/message-v2"
|
||||
@ -168,4 +169,6 @@ export const layer = Layer.effect(
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer))
|
||||
|
||||
export const node = LayerNode.make(layer, [Config.node])
|
||||
|
||||
export * as Image from "./image"
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
|
||||
import { Effect, Layer, Schema, Context, Stream } from "effect"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
@ -343,4 +345,6 @@ export const latest = (...args: Parameters<Interface["latest"]>) => runPromise((
|
||||
export const method = () => runPromise((s) => s.method())
|
||||
export const upgrade = (...args: Parameters<Interface["upgrade"]>) => runPromise((s) => s.upgrade(...args))
|
||||
|
||||
export const node = LayerNode.make(layer, [httpClient, AppProcess.node])
|
||||
|
||||
export * as Installation from "."
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import * as LSPClient from "./client"
|
||||
@ -504,4 +506,6 @@ export const defaultLayer = layer.pipe(
|
||||
|
||||
export * as Diagnostic from "./diagnostic"
|
||||
|
||||
export const node = LayerNode.make(layer, [Config.node, RuntimeFlags.node, FSUtil.node, EventV2Bridge.node])
|
||||
|
||||
export * as LSP from "./lsp"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import path from "path"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
@ -168,4 +169,6 @@ export const layer = Layer.effect(
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(EffectFlock.defaultLayer), Layer.provide(FSUtil.defaultLayer))
|
||||
|
||||
export const node = LayerNode.make(layer, [FSUtil.node, EffectFlock.node])
|
||||
|
||||
export * as McpAuth from "./auth"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { dynamicTool, type Tool, jsonSchema, type JSONSchema7 } from "ai"
|
||||
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
@ -1003,4 +1004,6 @@ export const defaultLayer = layer.pipe(
|
||||
Layer.provide(FSUtil.defaultLayer),
|
||||
)
|
||||
|
||||
export const node = LayerNode.make(layer, [CrossSpawnSpawner.node, McpAuth.node, EventV2Bridge.node, Config.node])
|
||||
|
||||
export * as MCP from "."
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { ConfigPermissionV1 } from "@opencode-ai/core/v1/config/permission"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { Wildcard } from "@opencode-ai/core/util/wildcard"
|
||||
@ -224,4 +225,6 @@ export function disabled(tools: string[], ruleset: PermissionV1.Ruleset): Set<st
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(EventV2Bridge.defaultLayer))
|
||||
|
||||
export const node = LayerNode.make(layer, [EventV2Bridge.node])
|
||||
|
||||
export * as Permission from "."
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import type {
|
||||
Hooks,
|
||||
PluginInput,
|
||||
@ -307,4 +308,6 @@ export const defaultLayer = layer.pipe(
|
||||
Layer.provide(RuntimeFlags.defaultLayer),
|
||||
)
|
||||
|
||||
export const node = LayerNode.make(layer, [EventV2Bridge.node, Config.node, RuntimeFlags.node])
|
||||
|
||||
export * as Plugin from "."
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Plugin } from "../plugin"
|
||||
import { Format } from "../format"
|
||||
import { LSP } from "@/lsp/lsp"
|
||||
@ -74,4 +75,16 @@ export const defaultLayer: Layer.Layer<Service> = layer.pipe(
|
||||
]),
|
||||
)
|
||||
|
||||
export const node = LayerNode.make(layer, [
|
||||
Config.node,
|
||||
Format.node,
|
||||
LSP.node,
|
||||
Plugin.node,
|
||||
Project.node,
|
||||
Search.node,
|
||||
ShareNext.node,
|
||||
Snapshot.node,
|
||||
Vcs.node,
|
||||
])
|
||||
|
||||
export * as InstanceBootstrap from "./bootstrap"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||
@ -7,6 +8,7 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { Context, Deferred, Duration, Effect, Exit, Layer, Scope } from "effect"
|
||||
import { type InstanceContext } from "./instance-context"
|
||||
import { InstanceBootstrap } from "./bootstrap-service"
|
||||
import { InstanceBootstrap as InstanceBootstrapGraph } from "./bootstrap"
|
||||
import * as Project from "./project"
|
||||
|
||||
export interface LoadInput {
|
||||
@ -202,4 +204,6 @@ export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootst
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(Project.defaultLayer))
|
||||
|
||||
export const node = LayerNode.make(layer, [Project.node, InstanceBootstrapGraph.node])
|
||||
|
||||
export * as InstanceStore from "./instance-store"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { and, eq, sql } from "drizzle-orm"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { ProjectDirectoryTable, ProjectTable } from "@opencode-ai/core/project/sql"
|
||||
@ -514,4 +515,15 @@ export const defaultLayer = layer.pipe(
|
||||
|
||||
export const use = serviceUse(Service)
|
||||
|
||||
export const node = LayerNode.make(layer, [
|
||||
FSUtil.node,
|
||||
AppProcess.node,
|
||||
CrossSpawnSpawner.node,
|
||||
ProjectV2.node,
|
||||
ProjectCopy.node,
|
||||
EventV2Bridge.node,
|
||||
RuntimeFlags.node,
|
||||
Database.node,
|
||||
])
|
||||
|
||||
export * as Project from "./project"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Effect, Layer, Context, Schema, Stream, Scope } from "effect"
|
||||
import { formatPatch, structuredPatch } from "diff"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
@ -425,4 +426,6 @@ export const layer: Layer.Layer<Service, never, Git.Service | EventV2Bridge.Serv
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(Git.defaultLayer), Layer.provide(EventV2Bridge.defaultLayer))
|
||||
|
||||
export const node = LayerNode.make(layer, [Git.node, EventV2Bridge.node])
|
||||
|
||||
export * as Vcs from "./vcs"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import { Auth } from "@/auth"
|
||||
@ -227,4 +228,6 @@ export const defaultLayer = Layer.suspend(() =>
|
||||
layer.pipe(Layer.provide(Auth.defaultLayer), Layer.provide(Plugin.defaultLayer)),
|
||||
)
|
||||
|
||||
export const node = LayerNode.make(layer, [Auth.node, Plugin.node])
|
||||
|
||||
export * as ProviderAuth from "./auth"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import os from "os"
|
||||
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
|
||||
import fuzzysort from "fuzzysort"
|
||||
@ -1948,4 +1949,14 @@ export function parseModel(model: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export const node = LayerNode.make(layer, [
|
||||
FSUtil.node,
|
||||
Config.node,
|
||||
Auth.node,
|
||||
Env.node,
|
||||
Plugin.node,
|
||||
ModelsDev.node,
|
||||
RuntimeFlags.node,
|
||||
])
|
||||
|
||||
export * as Provider from "./provider"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Deferred, Effect, Layer, Schema, Context } from "effect"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { SessionID, MessageID } from "@/session/schema"
|
||||
@ -223,4 +224,6 @@ export const layer = Layer.effect(
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(EventV2Bridge.defaultLayer))
|
||||
|
||||
export const node = LayerNode.make(layer, [EventV2Bridge.node])
|
||||
|
||||
export * as Question from "."
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
|
||||
import { Session } from "./session"
|
||||
@ -605,4 +606,15 @@ export const defaultLayer = Layer.suspend(() =>
|
||||
),
|
||||
)
|
||||
|
||||
export const node = LayerNode.make(layer, [
|
||||
Config.node,
|
||||
Session.node,
|
||||
Agent.node,
|
||||
Plugin.node,
|
||||
SessionProcessor.node,
|
||||
Provider.node,
|
||||
EventV2Bridge.node,
|
||||
RuntimeFlags.node,
|
||||
])
|
||||
|
||||
export * as SessionCompaction from "./compaction"
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
|
||||
import path from "path"
|
||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
@ -234,4 +236,6 @@ export function loaded(messages: SessionV1.WithParts[]) {
|
||||
return extract(messages)
|
||||
}
|
||||
|
||||
export const node = LayerNode.make(layer, [Config.node, FSUtil.node, Global.node, RuntimeFlags.node, httpClient])
|
||||
|
||||
export * as Instruction from "./instruction"
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { llmClient } from "@opencode-ai/core/effect/layer-node-platform"
|
||||
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||
@ -399,4 +401,15 @@ export const defaultLayer = Layer.suspend(() =>
|
||||
|
||||
export const hasToolCalls = LLMRequestPrep.hasToolCalls
|
||||
|
||||
export const node = LayerNode.make(layer, [
|
||||
Auth.node,
|
||||
Config.node,
|
||||
Provider.node,
|
||||
Plugin.node,
|
||||
Permission.node,
|
||||
EventV2Bridge.node,
|
||||
llmClient,
|
||||
RuntimeFlags.node,
|
||||
])
|
||||
|
||||
export * as LLM from "./llm"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
|
||||
import { Image } from "@/image/image"
|
||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||
@ -1064,4 +1065,20 @@ export const defaultLayer = Layer.suspend(() =>
|
||||
),
|
||||
)
|
||||
|
||||
export const node = LayerNode.make(layer, [
|
||||
Session.node,
|
||||
Config.node,
|
||||
Snapshot.node,
|
||||
Agent.node,
|
||||
LLM.node,
|
||||
Permission.node,
|
||||
Plugin.node,
|
||||
SessionSummary.node,
|
||||
SessionStatus.node,
|
||||
Image.node,
|
||||
EventV2Bridge.node,
|
||||
RuntimeFlags.node,
|
||||
Database.node,
|
||||
])
|
||||
|
||||
export * as SessionProcessor from "./processor"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
|
||||
import path from "path"
|
||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||
@ -1671,4 +1672,33 @@ const argsRegex = /(?:\[Image\s+\d+\]|"[^"]*"|'[^']*'|[^\s"']+)/gi
|
||||
const placeholderRegex = /\$(\d+)/g
|
||||
const quoteTrimRegex = /^["']|["']$/g
|
||||
|
||||
export const node = LayerNode.make(layer, [
|
||||
SessionStatus.node,
|
||||
Session.node,
|
||||
Agent.node,
|
||||
Provider.node,
|
||||
SessionProcessor.node,
|
||||
SessionCompaction.node,
|
||||
Plugin.node,
|
||||
Command.node,
|
||||
Config.node,
|
||||
Permission.node,
|
||||
FSUtil.node,
|
||||
MCP.node,
|
||||
LSP.node,
|
||||
ToolRegistry.node,
|
||||
Truncate.node,
|
||||
Image.node,
|
||||
CrossSpawnSpawner.node,
|
||||
Instruction.node,
|
||||
SessionRunState.node,
|
||||
SessionRevert.node,
|
||||
SessionSummary.node,
|
||||
SystemPrompt.node,
|
||||
LLM.node,
|
||||
EventV2Bridge.node,
|
||||
RuntimeFlags.node,
|
||||
Database.node,
|
||||
])
|
||||
|
||||
export * as SessionPrompt from "./prompt"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Effect, Layer, Context, Schema } from "effect"
|
||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
@ -147,4 +148,13 @@ export const defaultLayer = Layer.suspend(() =>
|
||||
),
|
||||
)
|
||||
|
||||
export const node = LayerNode.make(layer, [
|
||||
Session.node,
|
||||
Snapshot.node,
|
||||
Storage.node,
|
||||
EventV2Bridge.node,
|
||||
SessionSummary.node,
|
||||
SessionRunState.node,
|
||||
])
|
||||
|
||||
export * as SessionRevert from "./revert"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||
import { Runner } from "@/effect/runner"
|
||||
@ -150,4 +151,6 @@ function busyError(sessionID: SessionID) {
|
||||
return new Session.BusyError({ sessionID })
|
||||
}
|
||||
|
||||
export const node = LayerNode.make(layer, [BackgroundJob.node, SessionStatus.node])
|
||||
|
||||
export * as SessionRunState from "./run-state"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
|
||||
import { Slug } from "@opencode-ai/core/util/slug"
|
||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||
@ -1113,4 +1114,6 @@ export function* listGlobal(input?: {
|
||||
}
|
||||
}
|
||||
|
||||
export const node = LayerNode.make(layer, [BackgroundJob.node, RuntimeFlags.node, Database.node, EventV2Bridge.node])
|
||||
|
||||
export * as Session from "./session"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { SessionID } from "./schema"
|
||||
import { NonNegativeInt } from "@opencode-ai/core/schema"
|
||||
@ -91,4 +92,6 @@ export const layer = Layer.effect(
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(EventV2Bridge.defaultLayer))
|
||||
|
||||
export const node = LayerNode.make(layer, [EventV2Bridge.node])
|
||||
|
||||
export * as SessionStatus from "./status"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Effect, Layer, Context, Schema } from "effect"
|
||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
@ -159,4 +160,6 @@ export const DiffInput = Schema.Struct({
|
||||
})
|
||||
export type DiffInput = Schema.Schema.Type<typeof DiffInput>
|
||||
|
||||
export const node = LayerNode.make(layer, [Session.node, Snapshot.node, EventV2Bridge.node, Config.node])
|
||||
|
||||
export * as SessionSummary from "./summary"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
@ -81,4 +82,6 @@ export const layer = Layer.effect(
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(Skill.defaultLayer))
|
||||
|
||||
export const node = LayerNode.make(layer, [Skill.node])
|
||||
|
||||
export * as SystemPrompt from "./system"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { SessionID } from "./schema"
|
||||
import { Effect, Layer, Context, Schema } from "effect"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
@ -84,4 +85,6 @@ export const layer = Layer.effect(
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(EventV2Bridge.defaultLayer), Layer.provide(Database.defaultLayer))
|
||||
|
||||
export const node = LayerNode.make(layer, [EventV2Bridge.node, Database.node])
|
||||
|
||||
export * as Todo from "./todo"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Session } from "@/session/session"
|
||||
import { SessionID } from "@/session/schema"
|
||||
import { Effect, Layer, Scope, Context } from "effect"
|
||||
@ -55,4 +56,6 @@ export const defaultLayer = layer.pipe(
|
||||
Layer.provide(RuntimeFlags.defaultLayer),
|
||||
)
|
||||
|
||||
export const node = LayerNode.make(layer, [Config.node, Session.node, ShareNext.node, RuntimeFlags.node])
|
||||
|
||||
export * as SessionShare from "./session"
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
|
||||
import type * as SDK from "@opencode-ai/sdk/v2"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import { Effect, Exit, Layer, Option, Schema, Scope, Context, Stream } from "effect"
|
||||
@ -370,4 +372,14 @@ export const defaultLayer = layer.pipe(
|
||||
Layer.provide(Session.defaultLayer),
|
||||
)
|
||||
|
||||
export const node = LayerNode.make(layer, [
|
||||
Account.node,
|
||||
EventV2Bridge.node,
|
||||
Config.node,
|
||||
Database.node,
|
||||
httpClient,
|
||||
Provider.node,
|
||||
Session.node,
|
||||
])
|
||||
|
||||
export * as ShareNext from "./share-next"
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { httpClient, path } from "@opencode-ai/core/effect/layer-node-platform"
|
||||
import { NodePath } from "@effect/platform-node"
|
||||
import { Effect, Layer, Path, Schema, Context } from "effect"
|
||||
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
@ -102,4 +104,6 @@ export const defaultLayer: Layer.Layer<Service> = layer.pipe(
|
||||
Layer.provide(NodePath.layer),
|
||||
)
|
||||
|
||||
export const node = LayerNode.make(layer, [FSUtil.node, path, httpClient])
|
||||
|
||||
export * as Discovery from "./discovery"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
import { Effect, Layer, Context, Schema } from "effect"
|
||||
@ -353,4 +354,13 @@ export function fmt(list: Info[], opts: { verbose: boolean }) {
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
export const node = LayerNode.make(layer, [
|
||||
Discovery.node,
|
||||
Config.node,
|
||||
EventV2Bridge.node,
|
||||
FSUtil.node,
|
||||
Global.node,
|
||||
RuntimeFlags.node,
|
||||
])
|
||||
|
||||
export * as Skill from "."
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Cause, Duration, Effect, Layer, Schedule, Schema, Semaphore, Context } from "effect"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
import { formatPatch, structuredPatch } from "diff"
|
||||
@ -756,4 +757,6 @@ export const defaultLayer = layer.pipe(
|
||||
Layer.provide(Config.defaultLayer),
|
||||
)
|
||||
|
||||
export const node = LayerNode.make(layer, [FSUtil.node, AppProcess.node, Config.node])
|
||||
|
||||
export * as Snapshot from "."
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import path from "path"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
@ -323,4 +324,6 @@ export const layer = Layer.effect(
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer), Layer.provide(Git.defaultLayer))
|
||||
|
||||
export const node = LayerNode.make(layer, [FSUtil.node, Git.node])
|
||||
|
||||
export * as Storage from "./storage"
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
|
||||
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
|
||||
import { PlanExitTool } from "./plan"
|
||||
import { Session } from "@/session/session"
|
||||
import { QuestionTool } from "./question"
|
||||
@ -437,4 +440,28 @@ function isJsonSchemaObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
export const node = LayerNode.make(layer, [
|
||||
Config.node,
|
||||
Plugin.node,
|
||||
Question.node,
|
||||
Todo.node,
|
||||
Agent.node,
|
||||
Skill.node,
|
||||
Session.node,
|
||||
BackgroundJob.node,
|
||||
Provider.node,
|
||||
LSP.node,
|
||||
Instruction.node,
|
||||
FSUtil.node,
|
||||
EventV2Bridge.node,
|
||||
httpClient,
|
||||
CrossSpawnSpawner.node,
|
||||
Ripgrep.node,
|
||||
Search.node,
|
||||
Format.node,
|
||||
Truncate.node,
|
||||
RuntimeFlags.node,
|
||||
Database.node,
|
||||
])
|
||||
|
||||
export * as ToolRegistry from "./registry"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { NodePath } from "@effect/platform-node"
|
||||
import { Cause, Duration, Effect, Layer, Option, Schedule, Context } from "effect"
|
||||
import path from "path"
|
||||
@ -152,4 +153,6 @@ export const layer = Layer.effect(
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer), Layer.provide(NodePath.layer))
|
||||
|
||||
export const node = LayerNode.make(layer, [FSUtil.node])
|
||||
|
||||
export * as Truncate from "./truncate"
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { path } from "@opencode-ai/core/effect/layer-node-platform"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { InstanceLayer } from "@/project/instance-layer"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
@ -639,4 +641,14 @@ export const appLayer = layer.pipe(
|
||||
|
||||
export const defaultLayer = appLayer.pipe(Layer.provide(InstanceLayer.layer))
|
||||
|
||||
export const node = LayerNode.make(layer, [
|
||||
FSUtil.node,
|
||||
path,
|
||||
AppProcess.node,
|
||||
Git.node,
|
||||
Project.node,
|
||||
InstanceStore.node,
|
||||
Database.node,
|
||||
])
|
||||
|
||||
export * as Worktree from "."
|
||||
|
||||
101
packages/opencode/test/effect/app-graph-types.test.ts
Normal file
101
packages/opencode/test/effect/app-graph-types.test.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { test } from "bun:test"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
|
||||
class A extends Context.Service<A, { readonly value: "a" }>()("test/A") {}
|
||||
class B extends Context.Service<B, { readonly value: "b" }>()("test/B") {}
|
||||
class C extends Context.Service<C, { readonly value: "c" }>()("test/C") {}
|
||||
class LayerError {
|
||||
readonly _tag = "LayerError"
|
||||
}
|
||||
class NotFoundError {
|
||||
readonly _tag = "NotFoundError"
|
||||
}
|
||||
class DiskError {
|
||||
readonly _tag = "DiskError"
|
||||
}
|
||||
class NetworkError {
|
||||
readonly _tag = "NetworkError"
|
||||
}
|
||||
|
||||
const aImplementation = Layer.succeed(A, A.of({ value: "a" }))
|
||||
const bImplementation = Layer.effect(B, Effect.gen(function* () {
|
||||
yield* A
|
||||
return B.of({ value: "b" })
|
||||
}))
|
||||
const cImplementation = Layer.effect(C, Effect.gen(function* () {
|
||||
yield* A
|
||||
yield* B
|
||||
return C.of({ value: "c" })
|
||||
}))
|
||||
const failingAImplementation = Layer.effect(A, Effect.fail(new LayerError()))
|
||||
const notFoundAImplementation = Layer.effect(A, Effect.fail(new NotFoundError()))
|
||||
const diskAImplementation = Layer.effect(A, Effect.fail(new DiskError()))
|
||||
const networkAImplementation = Layer.effect(A, Effect.fail(new NetworkError()))
|
||||
const notFoundOrDiskAImplementation = Layer.effect(
|
||||
A,
|
||||
Effect.fail(new NotFoundError() as NotFoundError | DiskError),
|
||||
)
|
||||
|
||||
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false
|
||||
type Assert<T extends true> = T
|
||||
|
||||
type AProvides = Assert<Equal<Layer.Success<typeof aImplementation>, A>>
|
||||
type ARequires = Assert<Equal<Layer.Services<typeof aImplementation>, never>>
|
||||
type BProvides = Assert<Equal<Layer.Success<typeof bImplementation>, B>>
|
||||
type BRequires = Assert<Equal<Layer.Services<typeof bImplementation>, A>>
|
||||
type CRequires = Assert<Equal<Layer.Services<typeof cImplementation>, A | B>>
|
||||
void (0 as unknown as AProvides)
|
||||
void (0 as unknown as ARequires)
|
||||
void (0 as unknown as BProvides)
|
||||
void (0 as unknown as BRequires)
|
||||
void (0 as unknown as CRequires)
|
||||
|
||||
const a = LayerNode.make(aImplementation, [])
|
||||
const b = LayerNode.make(bImplementation, [a])
|
||||
const c = LayerNode.make(cImplementation, [a, b])
|
||||
const failingA = LayerNode.make(failingAImplementation, [])
|
||||
const bWithFailingA = LayerNode.make(bImplementation, [failingA])
|
||||
const notFoundA = LayerNode.make(notFoundAImplementation, [])
|
||||
const diskA = LayerNode.make(diskAImplementation, [])
|
||||
const networkA = LayerNode.make(networkAImplementation, [])
|
||||
const notFoundOrDiskA = LayerNode.make(notFoundOrDiskAImplementation, [])
|
||||
|
||||
// @ts-expect-error B requires A
|
||||
LayerNode.make(bImplementation, [])
|
||||
|
||||
// @ts-expect-error C requires both A and B
|
||||
LayerNode.make(cImplementation, [a])
|
||||
|
||||
type ANodeProvides = Assert<Equal<typeof a, LayerNode.Node<A, never>>>
|
||||
type BNodeProvides = Assert<Equal<typeof b, LayerNode.Node<B, never>>>
|
||||
type CNodeProvides = Assert<Equal<typeof c, LayerNode.Node<C, never>>>
|
||||
type FailingANodeError = Assert<Equal<typeof failingA, LayerNode.Node<A, LayerError>>>
|
||||
type DependentNodeError = Assert<Equal<typeof bWithFailingA, LayerNode.Node<B, LayerError>>>
|
||||
void (0 as unknown as ANodeProvides)
|
||||
void (0 as unknown as BNodeProvides)
|
||||
void (0 as unknown as CNodeProvides)
|
||||
void (0 as unknown as FailingANodeError)
|
||||
void (0 as unknown as DependentNodeError)
|
||||
|
||||
const closed = LayerNode.buildLayer(c)
|
||||
const closedWithError = LayerNode.buildLayer(bWithFailingA)
|
||||
type ClosedProvides = Assert<Equal<Layer.Success<typeof closed>, C>>
|
||||
type ClosedRequires = Assert<Equal<Layer.Services<typeof closed>, never>>
|
||||
type ClosedError = Assert<Equal<Layer.Error<typeof closedWithError>, LayerError>>
|
||||
void (0 as unknown as ClosedProvides)
|
||||
void (0 as unknown as ClosedRequires)
|
||||
void (0 as unknown as ClosedError)
|
||||
|
||||
const replacement = LayerNode.make(Layer.succeed(A, A.of({ value: "a" })), [])
|
||||
LayerNode.replace(a, replacement)
|
||||
LayerNode.replace(notFoundOrDiskA, notFoundA)
|
||||
LayerNode.replace(notFoundOrDiskA, diskA)
|
||||
|
||||
// @ts-expect-error An override for A must still provide A
|
||||
LayerNode.replace(a, b)
|
||||
|
||||
// @ts-expect-error A replacement cannot introduce NetworkError
|
||||
LayerNode.replace(notFoundOrDiskA, networkA)
|
||||
|
||||
test("type exploration compiles", () => {})
|
||||
208
packages/opencode/test/effect/app-graph.test.ts
Normal file
208
packages/opencode/test/effect/app-graph.test.ts
Normal file
@ -0,0 +1,208 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Cause, Context, Effect, Exit, Layer } from "effect"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
|
||||
const { buildLayer: build, group, replace } = LayerNode
|
||||
const node = LayerNode.make
|
||||
|
||||
class Value extends Context.Service<Value, { readonly value: string }>()("test/Value") {}
|
||||
class Greeting extends Context.Service<Greeting, { readonly text: string }>()("test/Greeting") {}
|
||||
|
||||
const value = LayerNode.make(Layer.succeed(Value, Value.of({ value: "production" })), [])
|
||||
const greetingImplementation = Layer.effect(
|
||||
Greeting,
|
||||
Effect.gen(function* () {
|
||||
return Greeting.of({ text: `hello ${(yield* Value).value}` })
|
||||
}),
|
||||
)
|
||||
const greeting = LayerNode.make(greetingImplementation, [value])
|
||||
|
||||
// @ts-expect-error Greeting requires Value
|
||||
LayerNode.make(greetingImplementation, [])
|
||||
|
||||
describe("app graph", () => {
|
||||
test("creates any selected dependency layer", async () => {
|
||||
const result = Effect.gen(function* () {
|
||||
return (yield* Greeting).text
|
||||
}).pipe(Effect.provide(build(greeting)))
|
||||
|
||||
expect(await Effect.runPromise(result)).toBe("hello production")
|
||||
})
|
||||
|
||||
test("applies overrides before dependency materialization", async () => {
|
||||
const replacement = node(Layer.succeed(Value, Value.of({ value: "simulation" })), [])
|
||||
const graph = build(greeting, { replacements: [replace(value, replacement)] })
|
||||
const result = Effect.gen(function* () {
|
||||
return (yield* Greeting).text
|
||||
}).pipe(Effect.provide(graph))
|
||||
|
||||
expect(await Effect.runPromise(result)).toBe("hello simulation")
|
||||
})
|
||||
|
||||
test("acquires a shared dependency once", async () => {
|
||||
class Shared extends Context.Service<Shared, { readonly value: string }>()("test/Shared") {}
|
||||
class Left extends Context.Service<Left, { readonly value: string }>()("test/Left") {}
|
||||
class Right extends Context.Service<Right, { readonly value: string }>()("test/Right") {}
|
||||
let acquisitions = 0
|
||||
const shared = node(
|
||||
Layer.effect(
|
||||
Shared,
|
||||
Effect.sync(() => {
|
||||
acquisitions++
|
||||
return Shared.of({ value: "shared" })
|
||||
}),
|
||||
),
|
||||
[],
|
||||
)
|
||||
const left = node(
|
||||
Layer.effect(
|
||||
Left,
|
||||
Effect.gen(function* () {
|
||||
return Left.of({ value: `${(yield* Shared).value}-left` })
|
||||
}),
|
||||
),
|
||||
[shared],
|
||||
)
|
||||
const right = node(
|
||||
Layer.effect(
|
||||
Right,
|
||||
Effect.gen(function* () {
|
||||
return Right.of({ value: `${(yield* Shared).value}-right` })
|
||||
}),
|
||||
),
|
||||
[shared],
|
||||
)
|
||||
|
||||
const result = Effect.gen(function* () {
|
||||
return [(yield* Left).value, (yield* Right).value]
|
||||
}).pipe(Effect.provide(build(group([left, right]))))
|
||||
|
||||
expect(await Effect.runPromise(result)).toEqual(["shared-left", "shared-right"])
|
||||
expect(acquisitions).toBe(1)
|
||||
})
|
||||
|
||||
test("applies a replacement to every transitive consumer", async () => {
|
||||
class Left extends Context.Service<Left, { readonly value: string }>()("test/ReplacementLeft") {}
|
||||
class Right extends Context.Service<Right, { readonly value: string }>()("test/ReplacementRight") {}
|
||||
const left = node(
|
||||
Layer.effect(
|
||||
Left,
|
||||
Effect.gen(function* () {
|
||||
return Left.of({ value: (yield* Value).value })
|
||||
}),
|
||||
),
|
||||
[value],
|
||||
)
|
||||
const right = node(
|
||||
Layer.effect(
|
||||
Right,
|
||||
Effect.gen(function* () {
|
||||
return Right.of({ value: (yield* Value).value })
|
||||
}),
|
||||
),
|
||||
[value],
|
||||
)
|
||||
const replacement = node(Layer.succeed(Value, Value.of({ value: "simulation" })), [])
|
||||
const graph = build(group([left, right]), { replacements: [replace(value, replacement)] })
|
||||
|
||||
const result = Effect.gen(function* () {
|
||||
return [(yield* Left).value, (yield* Right).value]
|
||||
}).pipe(Effect.provide(graph))
|
||||
|
||||
expect(await Effect.runPromise(result)).toEqual(["simulation", "simulation"])
|
||||
})
|
||||
|
||||
test("propagates layer acquisition errors", async () => {
|
||||
class AcquisitionError {
|
||||
readonly _tag = "AcquisitionError"
|
||||
}
|
||||
const failing = node(Layer.effect(Value, Effect.fail(new AcquisitionError())), [])
|
||||
const exit = await Effect.runPromiseExit(Effect.provide(Value, build(failing)))
|
||||
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(AcquisitionError)
|
||||
})
|
||||
|
||||
test("groups expose every selected service", async () => {
|
||||
class Count extends Context.Service<Count, { readonly value: number }>()("test/Count") {}
|
||||
const count = node(Layer.succeed(Count, Count.of({ value: 3 })), [])
|
||||
const result = Effect.gen(function* () {
|
||||
return { text: (yield* Value).value, count: (yield* Count).value }
|
||||
}).pipe(Effect.provide(build(group([value, count]))))
|
||||
|
||||
expect(await Effect.runPromise(result)).toEqual({ text: "production", count: 3 })
|
||||
})
|
||||
|
||||
test("builds an empty group", async () => {
|
||||
expect(await Effect.runPromise(Effect.succeed("ok").pipe(Effect.provide(build(group([])))))).toBe("ok")
|
||||
})
|
||||
|
||||
test("builds replacements with their own dependencies", async () => {
|
||||
class ReplacementConfig extends Context.Service<ReplacementConfig, { readonly value: string }>()(
|
||||
"test/ReplacementConfig",
|
||||
) {}
|
||||
const replacementConfig = node(
|
||||
Layer.succeed(ReplacementConfig, ReplacementConfig.of({ value: "replacement" })),
|
||||
[],
|
||||
)
|
||||
const replacement = node(
|
||||
Layer.effect(
|
||||
Value,
|
||||
Effect.gen(function* () {
|
||||
return Value.of({ value: (yield* ReplacementConfig).value })
|
||||
}),
|
||||
),
|
||||
[replacementConfig],
|
||||
)
|
||||
const result = Effect.gen(function* () {
|
||||
return (yield* Greeting).text
|
||||
}).pipe(Effect.provide(build(greeting, { replacements: [replace(value, replacement)] })))
|
||||
|
||||
expect(await Effect.runPromise(result)).toBe("hello replacement")
|
||||
})
|
||||
|
||||
test("does not acquire unreachable replacements", async () => {
|
||||
let acquisitions = 0
|
||||
const unreachable = node(Layer.succeed(Value, Value.of({ value: "unreachable" })), [])
|
||||
const replacement = node(
|
||||
Layer.effect(
|
||||
Value,
|
||||
Effect.sync(() => {
|
||||
acquisitions++
|
||||
return Value.of({ value: "replacement" })
|
||||
}),
|
||||
),
|
||||
[],
|
||||
)
|
||||
|
||||
await Effect.runPromise(Effect.provide(Greeting, build(greeting, { replacements: [replace(unreachable, replacement)] })))
|
||||
|
||||
expect(acquisitions).toBe(0)
|
||||
})
|
||||
|
||||
test("rejects a direct cycle", () => {
|
||||
const cyclic = node(Layer.succeed(Value, Value.of({ value: "cyclic" })), [])
|
||||
;(cyclic.dependencies as LayerNode.Node<unknown, unknown>[]).push(cyclic)
|
||||
|
||||
expect(() => build(cyclic)).toThrow("Cycle detected in app graph: layer#1 -> layer#1")
|
||||
})
|
||||
|
||||
test("rejects an indirect cycle", () => {
|
||||
const first = node(Layer.succeed(Value, Value.of({ value: "first" })), [])
|
||||
const second = node(Layer.succeed(Value, Value.of({ value: "second" })), [first])
|
||||
const third = node(Layer.succeed(Value, Value.of({ value: "third" })), [second])
|
||||
;(first.dependencies as LayerNode.Node<unknown, unknown>[]).push(third)
|
||||
|
||||
expect(() => build(first)).toThrow("Cycle detected in app graph: layer#1 -> layer#2 -> layer#3 -> layer#1")
|
||||
})
|
||||
|
||||
test("rejects a cycle introduced by a replacement", () => {
|
||||
const replacement = node(Layer.succeed(Value, Value.of({ value: "replacement" })), [])
|
||||
const consumer = node(greetingImplementation, [value])
|
||||
;(replacement.dependencies as LayerNode.Node<unknown, unknown>[]).push(consumer)
|
||||
|
||||
expect(() => build(consumer, { replacements: [replace(value, replacement)] })).toThrow(
|
||||
"Cycle detected in app graph: layer#1 -> layer#2 -> layer#1",
|
||||
)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user