feat(scout): materialize configured reference repos (#26692)
This commit is contained in:
@@ -147,6 +147,10 @@ test("reference config creates scout-backed subagents", async () => {
|
||||
config: {
|
||||
reference: {
|
||||
effect: "github.com/effect/effect-smol",
|
||||
effectDev: {
|
||||
repository: "https://github.com/effect/effect-smol",
|
||||
branch: "dev",
|
||||
},
|
||||
effectFull: {
|
||||
repository: "Effect-TS/effect",
|
||||
branch: "main",
|
||||
@@ -162,6 +166,7 @@ test("reference config creates scout-backed subagents", async () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const effect = await load(tmp.path, (svc) => svc.get("effect"))
|
||||
const effectDev = await load(tmp.path, (svc) => svc.get("effectDev"))
|
||||
const effectFull = await load(tmp.path, (svc) => svc.get("effectFull"))
|
||||
const local = await load(tmp.path, (svc) => svc.get("localdocs"))
|
||||
const localFull = await load(tmp.path, (svc) => svc.get("localdocsFull"))
|
||||
@@ -169,13 +174,19 @@ test("reference config creates scout-backed subagents", async () => {
|
||||
expect(effect).toBeDefined()
|
||||
expect(effect?.mode).toBe("subagent")
|
||||
expect(effect?.prompt).toContain("Repository: github.com/effect/effect-smol")
|
||||
expect(evalPerm(effect, "repo_clone")).toBe("allow")
|
||||
expect(effect?.prompt).toContain(`Cached directory: ${path.join(Global.Path.repos, "github.com", "effect", "effect-smol")}`)
|
||||
expect(effect?.prompt).toContain("Do not call repo_clone")
|
||||
expect(evalPerm(effect, "repo_clone")).toBe("deny")
|
||||
|
||||
expect(effectDev).toBeDefined()
|
||||
expect(effectDev?.prompt).toContain("Problem: Reference conflicts with @effect")
|
||||
expect(effectDev?.prompt).not.toContain("Cached directory:")
|
||||
|
||||
expect(effectFull).toBeDefined()
|
||||
expect(effectFull?.mode).toBe("subagent")
|
||||
expect(effectFull?.prompt).toContain("Repository: Effect-TS/effect")
|
||||
expect(effectFull?.prompt).toContain("Branch/ref: main")
|
||||
expect(evalPerm(effectFull, "repo_clone")).toBe("allow")
|
||||
expect(evalPerm(effectFull, "repo_clone")).toBe("deny")
|
||||
|
||||
expect(local).toBeDefined()
|
||||
expect(local?.mode).toBe("subagent")
|
||||
|
||||
249
packages/opencode/test/reference/reference.test.ts
Normal file
249
packages/opencode/test/reference/reference.test.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
import { afterEach, describe, expect } from "bun:test"
|
||||
import path from "path"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Git } from "../../src/git"
|
||||
import { Reference } from "../../src/reference/reference"
|
||||
import { disposeAllInstances, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
afterEach(async () => {
|
||||
await disposeAllInstances()
|
||||
})
|
||||
|
||||
const it = testEffect(
|
||||
Layer.mergeAll(
|
||||
AppFileSystem.defaultLayer,
|
||||
CrossSpawnSpawner.defaultLayer,
|
||||
Git.defaultLayer,
|
||||
Reference.defaultLayer,
|
||||
),
|
||||
)
|
||||
|
||||
const experimentalScout = <A, E, R>(self: Effect.Effect<A, E, R>) =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const previous = Flag.OPENCODE_EXPERIMENTAL_SCOUT
|
||||
Flag.OPENCODE_EXPERIMENTAL_SCOUT = true
|
||||
return previous
|
||||
}),
|
||||
() => self,
|
||||
(previous) =>
|
||||
Effect.sync(() => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_SCOUT = previous
|
||||
}),
|
||||
)
|
||||
|
||||
const githubBase = <A, E, R>(url: string, self: Effect.Effect<A, E, R>) =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const previous = process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL
|
||||
process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL = url
|
||||
return previous
|
||||
}),
|
||||
() => self,
|
||||
(previous) =>
|
||||
Effect.sync(() => {
|
||||
if (previous) process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL = previous
|
||||
else delete process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL
|
||||
}),
|
||||
)
|
||||
|
||||
const git = Effect.fn("ReferenceTest.git")(function* (cwd: string, args: string[]) {
|
||||
return yield* Effect.promise(async () => {
|
||||
const proc = Bun.spawn(["git", ...args], {
|
||||
cwd,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
const [stdout, stderr, code] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
])
|
||||
if (code !== 0) throw new Error(stderr.trim() || stdout.trim() || `git ${args.join(" ")} failed`)
|
||||
return stdout.trim()
|
||||
})
|
||||
})
|
||||
|
||||
const waitForContent = (
|
||||
fs: AppFileSystem.Interface,
|
||||
file: string,
|
||||
content: string,
|
||||
attempts = 50,
|
||||
): Effect.Effect<void, AppFileSystem.Error> =>
|
||||
Effect.gen(function* () {
|
||||
if ((yield* fs.readFileStringSafe(file)) === content) return
|
||||
if (attempts <= 0) throw new Error(`timed out waiting for ${file}`)
|
||||
yield* Effect.sleep("100 millis")
|
||||
yield* waitForContent(fs, file, content, attempts - 1)
|
||||
})
|
||||
|
||||
describe("reference", () => {
|
||||
it.live("resolves local and git references", () =>
|
||||
Effect.gen(function* () {
|
||||
const root = path.resolve("opencode-reference-root")
|
||||
const local = Reference.resolve({
|
||||
name: "docs",
|
||||
reference: { path: "../docs" },
|
||||
directory: path.join(root, "packages", "app"),
|
||||
worktree: root,
|
||||
})
|
||||
const repo = Reference.resolve({
|
||||
name: "effect",
|
||||
reference: { repository: "Effect-TS/effect", branch: "main" },
|
||||
directory: path.join(root, "packages", "app"),
|
||||
worktree: root,
|
||||
})
|
||||
|
||||
expect(local.kind).toBe("local")
|
||||
if (local.kind === "local") expect(local.path).toBe(path.resolve(root, "../docs"))
|
||||
expect(repo.kind).toBe("git")
|
||||
if (repo.kind === "git") {
|
||||
expect(repo.repository).toBe("Effect-TS/effect")
|
||||
expect(repo.branch).toBe("main")
|
||||
expect(repo.path).toBe(path.join(Global.Path.repos, "github.com", "Effect-TS", "effect"))
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("marks same-cache references with different branches invalid", () =>
|
||||
Effect.gen(function* () {
|
||||
const root = path.resolve("opencode-reference-root")
|
||||
const references = Reference.resolveAll({
|
||||
directory: root,
|
||||
worktree: root,
|
||||
references: {
|
||||
main: { repository: "owner/repo", branch: "main" },
|
||||
dev: { repository: "github.com/owner/repo", branch: "dev" },
|
||||
alsoMain: { repository: "https://github.com/owner/repo", branch: "main" },
|
||||
},
|
||||
})
|
||||
|
||||
expect(references.map((reference) => reference.kind)).toEqual(["git", "invalid", "git"])
|
||||
expect(references[1]?.kind).toBe("invalid")
|
||||
if (references[1]?.kind === "invalid") {
|
||||
expect(references[1].message).toContain("conflicts with @main")
|
||||
expect(references[1].message).toContain("@dev requests dev")
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("materializes configured git references during init", () =>
|
||||
experimentalScout(
|
||||
provideTmpdirInstance(
|
||||
(_dir) =>
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const cache = path.join(Global.Path.repos, "github.com", "opencode-reference-test", "repo")
|
||||
yield* fs.remove(cache, { recursive: true }).pipe(Effect.ignore)
|
||||
yield* Effect.addFinalizer(() => fs.remove(cache, { recursive: true }).pipe(Effect.ignore))
|
||||
|
||||
const source = yield* tmpdirScoped({ git: true })
|
||||
const remoteRoot = yield* tmpdirScoped()
|
||||
const remoteDir = path.join(remoteRoot, "opencode-reference-test")
|
||||
const remoteRepo = path.join(remoteDir, "repo.git")
|
||||
|
||||
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "configured\n"))
|
||||
yield* git(source, ["add", "."])
|
||||
yield* git(source, ["commit", "-m", "add readme"])
|
||||
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
|
||||
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
|
||||
|
||||
const reference = yield* Reference.Service
|
||||
yield* githubBase(
|
||||
`file://${remoteRoot}/`,
|
||||
Effect.gen(function* () {
|
||||
yield* reference.init()
|
||||
yield* waitForContent(fs, path.join(cache, "README.md"), "configured\n")
|
||||
}),
|
||||
)
|
||||
|
||||
expect(yield* fs.existsSafe(path.join(cache, ".git"))).toBe(true)
|
||||
expect(yield* fs.readFileString(path.join(cache, "README.md"))).toBe("configured\n")
|
||||
|
||||
const resolved = yield* reference.get("docs")
|
||||
expect(resolved?.kind).toBe("git")
|
||||
if (resolved?.kind === "git") expect(resolved.path).toBe(cache)
|
||||
}),
|
||||
{
|
||||
config: {
|
||||
reference: {
|
||||
docs: "opencode-reference-test/repo",
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("refreshes configured git references on new instance init", () =>
|
||||
experimentalScout(
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const cache = path.join(Global.Path.repos, "github.com", "opencode-reference-refresh", "repo")
|
||||
yield* fs.remove(cache, { recursive: true }).pipe(Effect.ignore)
|
||||
yield* Effect.addFinalizer(() => fs.remove(cache, { recursive: true }).pipe(Effect.ignore))
|
||||
|
||||
const source = yield* tmpdirScoped({ git: true })
|
||||
const remoteRoot = yield* tmpdirScoped()
|
||||
const remoteDir = path.join(remoteRoot, "opencode-reference-refresh")
|
||||
const remoteRepo = path.join(remoteDir, "repo.git")
|
||||
|
||||
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "v1\n"))
|
||||
yield* git(source, ["add", "."])
|
||||
yield* git(source, ["commit", "-m", "add readme"])
|
||||
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
|
||||
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
|
||||
|
||||
yield* githubBase(
|
||||
`file://${remoteRoot}/`,
|
||||
provideTmpdirInstance(
|
||||
(_dir) =>
|
||||
Effect.gen(function* () {
|
||||
const reference = yield* Reference.Service
|
||||
yield* reference.init()
|
||||
yield* waitForContent(fs, path.join(cache, "README.md"), "v1\n")
|
||||
}),
|
||||
{
|
||||
config: {
|
||||
reference: {
|
||||
docs: "opencode-reference-refresh/repo",
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
const branch = yield* git(source, ["branch", "--show-current"])
|
||||
yield* git(source, ["remote", "add", "origin", remoteRepo])
|
||||
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "v2\n"))
|
||||
yield* git(source, ["add", "."])
|
||||
yield* git(source, ["commit", "-m", "update readme"])
|
||||
yield* git(source, ["push", "origin", `${branch}:${branch}`])
|
||||
|
||||
yield* githubBase(
|
||||
`file://${remoteRoot}/`,
|
||||
provideTmpdirInstance(
|
||||
(_dir) =>
|
||||
Effect.gen(function* () {
|
||||
const reference = yield* Reference.Service
|
||||
yield* reference.init()
|
||||
yield* waitForContent(fs, path.join(cache, "README.md"), "v2\n")
|
||||
}),
|
||||
{
|
||||
config: {
|
||||
reference: {
|
||||
docs: "opencode-reference-refresh/repo",
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
@@ -46,6 +46,7 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import * as Database from "../../src/storage/db"
|
||||
import { Ripgrep } from "../../src/file/ripgrep"
|
||||
import { Format } from "../../src/format"
|
||||
import { Reference } from "../../src/reference/reference"
|
||||
import { provideTmpdirInstance, provideTmpdirServer } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { reply, TestLLMServer } from "../lib/llm-server"
|
||||
@@ -181,6 +182,7 @@ function makeHttp() {
|
||||
Layer.provide(FetchHttpClient.layer),
|
||||
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
||||
Layer.provide(Git.defaultLayer),
|
||||
Layer.provide(Reference.defaultLayer),
|
||||
Layer.provide(Ripgrep.defaultLayer),
|
||||
Layer.provide(Format.defaultLayer),
|
||||
Layer.provideMerge(todo),
|
||||
|
||||
@@ -57,6 +57,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { Ripgrep } from "../../src/file/ripgrep"
|
||||
import { Format } from "../../src/format"
|
||||
import { Reference } from "../../src/reference/reference"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
@@ -131,6 +132,7 @@ function makeHttp() {
|
||||
Layer.provide(FetchHttpClient.layer),
|
||||
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
||||
Layer.provide(Git.defaultLayer),
|
||||
Layer.provide(Reference.defaultLayer),
|
||||
Layer.provide(Ripgrep.defaultLayer),
|
||||
Layer.provide(Format.defaultLayer),
|
||||
Layer.provideMerge(todo),
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Truncate } from "@/tool/truncate"
|
||||
import { Agent } from "../../src/agent/agent"
|
||||
import { TestInstance } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { Reference } from "@/reference/reference"
|
||||
|
||||
const it = testEffect(
|
||||
Layer.mergeAll(
|
||||
@@ -18,6 +19,7 @@ const it = testEffect(
|
||||
Ripgrep.defaultLayer,
|
||||
Truncate.defaultLayer,
|
||||
Agent.defaultLayer,
|
||||
Reference.defaultLayer,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Agent } from "../../src/agent/agent"
|
||||
import { Ripgrep } from "../../src/file/ripgrep"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { Reference } from "@/reference/reference"
|
||||
|
||||
const it = testEffect(
|
||||
Layer.mergeAll(
|
||||
@@ -18,6 +19,7 @@ const it = testEffect(
|
||||
Ripgrep.defaultLayer,
|
||||
Truncate.defaultLayer,
|
||||
Agent.defaultLayer,
|
||||
Reference.defaultLayer,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import path from "path"
|
||||
import { Agent } from "../../src/agent/agent"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { LSP } from "@/lsp/lsp"
|
||||
import { Permission } from "../../src/permission"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
@@ -15,6 +17,7 @@ import { Tool } from "@/tool/tool"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { disposeAllInstances, provideInstance, TestInstance, tmpdirScoped } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { Reference } from "@/reference/reference"
|
||||
|
||||
const FIXTURES_DIR = path.join(import.meta.dir, "fixtures")
|
||||
|
||||
@@ -40,6 +43,7 @@ const it = testEffect(
|
||||
CrossSpawnSpawner.defaultLayer,
|
||||
Instruction.defaultLayer,
|
||||
LSP.defaultLayer,
|
||||
Reference.defaultLayer,
|
||||
Truncate.defaultLayer,
|
||||
),
|
||||
)
|
||||
@@ -81,6 +85,49 @@ const fail = Effect.fn("ReadToolTest.fail")(function* (
|
||||
const full = (p: string) => (process.platform === "win32" ? Filesystem.normalizePath(p) : p)
|
||||
const glob = (p: string) =>
|
||||
process.platform === "win32" ? Filesystem.normalizePathPattern(p) : p.replaceAll("\\", "/")
|
||||
const experimentalScout = <A, E, R>(self: Effect.Effect<A, E, R>) =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const previous = Flag.OPENCODE_EXPERIMENTAL_SCOUT
|
||||
Flag.OPENCODE_EXPERIMENTAL_SCOUT = true
|
||||
return previous
|
||||
}),
|
||||
() => self,
|
||||
(previous) =>
|
||||
Effect.sync(() => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_SCOUT = previous
|
||||
}),
|
||||
)
|
||||
const githubBase = <A, E, R>(url: string, self: Effect.Effect<A, E, R>) =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const previous = process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL
|
||||
process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL = url
|
||||
return previous
|
||||
}),
|
||||
() => self,
|
||||
(previous) =>
|
||||
Effect.sync(() => {
|
||||
if (previous) process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL = previous
|
||||
else delete process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL
|
||||
}),
|
||||
)
|
||||
const git = Effect.fn("ReadToolTest.git")(function* (cwd: string, args: string[]) {
|
||||
return yield* Effect.promise(async () => {
|
||||
const proc = Bun.spawn(["git", ...args], {
|
||||
cwd,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
const [stdout, stderr, code] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
])
|
||||
if (code !== 0) throw new Error(stderr.trim() || stdout.trim() || `git ${args.join(" ")} failed`)
|
||||
return stdout.trim()
|
||||
})
|
||||
})
|
||||
const put = Effect.fn("ReadToolTest.put")(function* (p: string, content: string | Buffer | Uint8Array) {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
yield* fs.writeWithDirs(p, content)
|
||||
@@ -212,6 +259,46 @@ describe("tool.read external_directory permission", () => {
|
||||
expect(ext).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("does not ask for external_directory permission when reading configured references", () =>
|
||||
experimentalScout(
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const cache = path.join(Global.Path.repos, "github.com", "opencode-read-reference", "repo")
|
||||
yield* fs.remove(cache, { recursive: true }).pipe(Effect.ignore)
|
||||
yield* Effect.addFinalizer(() => fs.remove(cache, { recursive: true }).pipe(Effect.ignore))
|
||||
|
||||
const source = yield* tmpdirScoped({ git: true })
|
||||
const remoteRoot = yield* tmpdirScoped()
|
||||
const remoteDir = path.join(remoteRoot, "opencode-read-reference")
|
||||
const remoteRepo = path.join(remoteDir, "repo.git")
|
||||
yield* put(path.join(source, "notes.md"), "reference notes")
|
||||
yield* git(source, ["add", "."])
|
||||
yield* git(source, ["commit", "-m", "add notes"])
|
||||
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
|
||||
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
|
||||
|
||||
const dir = yield* tmpdirScoped({
|
||||
git: true,
|
||||
config: {
|
||||
reference: {
|
||||
docs: "opencode-read-reference/repo",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { items, next } = asks()
|
||||
const result = yield* githubBase(
|
||||
`file://${remoteRoot}/`,
|
||||
exec(dir, { filePath: path.join(cache, "notes.md") }, next),
|
||||
)
|
||||
const ext = items.find((item) => item.permission === "external_directory")
|
||||
|
||||
expect(result.output).toContain("reference notes")
|
||||
expect(ext).toBeUndefined()
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
describe("tool.read env file permissions", () => {
|
||||
|
||||
@@ -25,6 +25,7 @@ import { Format } from "@/format"
|
||||
import { Ripgrep } from "@/file/ripgrep"
|
||||
import * as Truncate from "@/tool/truncate"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { Reference } from "@/reference/reference"
|
||||
|
||||
const node = CrossSpawnSpawner.defaultLayer
|
||||
const originalExperimentalScout = Flag.OPENCODE_EXPERIMENTAL_SCOUT
|
||||
@@ -42,6 +43,7 @@ const registryLayer = ToolRegistry.layer.pipe(
|
||||
Layer.provide(Session.defaultLayer),
|
||||
Layer.provide(Provider.defaultLayer),
|
||||
Layer.provide(Git.defaultLayer),
|
||||
Layer.provide(Reference.defaultLayer),
|
||||
Layer.provide(LSP.defaultLayer),
|
||||
Layer.provide(Instruction.defaultLayer),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
|
||||
Reference in New Issue
Block a user