refactor(effect): yield services instead of promise facades (#19325)
parent
ef7d1f7efa
commit
9c6f1edfd7
|
|
@ -8,8 +8,8 @@ Use `InstanceState` (from `src/effect/instance-state.ts`) for services that need
|
||||||
|
|
||||||
Use `makeRuntime` (from `src/effect/run-service.ts`) to create a per-service `ManagedRuntime` that lazily initializes and shares layers via a global `memoMap`. Returns `{ runPromise, runFork, runCallback }`.
|
Use `makeRuntime` (from `src/effect/run-service.ts`) to create a per-service `ManagedRuntime` that lazily initializes and shares layers via a global `memoMap`. Returns `{ runPromise, runFork, runCallback }`.
|
||||||
|
|
||||||
- Global services (no per-directory state): Account, Auth, Installation, Truncate
|
- Global services (no per-directory state): Account, Auth, AppFileSystem, Installation, Truncate, Worktree
|
||||||
- Instance-scoped (per-directory state via InstanceState): File, FileTime, FileWatcher, Format, Permission, Question, Skill, Snapshot, Vcs, ProviderAuth
|
- Instance-scoped (per-directory state via InstanceState): Agent, Bus, Command, Config, File, FileTime, FileWatcher, Format, LSP, MCP, Permission, Plugin, ProviderAuth, Pty, Question, SessionStatus, Skill, Snapshot, ToolRegistry, Vcs
|
||||||
|
|
||||||
Rule of thumb: if two open directories should not share one copy of the service, it needs `InstanceState`.
|
Rule of thumb: if two open directories should not share one copy of the service, it needs `InstanceState`.
|
||||||
|
|
||||||
|
|
@ -181,36 +181,39 @@ That is fine for leaf files like `schema.ts`. Keep the service surface in the ow
|
||||||
Fully migrated (single namespace, InstanceState where needed, flattened facade):
|
Fully migrated (single namespace, InstanceState where needed, flattened facade):
|
||||||
|
|
||||||
- [x] `Account` — `account/index.ts`
|
- [x] `Account` — `account/index.ts`
|
||||||
|
- [x] `Agent` — `agent/agent.ts`
|
||||||
|
- [x] `AppFileSystem` — `filesystem/index.ts`
|
||||||
- [x] `Auth` — `auth/index.ts` (uses `zod()` helper for Schema→Zod interop)
|
- [x] `Auth` — `auth/index.ts` (uses `zod()` helper for Schema→Zod interop)
|
||||||
|
- [x] `Bus` — `bus/index.ts`
|
||||||
|
- [x] `Command` — `command/index.ts`
|
||||||
|
- [x] `Config` — `config/config.ts`
|
||||||
|
- [x] `Discovery` — `skill/discovery.ts` (dependency-only layer, no standalone runtime)
|
||||||
- [x] `File` — `file/index.ts`
|
- [x] `File` — `file/index.ts`
|
||||||
- [x] `FileTime` — `file/time.ts`
|
- [x] `FileTime` — `file/time.ts`
|
||||||
- [x] `FileWatcher` — `file/watcher.ts`
|
- [x] `FileWatcher` — `file/watcher.ts`
|
||||||
- [x] `Format` — `format/index.ts`
|
- [x] `Format` — `format/index.ts`
|
||||||
- [x] `Installation` — `installation/index.ts`
|
- [x] `Installation` — `installation/index.ts`
|
||||||
|
- [x] `LSP` — `lsp/index.ts`
|
||||||
|
- [x] `MCP` — `mcp/index.ts`
|
||||||
|
- [x] `McpAuth` — `mcp/auth.ts`
|
||||||
- [x] `Permission` — `permission/index.ts`
|
- [x] `Permission` — `permission/index.ts`
|
||||||
|
- [x] `Plugin` — `plugin/index.ts`
|
||||||
|
- [x] `Project` — `project/project.ts`
|
||||||
- [x] `ProviderAuth` — `provider/auth.ts`
|
- [x] `ProviderAuth` — `provider/auth.ts`
|
||||||
|
- [x] `Pty` — `pty/index.ts`
|
||||||
- [x] `Question` — `question/index.ts`
|
- [x] `Question` — `question/index.ts`
|
||||||
|
- [x] `SessionStatus` — `session/status.ts`
|
||||||
- [x] `Skill` — `skill/index.ts`
|
- [x] `Skill` — `skill/index.ts`
|
||||||
- [x] `Snapshot` — `snapshot/index.ts`
|
- [x] `Snapshot` — `snapshot/index.ts`
|
||||||
|
- [x] `ToolRegistry` — `tool/registry.ts`
|
||||||
- [x] `Truncate` — `tool/truncate.ts`
|
- [x] `Truncate` — `tool/truncate.ts`
|
||||||
- [x] `Vcs` — `project/vcs.ts`
|
- [x] `Vcs` — `project/vcs.ts`
|
||||||
- [x] `Discovery` — `skill/discovery.ts`
|
- [x] `Worktree` — `worktree/index.ts`
|
||||||
- [x] `SessionStatus`
|
|
||||||
|
|
||||||
Still open and likely worth migrating:
|
Still open and likely worth migrating:
|
||||||
|
|
||||||
- [x] `Plugin`
|
|
||||||
- [x] `ToolRegistry`
|
|
||||||
- [ ] `Pty`
|
|
||||||
- [x] `Worktree`
|
|
||||||
- [x] `Bus`
|
|
||||||
- [x] `Command`
|
|
||||||
- [x] `Config`
|
|
||||||
- [ ] `Session`
|
- [ ] `Session`
|
||||||
- [ ] `SessionProcessor`
|
- [ ] `SessionProcessor`
|
||||||
- [ ] `SessionPrompt`
|
- [ ] `SessionPrompt`
|
||||||
- [ ] `SessionCompaction`
|
- [ ] `SessionCompaction`
|
||||||
- [ ] `Provider`
|
- [ ] `Provider`
|
||||||
- [x] `Project`
|
|
||||||
- [x] `LSP`
|
|
||||||
- [x] `MCP`
|
|
||||||
|
|
|
||||||
|
|
@ -72,13 +72,14 @@ export namespace Agent {
|
||||||
export const layer = Layer.effect(
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const config = () => Effect.promise(() => Config.get())
|
const config = yield* Config.Service
|
||||||
const auth = yield* Auth.Service
|
const auth = yield* Auth.Service
|
||||||
|
const skill = yield* Skill.Service
|
||||||
|
|
||||||
const state = yield* InstanceState.make<State>(
|
const state = yield* InstanceState.make<State>(
|
||||||
Effect.fn("Agent.state")(function* (ctx) {
|
Effect.fn("Agent.state")(function* (ctx) {
|
||||||
const cfg = yield* config()
|
const cfg = yield* config.get()
|
||||||
const skillDirs = yield* Effect.promise(() => Skill.dirs())
|
const skillDirs = yield* skill.dirs()
|
||||||
const whitelistedDirs = [Truncate.GLOB, ...skillDirs.map((dir) => path.join(dir, "*"))]
|
const whitelistedDirs = [Truncate.GLOB, ...skillDirs.map((dir) => path.join(dir, "*"))]
|
||||||
|
|
||||||
const defaults = Permission.fromConfig({
|
const defaults = Permission.fromConfig({
|
||||||
|
|
@ -281,7 +282,7 @@ export namespace Agent {
|
||||||
})
|
})
|
||||||
|
|
||||||
const list = Effect.fnUntraced(function* () {
|
const list = Effect.fnUntraced(function* () {
|
||||||
const cfg = yield* config()
|
const cfg = yield* config.get()
|
||||||
return pipe(
|
return pipe(
|
||||||
agents,
|
agents,
|
||||||
values(),
|
values(),
|
||||||
|
|
@ -293,7 +294,7 @@ export namespace Agent {
|
||||||
})
|
})
|
||||||
|
|
||||||
const defaultAgent = Effect.fnUntraced(function* () {
|
const defaultAgent = Effect.fnUntraced(function* () {
|
||||||
const c = yield* config()
|
const c = yield* config.get()
|
||||||
if (c.default_agent) {
|
if (c.default_agent) {
|
||||||
const agent = agents[c.default_agent]
|
const agent = agents[c.default_agent]
|
||||||
if (!agent) throw new Error(`default agent "${c.default_agent}" not found`)
|
if (!agent) throw new Error(`default agent "${c.default_agent}" not found`)
|
||||||
|
|
@ -328,7 +329,7 @@ export namespace Agent {
|
||||||
description: string
|
description: string
|
||||||
model?: { providerID: ProviderID; modelID: ModelID }
|
model?: { providerID: ProviderID; modelID: ModelID }
|
||||||
}) {
|
}) {
|
||||||
const cfg = yield* config()
|
const cfg = yield* config.get()
|
||||||
const model = input.model ?? (yield* Effect.promise(() => Provider.defaultModel()))
|
const model = input.model ?? (yield* Effect.promise(() => Provider.defaultModel()))
|
||||||
const resolved = yield* Effect.promise(() => Provider.getModel(model.providerID, model.modelID))
|
const resolved = yield* Effect.promise(() => Provider.getModel(model.providerID, model.modelID))
|
||||||
const language = yield* Effect.promise(() => Provider.getLanguage(resolved))
|
const language = yield* Effect.promise(() => Provider.getLanguage(resolved))
|
||||||
|
|
@ -391,7 +392,11 @@ export namespace Agent {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(Auth.layer))
|
export const defaultLayer = layer.pipe(
|
||||||
|
Layer.provide(Auth.layer),
|
||||||
|
Layer.provide(Config.defaultLayer),
|
||||||
|
Layer.provide(Skill.defaultLayer),
|
||||||
|
)
|
||||||
|
|
||||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,8 +75,12 @@ export namespace Command {
|
||||||
export const layer = Layer.effect(
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
const config = yield* Config.Service
|
||||||
|
const mcp = yield* MCP.Service
|
||||||
|
const skill = yield* Skill.Service
|
||||||
|
|
||||||
const init = Effect.fn("Command.state")(function* (ctx) {
|
const init = Effect.fn("Command.state")(function* (ctx) {
|
||||||
const cfg = yield* Effect.promise(() => Config.get())
|
const cfg = yield* config.get()
|
||||||
const commands: Record<string, Info> = {}
|
const commands: Record<string, Info> = {}
|
||||||
|
|
||||||
commands[Default.INIT] = {
|
commands[Default.INIT] = {
|
||||||
|
|
@ -114,7 +118,7 @@ export namespace Command {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [name, prompt] of Object.entries(yield* Effect.promise(() => MCP.prompts()))) {
|
for (const [name, prompt] of Object.entries(yield* mcp.prompts())) {
|
||||||
commands[name] = {
|
commands[name] = {
|
||||||
name,
|
name,
|
||||||
source: "mcp",
|
source: "mcp",
|
||||||
|
|
@ -139,14 +143,14 @@ export namespace Command {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const skill of yield* Effect.promise(() => Skill.all())) {
|
for (const item of yield* skill.all()) {
|
||||||
if (commands[skill.name]) continue
|
if (commands[item.name]) continue
|
||||||
commands[skill.name] = {
|
commands[item.name] = {
|
||||||
name: skill.name,
|
name: item.name,
|
||||||
description: skill.description,
|
description: item.description,
|
||||||
source: "skill",
|
source: "skill",
|
||||||
get template() {
|
get template() {
|
||||||
return skill.content
|
return item.content
|
||||||
},
|
},
|
||||||
hints: [],
|
hints: [],
|
||||||
}
|
}
|
||||||
|
|
@ -173,7 +177,13 @@ export namespace Command {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const { runPromise } = makeRuntime(Service, layer)
|
export const defaultLayer = layer.pipe(
|
||||||
|
Layer.provide(Config.defaultLayer),
|
||||||
|
Layer.provide(MCP.defaultLayer),
|
||||||
|
Layer.provide(Skill.defaultLayer),
|
||||||
|
)
|
||||||
|
|
||||||
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
export async function get(name: string) {
|
export async function get(name: string) {
|
||||||
return runPromise((svc) => svc.get(name))
|
return runPromise((svc) => svc.get(name))
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ import { Lock } from "@/util/lock"
|
||||||
import { AppFileSystem } from "@/filesystem"
|
import { AppFileSystem } from "@/filesystem"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
import { makeRuntime } from "@/effect/run-service"
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
import { Duration, Effect, Layer, ServiceMap } from "effect"
|
import { Duration, Effect, Layer, Option, ServiceMap } from "effect"
|
||||||
|
|
||||||
export namespace Config {
|
export namespace Config {
|
||||||
const ModelId = z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" })
|
const ModelId = z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" })
|
||||||
|
|
@ -1136,10 +1136,12 @@ export namespace Config {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const layer: Layer.Layer<Service, never, AppFileSystem.Service> = Layer.effect(
|
export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Auth.Service | Account.Service> = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const fs = yield* AppFileSystem.Service
|
const fs = yield* AppFileSystem.Service
|
||||||
|
const authSvc = yield* Auth.Service
|
||||||
|
const accountSvc = yield* Account.Service
|
||||||
|
|
||||||
const readConfigFile = Effect.fnUntraced(function* (filepath: string) {
|
const readConfigFile = Effect.fnUntraced(function* (filepath: string) {
|
||||||
return yield* fs.readFileString(filepath).pipe(
|
return yield* fs.readFileString(filepath).pipe(
|
||||||
|
|
@ -1256,7 +1258,7 @@ export namespace Config {
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadInstanceState = Effect.fnUntraced(function* (ctx: InstanceContext) {
|
const loadInstanceState = Effect.fnUntraced(function* (ctx: InstanceContext) {
|
||||||
const auth = yield* Effect.promise(() => Auth.all())
|
const auth = yield* authSvc.all().pipe(Effect.orDie)
|
||||||
|
|
||||||
let result: Info = {}
|
let result: Info = {}
|
||||||
for (const [key, value] of Object.entries(auth)) {
|
for (const [key, value] of Object.entries(auth)) {
|
||||||
|
|
@ -1344,17 +1346,20 @@ export namespace Config {
|
||||||
log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
|
log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
|
||||||
}
|
}
|
||||||
|
|
||||||
const active = yield* Effect.promise(() => Account.active())
|
const active = Option.getOrUndefined(yield* accountSvc.active().pipe(Effect.orDie))
|
||||||
if (active?.active_org_id) {
|
if (active?.active_org_id) {
|
||||||
yield* Effect.gen(function* () {
|
yield* Effect.gen(function* () {
|
||||||
const [config, token] = yield* Effect.promise(() =>
|
const [configOpt, tokenOpt] = yield* Effect.all(
|
||||||
Promise.all([Account.config(active.id, active.active_org_id!), Account.token(active.id)]),
|
[accountSvc.config(active.id, active.active_org_id!), accountSvc.token(active.id)],
|
||||||
|
{ concurrency: 2 },
|
||||||
)
|
)
|
||||||
|
const token = Option.getOrUndefined(tokenOpt)
|
||||||
if (token) {
|
if (token) {
|
||||||
process.env["OPENCODE_CONSOLE_TOKEN"] = token
|
process.env["OPENCODE_CONSOLE_TOKEN"] = token
|
||||||
Env.set("OPENCODE_CONSOLE_TOKEN", token)
|
Env.set("OPENCODE_CONSOLE_TOKEN", token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const config = Option.getOrUndefined(configOpt)
|
||||||
if (config) {
|
if (config) {
|
||||||
result = mergeConfigConcatArrays(
|
result = mergeConfigConcatArrays(
|
||||||
result,
|
result,
|
||||||
|
|
@ -1365,7 +1370,7 @@ export namespace Config {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.catchDefect((err) => {
|
Effect.catch((err) => {
|
||||||
log.debug("failed to fetch remote account config", {
|
log.debug("failed to fetch remote account config", {
|
||||||
error: err instanceof Error ? err.message : String(err),
|
error: err instanceof Error ? err.message : String(err),
|
||||||
})
|
})
|
||||||
|
|
@ -1502,7 +1507,11 @@ export namespace Config {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
|
export const defaultLayer = layer.pipe(
|
||||||
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
|
Layer.provide(Auth.layer),
|
||||||
|
Layer.provide(Account.defaultLayer),
|
||||||
|
)
|
||||||
|
|
||||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type * as Arr from "effect/Array"
|
import type * as Arr from "effect/Array"
|
||||||
import { NodeSink, NodeStream } from "@effect/platform-node"
|
import { NodeFileSystem, NodeSink, NodeStream } from "@effect/platform-node"
|
||||||
|
import * as NodePath from "@effect/platform-node/NodePath"
|
||||||
import * as Deferred from "effect/Deferred"
|
import * as Deferred from "effect/Deferred"
|
||||||
import * as Effect from "effect/Effect"
|
import * as Effect from "effect/Effect"
|
||||||
import * as Exit from "effect/Exit"
|
import * as Exit from "effect/Exit"
|
||||||
|
|
@ -474,3 +475,5 @@ export const layer: Layer.Layer<ChildProcessSpawner, never, FileSystem.FileSyste
|
||||||
ChildProcessSpawner,
|
ChildProcessSpawner,
|
||||||
make,
|
make,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const defaultLayer = layer.pipe(Layer.provide(NodeFileSystem.layer), Layer.provide(NodePath.layer))
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,8 @@ export namespace FileWatcher {
|
||||||
export const layer = Layer.effect(
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
const config = yield* Config.Service
|
||||||
|
|
||||||
const state = yield* InstanceState.make(
|
const state = yield* InstanceState.make(
|
||||||
Effect.fn("FileWatcher.state")(
|
Effect.fn("FileWatcher.state")(
|
||||||
function* () {
|
function* () {
|
||||||
|
|
@ -117,7 +119,7 @@ export namespace FileWatcher {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const cfg = yield* Effect.promise(() => Config.get())
|
const cfg = yield* config.get()
|
||||||
const cfgIgnores = cfg.watcher?.ignore ?? []
|
const cfgIgnores = cfg.watcher?.ignore ?? []
|
||||||
|
|
||||||
if (yield* Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER) {
|
if (yield* Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER) {
|
||||||
|
|
@ -159,7 +161,9 @@ export namespace FileWatcher {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const { runPromise } = makeRuntime(Service, layer)
|
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer))
|
||||||
|
|
||||||
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
export function init() {
|
export function init() {
|
||||||
return runPromise((svc) => svc.init())
|
return runPromise((svc) => svc.init())
|
||||||
|
|
|
||||||
|
|
@ -35,12 +35,14 @@ export namespace Format {
|
||||||
export const layer = Layer.effect(
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
const config = yield* Config.Service
|
||||||
|
|
||||||
const state = yield* InstanceState.make(
|
const state = yield* InstanceState.make(
|
||||||
Effect.fn("Format.state")(function* (_ctx) {
|
Effect.fn("Format.state")(function* (_ctx) {
|
||||||
const enabled: Record<string, boolean> = {}
|
const enabled: Record<string, boolean> = {}
|
||||||
const formatters: Record<string, Formatter.Info> = {}
|
const formatters: Record<string, Formatter.Info> = {}
|
||||||
|
|
||||||
const cfg = yield* Effect.promise(() => Config.get())
|
const cfg = yield* config.get()
|
||||||
|
|
||||||
if (cfg.formatter !== false) {
|
if (cfg.formatter !== false) {
|
||||||
for (const item of Object.values(Formatter)) {
|
for (const item of Object.values(Formatter)) {
|
||||||
|
|
@ -167,7 +169,9 @@ export namespace Format {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const { runPromise } = makeRuntime(Service, layer)
|
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer))
|
||||||
|
|
||||||
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
export async function init() {
|
export async function init() {
|
||||||
return runPromise((s) => s.init())
|
return runPromise((s) => s.init())
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { NodeFileSystem, NodePath } from "@effect/platform-node"
|
|
||||||
import { Effect, Layer, Schema, ServiceMap, Stream } from "effect"
|
import { Effect, Layer, Schema, ServiceMap, Stream } from "effect"
|
||||||
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||||
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
|
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
|
||||||
|
|
@ -341,9 +340,7 @@ export namespace Installation {
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(
|
export const defaultLayer = layer.pipe(
|
||||||
Layer.provide(FetchHttpClient.layer),
|
Layer.provide(FetchHttpClient.layer),
|
||||||
Layer.provide(CrossSpawnSpawner.layer),
|
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
||||||
Layer.provide(NodeFileSystem.layer),
|
|
||||||
Layer.provide(NodePath.layer),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
|
||||||
|
|
@ -161,9 +161,11 @@ export namespace LSP {
|
||||||
export const layer = Layer.effect(
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
const config = yield* Config.Service
|
||||||
|
|
||||||
const state = yield* InstanceState.make<State>(
|
const state = yield* InstanceState.make<State>(
|
||||||
Effect.fn("LSP.state")(function* () {
|
Effect.fn("LSP.state")(function* () {
|
||||||
const cfg = yield* Effect.promise(() => Config.get())
|
const cfg = yield* config.get()
|
||||||
|
|
||||||
const servers: Record<string, LSPServer.Info> = {}
|
const servers: Record<string, LSPServer.Info> = {}
|
||||||
|
|
||||||
|
|
@ -504,7 +506,9 @@ export namespace LSP {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const { runPromise } = makeRuntime(Service, layer)
|
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer))
|
||||||
|
|
||||||
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
export const init = async () => runPromise((svc) => svc.init())
|
export const init = async () => runPromise((svc) => svc.init())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,6 @@ import { InstanceState } from "@/effect/instance-state"
|
||||||
import { makeRuntime } from "@/effect/run-service"
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||||
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
|
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
|
||||||
import { NodeFileSystem } from "@effect/platform-node"
|
|
||||||
import * as NodePath from "@effect/platform-node/NodePath"
|
|
||||||
|
|
||||||
export namespace MCP {
|
export namespace MCP {
|
||||||
const log = Log.create({ service: "mcp" })
|
const log = Log.create({ service: "mcp" })
|
||||||
|
|
@ -437,6 +435,7 @@ export namespace MCP {
|
||||||
log.info("create() successfully created client", { key, toolCount: listed.length })
|
log.info("create() successfully created client", { key, toolCount: listed.length })
|
||||||
return { mcpClient, status, defs: listed } satisfies CreateResult
|
return { mcpClient, status, defs: listed } satisfies CreateResult
|
||||||
})
|
})
|
||||||
|
const cfgSvc = yield* Config.Service
|
||||||
|
|
||||||
const descendants = Effect.fnUntraced(
|
const descendants = Effect.fnUntraced(
|
||||||
function* (pid: number) {
|
function* (pid: number) {
|
||||||
|
|
@ -478,11 +477,11 @@ export namespace MCP {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getConfig = () => Effect.promise(() => Config.get())
|
|
||||||
|
|
||||||
const cache = yield* InstanceState.make<State>(
|
const cache = yield* InstanceState.make<State>(
|
||||||
Effect.fn("MCP.state")(function* () {
|
Effect.fn("MCP.state")(function* () {
|
||||||
const cfg = yield* getConfig()
|
|
||||||
|
const cfg = yield* cfgSvc.get()
|
||||||
const config = cfg.mcp ?? {}
|
const config = cfg.mcp ?? {}
|
||||||
const s: State = {
|
const s: State = {
|
||||||
status: {},
|
status: {},
|
||||||
|
|
@ -553,7 +552,8 @@ export namespace MCP {
|
||||||
|
|
||||||
const status = Effect.fn("MCP.status")(function* () {
|
const status = Effect.fn("MCP.status")(function* () {
|
||||||
const s = yield* InstanceState.get(cache)
|
const s = yield* InstanceState.get(cache)
|
||||||
const cfg = yield* getConfig()
|
|
||||||
|
const cfg = yield* cfgSvc.get()
|
||||||
const config = cfg.mcp ?? {}
|
const config = cfg.mcp ?? {}
|
||||||
const result: Record<string, Status> = {}
|
const result: Record<string, Status> = {}
|
||||||
|
|
||||||
|
|
@ -613,7 +613,8 @@ export namespace MCP {
|
||||||
const tools = Effect.fn("MCP.tools")(function* () {
|
const tools = Effect.fn("MCP.tools")(function* () {
|
||||||
const result: Record<string, Tool> = {}
|
const result: Record<string, Tool> = {}
|
||||||
const s = yield* InstanceState.get(cache)
|
const s = yield* InstanceState.get(cache)
|
||||||
const cfg = yield* getConfig()
|
|
||||||
|
const cfg = yield* cfgSvc.get()
|
||||||
const config = cfg.mcp ?? {}
|
const config = cfg.mcp ?? {}
|
||||||
const defaultTimeout = cfg.experimental?.mcp_timeout
|
const defaultTimeout = cfg.experimental?.mcp_timeout
|
||||||
|
|
||||||
|
|
@ -705,7 +706,8 @@ export namespace MCP {
|
||||||
})
|
})
|
||||||
|
|
||||||
const getMcpConfig = Effect.fnUntraced(function* (mcpName: string) {
|
const getMcpConfig = Effect.fnUntraced(function* (mcpName: string) {
|
||||||
const cfg = yield* getConfig()
|
|
||||||
|
const cfg = yield* cfgSvc.get()
|
||||||
const mcpConfig = cfg.mcp?.[mcpName]
|
const mcpConfig = cfg.mcp?.[mcpName]
|
||||||
if (!mcpConfig || !isMcpConfigured(mcpConfig)) return undefined
|
if (!mcpConfig || !isMcpConfigured(mcpConfig)) return undefined
|
||||||
return mcpConfig
|
return mcpConfig
|
||||||
|
|
@ -876,13 +878,12 @@ export namespace MCP {
|
||||||
|
|
||||||
// --- Per-service runtime ---
|
// --- Per-service runtime ---
|
||||||
|
|
||||||
const defaultLayer = layer.pipe(
|
export const defaultLayer = layer.pipe(
|
||||||
Layer.provide(McpAuth.layer),
|
Layer.provide(McpAuth.layer),
|
||||||
Layer.provide(Bus.layer),
|
Layer.provide(Bus.layer),
|
||||||
Layer.provide(CrossSpawnSpawner.layer),
|
Layer.provide(Config.defaultLayer),
|
||||||
|
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
||||||
Layer.provide(AppFileSystem.defaultLayer),
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
Layer.provide(NodeFileSystem.layer),
|
|
||||||
Layer.provide(NodePath.layer),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ export namespace Project {
|
||||||
> = Layer.effect(
|
> = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const fsys = yield* AppFileSystem.Service
|
const fs = yield* AppFileSystem.Service
|
||||||
const pathSvc = yield* Path.Path
|
const pathSvc = yield* Path.Path
|
||||||
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
||||||
|
|
||||||
|
|
@ -155,7 +155,7 @@ export namespace Project {
|
||||||
const scope = yield* Scope.Scope
|
const scope = yield* Scope.Scope
|
||||||
|
|
||||||
const readCachedProjectId = Effect.fnUntraced(function* (dir: string) {
|
const readCachedProjectId = Effect.fnUntraced(function* (dir: string) {
|
||||||
return yield* fsys.readFileString(pathSvc.join(dir, "opencode")).pipe(
|
return yield* fs.readFileString(pathSvc.join(dir, "opencode")).pipe(
|
||||||
Effect.map((x) => x.trim()),
|
Effect.map((x) => x.trim()),
|
||||||
Effect.map(ProjectID.make),
|
Effect.map(ProjectID.make),
|
||||||
Effect.catch(() => Effect.succeed(undefined)),
|
Effect.catch(() => Effect.succeed(undefined)),
|
||||||
|
|
@ -169,7 +169,7 @@ export namespace Project {
|
||||||
type DiscoveryResult = { id: ProjectID; worktree: string; sandbox: string; vcs: Info["vcs"] }
|
type DiscoveryResult = { id: ProjectID; worktree: string; sandbox: string; vcs: Info["vcs"] }
|
||||||
|
|
||||||
const data: DiscoveryResult = yield* Effect.gen(function* () {
|
const data: DiscoveryResult = yield* Effect.gen(function* () {
|
||||||
const dotgitMatches = yield* fsys.up({ targets: [".git"], start: directory }).pipe(Effect.orDie)
|
const dotgitMatches = yield* fs.up({ targets: [".git"], start: directory }).pipe(Effect.orDie)
|
||||||
const dotgit = dotgitMatches[0]
|
const dotgit = dotgitMatches[0]
|
||||||
|
|
||||||
if (!dotgit) {
|
if (!dotgit) {
|
||||||
|
|
@ -222,7 +222,7 @@ export namespace Project {
|
||||||
|
|
||||||
id = roots[0] ? ProjectID.make(roots[0]) : undefined
|
id = roots[0] ? ProjectID.make(roots[0]) : undefined
|
||||||
if (id) {
|
if (id) {
|
||||||
yield* fsys.writeFileString(pathSvc.join(worktree, ".git", "opencode"), id).pipe(Effect.ignore)
|
yield* fs.writeFileString(pathSvc.join(worktree, ".git", "opencode"), id).pipe(Effect.ignore)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -270,7 +270,7 @@ export namespace Project {
|
||||||
result.sandboxes = yield* Effect.forEach(
|
result.sandboxes = yield* Effect.forEach(
|
||||||
result.sandboxes,
|
result.sandboxes,
|
||||||
(s) =>
|
(s) =>
|
||||||
fsys.exists(s).pipe(
|
fs.exists(s).pipe(
|
||||||
Effect.orDie,
|
Effect.orDie,
|
||||||
Effect.map((exists) => (exists ? s : undefined)),
|
Effect.map((exists) => (exists ? s : undefined)),
|
||||||
),
|
),
|
||||||
|
|
@ -329,7 +329,7 @@ export namespace Project {
|
||||||
if (input.icon?.override) return
|
if (input.icon?.override) return
|
||||||
if (input.icon?.url) return
|
if (input.icon?.url) return
|
||||||
|
|
||||||
const matches = yield* fsys
|
const matches = yield* fs
|
||||||
.glob("**/favicon.{ico,png,svg,jpg,jpeg,webp}", {
|
.glob("**/favicon.{ico,png,svg,jpg,jpeg,webp}", {
|
||||||
cwd: input.worktree,
|
cwd: input.worktree,
|
||||||
absolute: true,
|
absolute: true,
|
||||||
|
|
@ -339,7 +339,7 @@ export namespace Project {
|
||||||
const shortest = matches.sort((a, b) => a.length - b.length)[0]
|
const shortest = matches.sort((a, b) => a.length - b.length)[0]
|
||||||
if (!shortest) return
|
if (!shortest) return
|
||||||
|
|
||||||
const buffer = yield* fsys.readFile(shortest).pipe(Effect.orDie)
|
const buffer = yield* fs.readFile(shortest).pipe(Effect.orDie)
|
||||||
const base64 = Buffer.from(buffer).toString("base64")
|
const base64 = Buffer.from(buffer).toString("base64")
|
||||||
const mime = AppFileSystem.mimeType(shortest)
|
const mime = AppFileSystem.mimeType(shortest)
|
||||||
const url = `data:${mime};base64,${base64}`
|
const url = `data:${mime};base64,${base64}`
|
||||||
|
|
@ -400,7 +400,7 @@ export namespace Project {
|
||||||
return yield* Effect.forEach(
|
return yield* Effect.forEach(
|
||||||
data.sandboxes,
|
data.sandboxes,
|
||||||
(dir) =>
|
(dir) =>
|
||||||
fsys.isDir(dir).pipe(
|
fs.isDir(dir).pipe(
|
||||||
Effect.orDie,
|
Effect.orDie,
|
||||||
Effect.map((ok) => (ok ? dir : undefined)),
|
Effect.map((ok) => (ok ? dir : undefined)),
|
||||||
),
|
),
|
||||||
|
|
@ -457,9 +457,8 @@ export namespace Project {
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(
|
export const defaultLayer = layer.pipe(
|
||||||
Layer.provide(CrossSpawnSpawner.layer),
|
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
||||||
Layer.provide(AppFileSystem.defaultLayer),
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
Layer.provide(NodeFileSystem.layer),
|
|
||||||
Layer.provide(NodePath.layer),
|
Layer.provide(NodePath.layer),
|
||||||
)
|
)
|
||||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
|
||||||
|
|
@ -273,7 +273,7 @@ export namespace Pty {
|
||||||
if (input.size) {
|
if (input.size) {
|
||||||
session.process.resize(input.size.cols, input.size.rows)
|
session.process.resize(input.size.cols, input.size.rows)
|
||||||
}
|
}
|
||||||
yield* Effect.promise(() => Bus.publish(Event.Updated, { info: session.info }))
|
void Bus.publish(Event.Updated, { info: session.info })
|
||||||
return session.info
|
return session.info
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,24 +60,28 @@ export namespace Snapshot {
|
||||||
|
|
||||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Snapshot") {}
|
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Snapshot") {}
|
||||||
|
|
||||||
export const layer: Layer.Layer<Service, never, AppFileSystem.Service | ChildProcessSpawner.ChildProcessSpawner> =
|
export const layer: Layer.Layer<
|
||||||
Layer.effect(
|
Service,
|
||||||
Service,
|
never,
|
||||||
Effect.gen(function* () {
|
AppFileSystem.Service | ChildProcessSpawner.ChildProcessSpawner | Config.Service
|
||||||
const fs = yield* AppFileSystem.Service
|
> = Layer.effect(
|
||||||
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
Service,
|
||||||
const locks = new Map<string, Semaphore.Semaphore>()
|
Effect.gen(function* () {
|
||||||
|
const fs = yield* AppFileSystem.Service
|
||||||
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
||||||
|
const config = yield* Config.Service
|
||||||
|
const locks = new Map<string, Semaphore.Semaphore>()
|
||||||
|
|
||||||
const lock = (key: string) => {
|
const lock = (key: string) => {
|
||||||
const hit = locks.get(key)
|
const hit = locks.get(key)
|
||||||
if (hit) return hit
|
if (hit) return hit
|
||||||
|
|
||||||
const next = Semaphore.makeUnsafe(1)
|
const next = Semaphore.makeUnsafe(1)
|
||||||
locks.set(key, next)
|
locks.set(key, next)
|
||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = yield* InstanceState.make<State>(
|
const state = yield* InstanceState.make<State>(
|
||||||
Effect.fn("Snapshot.state")(function* (ctx) {
|
Effect.fn("Snapshot.state")(function* (ctx) {
|
||||||
const state = {
|
const state = {
|
||||||
directory: ctx.directory,
|
directory: ctx.directory,
|
||||||
|
|
@ -123,7 +127,7 @@ export namespace Snapshot {
|
||||||
|
|
||||||
const enabled = Effect.fnUntraced(function* () {
|
const enabled = Effect.fnUntraced(function* () {
|
||||||
if (state.vcs !== "git") return false
|
if (state.vcs !== "git") return false
|
||||||
return (yield* Effect.promise(() => Config.get())).snapshot !== false
|
return (yield* config.get()).snapshot !== false
|
||||||
})
|
})
|
||||||
|
|
||||||
const excludes = Effect.fnUntraced(function* () {
|
const excludes = Effect.fnUntraced(function* () {
|
||||||
|
|
@ -423,40 +427,39 @@ export namespace Snapshot {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
init: Effect.fn("Snapshot.init")(function* () {
|
init: Effect.fn("Snapshot.init")(function* () {
|
||||||
yield* InstanceState.get(state)
|
yield* InstanceState.get(state)
|
||||||
}),
|
}),
|
||||||
cleanup: Effect.fn("Snapshot.cleanup")(function* () {
|
cleanup: Effect.fn("Snapshot.cleanup")(function* () {
|
||||||
return yield* InstanceState.useEffect(state, (s) => s.cleanup())
|
return yield* InstanceState.useEffect(state, (s) => s.cleanup())
|
||||||
}),
|
}),
|
||||||
track: Effect.fn("Snapshot.track")(function* () {
|
track: Effect.fn("Snapshot.track")(function* () {
|
||||||
return yield* InstanceState.useEffect(state, (s) => s.track())
|
return yield* InstanceState.useEffect(state, (s) => s.track())
|
||||||
}),
|
}),
|
||||||
patch: Effect.fn("Snapshot.patch")(function* (hash: string) {
|
patch: Effect.fn("Snapshot.patch")(function* (hash: string) {
|
||||||
return yield* InstanceState.useEffect(state, (s) => s.patch(hash))
|
return yield* InstanceState.useEffect(state, (s) => s.patch(hash))
|
||||||
}),
|
}),
|
||||||
restore: Effect.fn("Snapshot.restore")(function* (snapshot: string) {
|
restore: Effect.fn("Snapshot.restore")(function* (snapshot: string) {
|
||||||
return yield* InstanceState.useEffect(state, (s) => s.restore(snapshot))
|
return yield* InstanceState.useEffect(state, (s) => s.restore(snapshot))
|
||||||
}),
|
}),
|
||||||
revert: Effect.fn("Snapshot.revert")(function* (patches: Snapshot.Patch[]) {
|
revert: Effect.fn("Snapshot.revert")(function* (patches: Snapshot.Patch[]) {
|
||||||
return yield* InstanceState.useEffect(state, (s) => s.revert(patches))
|
return yield* InstanceState.useEffect(state, (s) => s.revert(patches))
|
||||||
}),
|
}),
|
||||||
diff: Effect.fn("Snapshot.diff")(function* (hash: string) {
|
diff: Effect.fn("Snapshot.diff")(function* (hash: string) {
|
||||||
return yield* InstanceState.useEffect(state, (s) => s.diff(hash))
|
return yield* InstanceState.useEffect(state, (s) => s.diff(hash))
|
||||||
}),
|
}),
|
||||||
diffFull: Effect.fn("Snapshot.diffFull")(function* (from: string, to: string) {
|
diffFull: Effect.fn("Snapshot.diffFull")(function* (from: string, to: string) {
|
||||||
return yield* InstanceState.useEffect(state, (s) => s.diffFull(from, to))
|
return yield* InstanceState.useEffect(state, (s) => s.diffFull(from, to))
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(
|
export const defaultLayer = layer.pipe(
|
||||||
Layer.provide(CrossSpawnSpawner.layer),
|
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
||||||
Layer.provide(AppFileSystem.defaultLayer),
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
Layer.provide(NodeFileSystem.layer), // needed by CrossSpawnSpawner
|
Layer.provide(Config.defaultLayer),
|
||||||
Layer.provide(NodePath.layer),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,10 @@ import { Log } from "../util/log"
|
||||||
import { Slug } from "@opencode-ai/util/slug"
|
import { Slug } from "@opencode-ai/util/slug"
|
||||||
import { BusEvent } from "@/bus/bus-event"
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
import { GlobalBus } from "@/bus/global"
|
import { GlobalBus } from "@/bus/global"
|
||||||
import { Effect, FileSystem, Layer, Path, Scope, ServiceMap, Stream } from "effect"
|
import { Effect, Layer, Path, Scope, ServiceMap, Stream } from "effect"
|
||||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||||
import { NodeFileSystem, NodePath } from "@effect/platform-node"
|
import { NodePath } from "@effect/platform-node"
|
||||||
|
import { AppFileSystem } from "@/filesystem"
|
||||||
import { makeRuntime } from "@/effect/run-service"
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
|
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
|
||||||
|
|
||||||
|
|
@ -167,14 +168,15 @@ export namespace Worktree {
|
||||||
export const layer: Layer.Layer<
|
export const layer: Layer.Layer<
|
||||||
Service,
|
Service,
|
||||||
never,
|
never,
|
||||||
FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner
|
AppFileSystem.Service | Path.Path | ChildProcessSpawner.ChildProcessSpawner | Project.Service
|
||||||
> = Layer.effect(
|
> = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const scope = yield* Scope.Scope
|
const scope = yield* Scope.Scope
|
||||||
const fsys = yield* FileSystem.FileSystem
|
const fs = yield* AppFileSystem.Service
|
||||||
const pathSvc = yield* Path.Path
|
const pathSvc = yield* Path.Path
|
||||||
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
||||||
|
const project = yield* Project.Service
|
||||||
|
|
||||||
const git = Effect.fnUntraced(
|
const git = Effect.fnUntraced(
|
||||||
function* (args: string[], opts?: { cwd?: string }) {
|
function* (args: string[], opts?: { cwd?: string }) {
|
||||||
|
|
@ -201,7 +203,7 @@ export namespace Worktree {
|
||||||
const branch = `opencode/${name}`
|
const branch = `opencode/${name}`
|
||||||
const directory = pathSvc.join(root, name)
|
const directory = pathSvc.join(root, name)
|
||||||
|
|
||||||
if (yield* fsys.exists(directory).pipe(Effect.orDie)) continue
|
if (yield* fs.exists(directory).pipe(Effect.orDie)) continue
|
||||||
|
|
||||||
const ref = `refs/heads/${branch}`
|
const ref = `refs/heads/${branch}`
|
||||||
const branchCheck = yield* git(["show-ref", "--verify", "--quiet", ref], { cwd: Instance.worktree })
|
const branchCheck = yield* git(["show-ref", "--verify", "--quiet", ref], { cwd: Instance.worktree })
|
||||||
|
|
@ -218,7 +220,7 @@ export namespace Worktree {
|
||||||
}
|
}
|
||||||
|
|
||||||
const root = pathSvc.join(Global.Path.data, "worktree", Instance.project.id)
|
const root = pathSvc.join(Global.Path.data, "worktree", Instance.project.id)
|
||||||
yield* fsys.makeDirectory(root, { recursive: true }).pipe(Effect.orDie)
|
yield* fs.makeDirectory(root, { recursive: true }).pipe(Effect.orDie)
|
||||||
|
|
||||||
const base = name ? slugify(name) : ""
|
const base = name ? slugify(name) : ""
|
||||||
return yield* candidate(root, base || undefined)
|
return yield* candidate(root, base || undefined)
|
||||||
|
|
@ -232,7 +234,7 @@ export namespace Worktree {
|
||||||
throw new CreateFailedError({ message: created.stderr || created.text || "Failed to create git worktree" })
|
throw new CreateFailedError({ message: created.stderr || created.text || "Failed to create git worktree" })
|
||||||
}
|
}
|
||||||
|
|
||||||
yield* Effect.promise(() => Project.addSandbox(Instance.project.id, info.directory).catch(() => undefined))
|
yield* project.addSandbox(Instance.project.id, info.directory).pipe(Effect.catch(() => Effect.void))
|
||||||
})
|
})
|
||||||
|
|
||||||
const boot = Effect.fnUntraced(function* (info: Info, startCommand?: string) {
|
const boot = Effect.fnUntraced(function* (info: Info, startCommand?: string) {
|
||||||
|
|
@ -297,7 +299,7 @@ export namespace Worktree {
|
||||||
|
|
||||||
const canonical = Effect.fnUntraced(function* (input: string) {
|
const canonical = Effect.fnUntraced(function* (input: string) {
|
||||||
const abs = pathSvc.resolve(input)
|
const abs = pathSvc.resolve(input)
|
||||||
const real = yield* fsys.realPath(abs).pipe(Effect.catch(() => Effect.succeed(abs)))
|
const real = yield* fs.realPath(abs).pipe(Effect.catch(() => Effect.succeed(abs)))
|
||||||
const normalized = pathSvc.normalize(real)
|
const normalized = pathSvc.normalize(real)
|
||||||
return process.platform === "win32" ? normalized.toLowerCase() : normalized
|
return process.platform === "win32" ? normalized.toLowerCase() : normalized
|
||||||
})
|
})
|
||||||
|
|
@ -334,7 +336,7 @@ export namespace Worktree {
|
||||||
})
|
})
|
||||||
|
|
||||||
function stopFsmonitor(target: string) {
|
function stopFsmonitor(target: string) {
|
||||||
return fsys.exists(target).pipe(
|
return fs.exists(target).pipe(
|
||||||
Effect.orDie,
|
Effect.orDie,
|
||||||
Effect.flatMap((exists) => (exists ? git(["fsmonitor--daemon", "stop"], { cwd: target }) : Effect.void)),
|
Effect.flatMap((exists) => (exists ? git(["fsmonitor--daemon", "stop"], { cwd: target }) : Effect.void)),
|
||||||
)
|
)
|
||||||
|
|
@ -364,7 +366,7 @@ export namespace Worktree {
|
||||||
const entry = yield* locateWorktree(entries, directory)
|
const entry = yield* locateWorktree(entries, directory)
|
||||||
|
|
||||||
if (!entry?.path) {
|
if (!entry?.path) {
|
||||||
const directoryExists = yield* fsys.exists(directory).pipe(Effect.orDie)
|
const directoryExists = yield* fs.exists(directory).pipe(Effect.orDie)
|
||||||
if (directoryExists) {
|
if (directoryExists) {
|
||||||
yield* stopFsmonitor(directory)
|
yield* stopFsmonitor(directory)
|
||||||
yield* cleanDirectory(directory)
|
yield* cleanDirectory(directory)
|
||||||
|
|
@ -464,7 +466,7 @@ export namespace Worktree {
|
||||||
const target = yield* canonical(pathSvc.resolve(root, entry))
|
const target = yield* canonical(pathSvc.resolve(root, entry))
|
||||||
if (target === base) return
|
if (target === base) return
|
||||||
if (!target.startsWith(`${base}${pathSvc.sep}`)) return
|
if (!target.startsWith(`${base}${pathSvc.sep}`)) return
|
||||||
yield* fsys.remove(target, { recursive: true }).pipe(Effect.ignore)
|
yield* fs.remove(target, { recursive: true }).pipe(Effect.ignore)
|
||||||
}),
|
}),
|
||||||
{ concurrency: "unbounded" },
|
{ concurrency: "unbounded" },
|
||||||
)
|
)
|
||||||
|
|
@ -603,8 +605,9 @@ export namespace Worktree {
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultLayer = layer.pipe(
|
const defaultLayer = layer.pipe(
|
||||||
Layer.provide(CrossSpawnSpawner.layer),
|
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
||||||
Layer.provide(NodeFileSystem.layer),
|
Layer.provide(Project.defaultLayer),
|
||||||
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
Layer.provide(NodePath.layer),
|
Layer.provide(NodePath.layer),
|
||||||
)
|
)
|
||||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,19 @@
|
||||||
import { test, expect, describe, mock, afterEach, spyOn } from "bun:test"
|
import { test, expect, describe, mock, afterEach, spyOn } from "bun:test"
|
||||||
|
import { Effect, Layer, Option } from "effect"
|
||||||
|
import { NodeFileSystem, NodePath } from "@effect/platform-node"
|
||||||
import { Config } from "../../src/config/config"
|
import { Config } from "../../src/config/config"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
import { Auth } from "../../src/auth"
|
import { Auth } from "../../src/auth"
|
||||||
import { AccessToken, Account, AccountID, OrgID } from "../../src/account"
|
import { AccessToken, Account, AccountID, OrgID } from "../../src/account"
|
||||||
|
import { AppFileSystem } from "../../src/filesystem"
|
||||||
|
import { provideTmpdirInstance } from "../fixture/fixture"
|
||||||
import { tmpdir } from "../fixture/fixture"
|
import { tmpdir } from "../fixture/fixture"
|
||||||
|
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
||||||
|
|
||||||
|
/** Infra layer that provides FileSystem, Path, ChildProcessSpawner for test fixtures */
|
||||||
|
const infra = CrossSpawnSpawner.defaultLayer.pipe(
|
||||||
|
Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)),
|
||||||
|
)
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import { pathToFileURL } from "url"
|
import { pathToFileURL } from "url"
|
||||||
|
|
@ -12,6 +22,14 @@ import { ProjectID } from "../../src/project/schema"
|
||||||
import { Filesystem } from "../../src/util/filesystem"
|
import { Filesystem } from "../../src/util/filesystem"
|
||||||
import { BunProc } from "../../src/bun"
|
import { BunProc } from "../../src/bun"
|
||||||
|
|
||||||
|
const emptyAccount = Layer.mock(Account.Service)({
|
||||||
|
active: () => Effect.succeed(Option.none()),
|
||||||
|
})
|
||||||
|
|
||||||
|
const emptyAuth = Layer.mock(Auth.Service)({
|
||||||
|
all: () => Effect.succeed({}),
|
||||||
|
})
|
||||||
|
|
||||||
// Get managed config directory from environment (set in preload.ts)
|
// Get managed config directory from environment (set in preload.ts)
|
||||||
const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR!
|
const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR!
|
||||||
|
|
||||||
|
|
@ -246,43 +264,44 @@ test("preserves env variables when adding $schema to config", async () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test("resolves env templates in account config with account token", async () => {
|
test("resolves env templates in account config with account token", async () => {
|
||||||
const originalActive = Account.active
|
|
||||||
const originalConfig = Account.config
|
|
||||||
const originalToken = Account.token
|
|
||||||
const originalControlToken = process.env["OPENCODE_CONSOLE_TOKEN"]
|
const originalControlToken = process.env["OPENCODE_CONSOLE_TOKEN"]
|
||||||
|
|
||||||
Account.active = mock(async () => ({
|
const fakeAccount = Layer.mock(Account.Service)({
|
||||||
id: AccountID.make("account-1"),
|
active: () =>
|
||||||
email: "user@example.com",
|
Effect.succeed(
|
||||||
url: "https://control.example.com",
|
Option.some({
|
||||||
active_org_id: OrgID.make("org-1"),
|
id: AccountID.make("account-1"),
|
||||||
}))
|
email: "user@example.com",
|
||||||
|
url: "https://control.example.com",
|
||||||
|
active_org_id: OrgID.make("org-1"),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
config: () =>
|
||||||
|
Effect.succeed(
|
||||||
|
Option.some({
|
||||||
|
provider: { opencode: { options: { apiKey: "{env:OPENCODE_CONSOLE_TOKEN}" } } },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
token: () => Effect.succeed(Option.some(AccessToken.make("st_test_token"))),
|
||||||
|
})
|
||||||
|
|
||||||
Account.config = mock(async () => ({
|
const layer = Config.layer.pipe(
|
||||||
provider: {
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
opencode: {
|
Layer.provide(emptyAuth),
|
||||||
options: {
|
Layer.provide(fakeAccount),
|
||||||
apiKey: "{env:OPENCODE_CONSOLE_TOKEN}",
|
Layer.provideMerge(infra),
|
||||||
},
|
)
|
||||||
},
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
Account.token = mock(async () => AccessToken.make("st_test_token"))
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await using tmp = await tmpdir()
|
await provideTmpdirInstance(() =>
|
||||||
await Instance.provide({
|
Config.Service.use((svc) =>
|
||||||
directory: tmp.path,
|
Effect.gen(function* () {
|
||||||
fn: async () => {
|
const config = yield* svc.get()
|
||||||
const config = await Config.get()
|
expect(config.provider?.["opencode"]?.options?.apiKey).toBe("st_test_token")
|
||||||
expect(config.provider?.["opencode"]?.options?.apiKey).toBe("st_test_token")
|
}),
|
||||||
},
|
),
|
||||||
})
|
).pipe(Effect.scoped, Effect.provide(layer), Effect.runPromise)
|
||||||
} finally {
|
} finally {
|
||||||
Account.active = originalActive
|
|
||||||
Account.config = originalConfig
|
|
||||||
Account.token = originalToken
|
|
||||||
if (originalControlToken !== undefined) {
|
if (originalControlToken !== undefined) {
|
||||||
process.env["OPENCODE_CONSOLE_TOKEN"] = originalControlToken
|
process.env["OPENCODE_CONSOLE_TOKEN"] = originalControlToken
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1588,7 +1607,7 @@ test("local .opencode config can override MCP from project config", async () =>
|
||||||
test("project config overrides remote well-known config", async () => {
|
test("project config overrides remote well-known config", async () => {
|
||||||
const originalFetch = globalThis.fetch
|
const originalFetch = globalThis.fetch
|
||||||
let fetchedUrl: string | undefined
|
let fetchedUrl: string | undefined
|
||||||
const mockFetch = mock((url: string | URL | Request) => {
|
globalThis.fetch = mock((url: string | URL | Request) => {
|
||||||
const urlStr = url.toString()
|
const urlStr = url.toString()
|
||||||
if (urlStr.includes(".well-known/opencode")) {
|
if (urlStr.includes(".well-known/opencode")) {
|
||||||
fetchedUrl = urlStr
|
fetchedUrl = urlStr
|
||||||
|
|
@ -1596,13 +1615,7 @@ test("project config overrides remote well-known config", async () => {
|
||||||
new Response(
|
new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
config: {
|
config: {
|
||||||
mcp: {
|
mcp: { jira: { type: "remote", url: "https://jira.example.com/mcp", enabled: false } },
|
||||||
jira: {
|
|
||||||
type: "remote",
|
|
||||||
url: "https://jira.example.com/mcp",
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{ status: 200 },
|
{ status: 200 },
|
||||||
|
|
@ -1610,60 +1623,46 @@ test("project config overrides remote well-known config", async () => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return originalFetch(url)
|
return originalFetch(url)
|
||||||
})
|
}) as unknown as typeof fetch
|
||||||
globalThis.fetch = mockFetch as unknown as typeof fetch
|
|
||||||
|
|
||||||
const originalAuthAll = Auth.all
|
const fakeAuth = Layer.mock(Auth.Service)({
|
||||||
Auth.all = mock(() =>
|
all: () =>
|
||||||
Promise.resolve({
|
Effect.succeed({
|
||||||
"https://example.com": {
|
"https://example.com": new Auth.WellKnown({ type: "wellknown", key: "TEST_TOKEN", token: "test-token" }),
|
||||||
type: "wellknown" as const,
|
}),
|
||||||
key: "TEST_TOKEN",
|
})
|
||||||
token: "test-token",
|
|
||||||
},
|
const layer = Config.layer.pipe(
|
||||||
}),
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
|
Layer.provide(fakeAuth),
|
||||||
|
Layer.provide(emptyAccount),
|
||||||
|
Layer.provideMerge(infra),
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await using tmp = await tmpdir({
|
await provideTmpdirInstance(
|
||||||
git: true,
|
() =>
|
||||||
init: async (dir) => {
|
Config.Service.use((svc) =>
|
||||||
// Project config enables jira (overriding remote default)
|
Effect.gen(function* () {
|
||||||
await Filesystem.write(
|
const config = yield* svc.get()
|
||||||
path.join(dir, "opencode.json"),
|
expect(fetchedUrl).toBe("https://example.com/.well-known/opencode")
|
||||||
JSON.stringify({
|
expect(config.mcp?.jira?.enabled).toBe(true)
|
||||||
$schema: "https://opencode.ai/config.json",
|
|
||||||
mcp: {
|
|
||||||
jira: {
|
|
||||||
type: "remote",
|
|
||||||
url: "https://jira.example.com/mcp",
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
)
|
),
|
||||||
|
{
|
||||||
|
git: true,
|
||||||
|
config: { mcp: { jira: { type: "remote", url: "https://jira.example.com/mcp", enabled: true } } },
|
||||||
},
|
},
|
||||||
})
|
).pipe(Effect.scoped, Effect.provide(layer), Effect.runPromise)
|
||||||
await Instance.provide({
|
|
||||||
directory: tmp.path,
|
|
||||||
fn: async () => {
|
|
||||||
const config = await Config.get()
|
|
||||||
// Verify fetch was called for wellknown config
|
|
||||||
expect(fetchedUrl).toBe("https://example.com/.well-known/opencode")
|
|
||||||
// Project config (enabled: true) should override remote (enabled: false)
|
|
||||||
expect(config.mcp?.jira?.enabled).toBe(true)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} finally {
|
} finally {
|
||||||
globalThis.fetch = originalFetch
|
globalThis.fetch = originalFetch
|
||||||
Auth.all = originalAuthAll
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test("wellknown URL with trailing slash is normalized", async () => {
|
test("wellknown URL with trailing slash is normalized", async () => {
|
||||||
const originalFetch = globalThis.fetch
|
const originalFetch = globalThis.fetch
|
||||||
let fetchedUrl: string | undefined
|
let fetchedUrl: string | undefined
|
||||||
const mockFetch = mock((url: string | URL | Request) => {
|
globalThis.fetch = mock((url: string | URL | Request) => {
|
||||||
const urlStr = url.toString()
|
const urlStr = url.toString()
|
||||||
if (urlStr.includes(".well-known/opencode")) {
|
if (urlStr.includes(".well-known/opencode")) {
|
||||||
fetchedUrl = urlStr
|
fetchedUrl = urlStr
|
||||||
|
|
@ -1671,13 +1670,7 @@ test("wellknown URL with trailing slash is normalized", async () => {
|
||||||
new Response(
|
new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
config: {
|
config: {
|
||||||
mcp: {
|
mcp: { slack: { type: "remote", url: "https://slack.example.com/mcp", enabled: true } },
|
||||||
slack: {
|
|
||||||
type: "remote",
|
|
||||||
url: "https://slack.example.com/mcp",
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{ status: 200 },
|
{ status: 200 },
|
||||||
|
|
@ -1685,43 +1678,35 @@ test("wellknown URL with trailing slash is normalized", async () => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return originalFetch(url)
|
return originalFetch(url)
|
||||||
})
|
}) as unknown as typeof fetch
|
||||||
globalThis.fetch = mockFetch as unknown as typeof fetch
|
|
||||||
|
|
||||||
const originalAuthAll = Auth.all
|
const fakeAuth = Layer.mock(Auth.Service)({
|
||||||
Auth.all = mock(() =>
|
all: () =>
|
||||||
Promise.resolve({
|
Effect.succeed({
|
||||||
"https://example.com/": {
|
"https://example.com/": new Auth.WellKnown({ type: "wellknown", key: "TEST_TOKEN", token: "test-token" }),
|
||||||
type: "wellknown" as const,
|
}),
|
||||||
key: "TEST_TOKEN",
|
})
|
||||||
token: "test-token",
|
|
||||||
},
|
const layer = Config.layer.pipe(
|
||||||
}),
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
|
Layer.provide(fakeAuth),
|
||||||
|
Layer.provide(emptyAccount),
|
||||||
|
Layer.provideMerge(infra),
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await using tmp = await tmpdir({
|
await provideTmpdirInstance(
|
||||||
git: true,
|
() =>
|
||||||
init: async (dir) => {
|
Config.Service.use((svc) =>
|
||||||
await Filesystem.write(
|
Effect.gen(function* () {
|
||||||
path.join(dir, "opencode.json"),
|
yield* svc.get()
|
||||||
JSON.stringify({
|
expect(fetchedUrl).toBe("https://example.com/.well-known/opencode")
|
||||||
$schema: "https://opencode.ai/config.json",
|
|
||||||
}),
|
}),
|
||||||
)
|
),
|
||||||
},
|
{ git: true },
|
||||||
})
|
).pipe(Effect.scoped, Effect.provide(layer), Effect.runPromise)
|
||||||
await Instance.provide({
|
|
||||||
directory: tmp.path,
|
|
||||||
fn: async () => {
|
|
||||||
await Config.get()
|
|
||||||
// Trailing slash should be stripped — no double slash in the fetch URL
|
|
||||||
expect(fetchedUrl).toBe("https://example.com/.well-known/opencode")
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} finally {
|
} finally {
|
||||||
globalThis.fetch = originalFetch
|
globalThis.fetch = originalFetch
|
||||||
Auth.all = originalAuthAll
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
||||||
import { tmpdir } from "../fixture/fixture"
|
import { tmpdir } from "../fixture/fixture"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
|
|
||||||
const live = CrossSpawnSpawner.layer.pipe(Layer.provide(NodeFileSystem.layer), Layer.provide(NodePath.layer))
|
const live = CrossSpawnSpawner.defaultLayer
|
||||||
const fx = testEffect(live)
|
const fx = testEffect(live)
|
||||||
|
|
||||||
function js(code: string, opts?: ChildProcess.CommandOptions) {
|
function js(code: string, opts?: ChildProcess.CommandOptions) {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import path from "path"
|
||||||
import { ConfigProvider, Deferred, Effect, Layer, ManagedRuntime, Option } from "effect"
|
import { ConfigProvider, Deferred, Effect, Layer, ManagedRuntime, Option } from "effect"
|
||||||
import { tmpdir } from "../fixture/fixture"
|
import { tmpdir } from "../fixture/fixture"
|
||||||
import { Bus } from "../../src/bus"
|
import { Bus } from "../../src/bus"
|
||||||
|
import { Config } from "../../src/config/config"
|
||||||
import { FileWatcher } from "../../src/file/watcher"
|
import { FileWatcher } from "../../src/file/watcher"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
|
|
||||||
|
|
@ -30,6 +31,7 @@ function withWatcher<E>(directory: string, body: Effect.Effect<void, E>) {
|
||||||
directory,
|
directory,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const layer: Layer.Layer<FileWatcher.Service, never, never> = FileWatcher.layer.pipe(
|
const layer: Layer.Layer<FileWatcher.Service, never, never> = FileWatcher.layer.pipe(
|
||||||
|
Layer.provide(Config.defaultLayer),
|
||||||
Layer.provide(watcherConfigLayer),
|
Layer.provide(watcherConfigLayer),
|
||||||
)
|
)
|
||||||
const rt = ManagedRuntime.make(layer)
|
const rt = ManagedRuntime.make(layer)
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,14 @@ import { Effect, Layer } from "effect"
|
||||||
import { provideTmpdirInstance } from "../fixture/fixture"
|
import { provideTmpdirInstance } from "../fixture/fixture"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { Format } from "../../src/format"
|
import { Format } from "../../src/format"
|
||||||
|
import { Config } from "../../src/config/config"
|
||||||
import * as Formatter from "../../src/format/formatter"
|
import * as Formatter from "../../src/format/formatter"
|
||||||
|
|
||||||
const node = NodeChildProcessSpawner.layer.pipe(
|
const node = NodeChildProcessSpawner.layer.pipe(
|
||||||
Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)),
|
Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)),
|
||||||
)
|
)
|
||||||
|
|
||||||
const it = testEffect(Layer.mergeAll(Format.layer, node))
|
const it = testEffect(Layer.mergeAll(Format.layer, node).pipe(Layer.provide(Config.defaultLayer)))
|
||||||
|
|
||||||
describe("Format", () => {
|
describe("Format", () => {
|
||||||
it.effect("status() returns built-in formatters when no config overrides", () =>
|
it.effect("status() returns built-in formatters when no config overrides", () =>
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ function mockGitFailure(failArg: string) {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
).pipe(Layer.provide(CrossSpawnSpawner.layer), Layer.provide(NodeFileSystem.layer), Layer.provide(NodePath.layer))
|
).pipe(Layer.provide(CrossSpawnSpawner.defaultLayer))
|
||||||
}
|
}
|
||||||
|
|
||||||
function projectLayerWithFailure(failArg: string) {
|
function projectLayerWithFailure(failArg: string) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue