Compare commits

...

7 Commits

Author SHA1 Message Date
Shoubhit Dash 1bb7b299e5 test(reference): serialize github base overrides 2026-06-03 19:30:48 +05:30
Shoubhit Dash 330faabf9a fix(reference): own legacy warmup before core fork 2026-06-03 18:50:03 +05:30
Shoubhit Dash cfe8327e19 fix(reference): initialize bridge state before returning 2026-06-03 17:50:04 +05:30
Shoubhit Dash cc198ce525 test(reference): allow slower windows materialization 2026-06-03 17:06:16 +05:30
Shoubhit Dash c5175b6cc1 fix(opencode): decode legacy reference config 2026-06-03 16:37:28 +05:30
Shoubhit Dash 00b3212b82 refactor(opencode): bridge project references to core 2026-06-03 16:28:49 +05:30
Shoubhit Dash 0ce1866e8e fix(core): resolve local references from location root 2026-06-03 16:28:30 +05:30
24 changed files with 648 additions and 1021 deletions
+1 -1
View File
@@ -73,7 +73,7 @@ export const layer = Layer.effect(
references: ConfigReference.normalize(
Object.assign({}, ...(yield* config.get()).map((document) => document.info.references ?? {})),
),
directory: location.project.directory,
directory: location.vcs ? location.project.directory : location.directory,
home: global.home,
repos: global.repos,
})
+5 -2
View File
@@ -2,11 +2,14 @@ import { Location } from "@opencode-ai/core/location"
import { Project } from "@opencode-ai/core/project"
import { AbsolutePath } from "@opencode-ai/core/schema"
export function location(ref: Location.Ref, input: { projectDirectory?: AbsolutePath; vcs?: Project.Vcs } = {}) {
export function location(
ref: Location.Ref,
input: { projectID?: Project.ID; projectDirectory?: AbsolutePath; vcs?: Project.Vcs } = {},
) {
return {
directory: ref.directory,
workspaceID: ref.workspaceID,
project: { id: Project.ID.global, directory: input.projectDirectory ?? ref.directory },
project: { id: input.projectID ?? Project.ID.global, directory: input.projectDirectory ?? ref.directory },
vcs: input.vcs,
} satisfies Location.Interface
}
+67 -1
View File
@@ -8,6 +8,7 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Global } from "@opencode-ai/core/global"
import { Location } from "@opencode-ai/core/location"
import { Project } from "@opencode-ai/core/project"
import { ProjectReference } from "@opencode-ai/core/project-reference"
import { Repository } from "@opencode-ai/core/repository"
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
@@ -86,6 +87,64 @@ describe("ProjectReference", () => {
}),
)
it.live("resolves local references from the opened directory for global locations", () =>
withoutReferences(
withTmp((tmp) =>
withReferences(
Effect.gen(function* () {
const references = yield* ProjectReference.Service
expect(yield* references.get("docs")).toMatchObject({
name: "docs",
kind: "local",
path: path.join(tmp.path, "opened", "docs"),
})
}).pipe(
Effect.provide(
testLayer({
directory: path.join(tmp.path, "opened"),
project: "/",
projectID: Project.ID.global,
repos: path.join(tmp.path, "repos"),
documents: [document({ docs: "./docs" })],
ensure: () => Effect.die("unexpected ensure"),
}),
),
),
),
),
),
)
it.live("resolves local references from the project root for global-id Git locations", () =>
withoutReferences(
withTmp((tmp) => {
const project = path.join(tmp.path, "project")
return withReferences(
Effect.gen(function* () {
const references = yield* ProjectReference.Service
expect(yield* references.get("docs")).toMatchObject({
name: "docs",
kind: "local",
path: path.join(project, "docs"),
})
}).pipe(
Effect.provide(
testLayer({
directory: path.join(project, "nested"),
project,
projectID: Project.ID.global,
vcs: { type: "git", store: AbsolutePath.make(path.join(project, ".git")) },
repos: path.join(tmp.path, "repos"),
documents: [document({ docs: "./docs" })],
ensure: () => Effect.die("unexpected ensure"),
}),
),
),
)
}),
),
)
it.live("merges config aliases and exposes mention and managed-path operations", () =>
withoutReferences(
withTmp((tmp) => {
@@ -139,6 +198,7 @@ describe("ProjectReference", () => {
testLayer({
directory: nested,
project,
vcs: { type: "git", store: AbsolutePath.make(path.join(project, ".git")) },
repos,
documents: [
document({ docs: { path: "./old-docs" }, sdk: "owner/old" }),
@@ -236,6 +296,8 @@ function result(
function testLayer(input: {
directory: string
project: string
projectID?: Project.ID
vcs?: Project.Vcs
repos: string
documents: Config.Loaded[]
ensure: RepositoryCache.Interface["ensure"]
@@ -250,7 +312,11 @@ function testLayer(input: {
Location.Service.of(
location(
{ directory: AbsolutePath.make(input.directory) },
{ projectDirectory: AbsolutePath.make(input.project) },
{
projectID: input.projectID ?? Project.ID.make("project"),
projectDirectory: AbsolutePath.make(input.project),
vcs: input.vcs,
},
),
),
),
@@ -18,8 +18,9 @@ import { Locale } from "@/util/locale"
import type { PromptInfo } from "./history"
import { useFrecency } from "./frecency"
import { useBindings, useCommandSlashes, useOpencodeModeStack } from "../../keymap"
import { Reference } from "@/reference/reference"
import { ConfigReference } from "@/config/reference"
import { Reference } from "@/reference"
import { ConfigReference } from "@opencode-ai/core/config/reference"
import { Flag } from "@opencode-ai/core/flag/flag"
import { displayCharAt, mentionTriggerIndex } from "@/cli/cmd/prompt-display"
function removeLineRange(input: string) {
@@ -329,11 +330,13 @@ export function Autocomplete(props: {
}
const references = createMemo(() =>
Reference.resolveAll({
references: ConfigReference.normalize(sync.data.config.reference ?? {}),
directory: sync.path.directory || process.cwd(),
worktree: sync.path.worktree || sync.path.directory || process.cwd(),
}),
Flag.OPENCODE_EXPERIMENTAL_REFERENCES
? Reference.resolveAll({
references: ConfigReference.normalize(sync.data.config.reference ?? {}),
directory: sync.path.directory || process.cwd(),
worktree: sync.path.worktree || sync.path.directory || process.cwd(),
})
: [],
)
const referenceSearch = createMemo(() => {
-48
View File
@@ -1,48 +0,0 @@
export * as ConfigReference from "./reference"
import { ConfigReferenceV1 } from "@opencode-ai/core/v1/config/reference"
export type NormalizedEntry =
| {
kind: "local"
path: string
}
| {
kind: "git"
repository: string
branch?: string
}
| {
kind: "invalid"
message: string
}
export type NormalizedInfo = Record<string, NormalizedEntry>
export function validateAlias(name: string) {
if (name.length === 0) return "Reference alias must not be empty"
if (/[\/\s`,]/.test(name)) {
return "Reference alias must not contain /, whitespace, comma, or backtick"
}
}
export function normalizeEntry(entry: ConfigReferenceV1.Entry): NormalizedEntry {
if (typeof entry === "string") {
if (entry.startsWith(".") || entry.startsWith("/") || entry.startsWith("~")) {
return { kind: "local", path: entry }
}
return { kind: "git", repository: entry }
}
if ("path" in entry) return { kind: "local", path: entry.path }
return { kind: "git", repository: entry.repository, branch: entry.branch }
}
export function normalize(info: ConfigReferenceV1.Info): NormalizedInfo {
return Object.fromEntries(
Object.entries(info).map(([name, entry]) => {
const aliasError = validateAlias(name)
return [name, aliasError ? { kind: "invalid" as const, message: aliasError } : normalizeEntry(entry)] as const
}),
)
}
+1 -1
View File
@@ -41,7 +41,7 @@ import { Format } from "@/format"
import { InstanceLayer } from "@/project/instance-layer"
import { Project } from "@/project/project"
import { Vcs } from "@/project/vcs"
import { Reference } from "@/reference/reference"
import { Reference } from "@/reference"
import { Workspace } from "@/control-plane/workspace"
import { Worktree } from "@/worktree"
import { Installation } from "@/installation"
+1 -1
View File
@@ -9,7 +9,7 @@ import { ShareNext } from "@/share/share-next"
import { Effect, Layer } from "effect"
import { Config } from "@/config/config"
import { Service } from "./bootstrap-service"
import { Reference } from "@/reference/reference"
import { Reference } from "@/reference"
export { Service } from "./bootstrap-service"
export type { Interface } from "./bootstrap-service"
+112
View File
@@ -0,0 +1,112 @@
export * as Reference from "./reference"
import * as InstanceState from "@/effect/instance-state"
import { Config } from "@/config/config"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Global } from "@opencode-ai/core/global"
import { Location } from "@opencode-ai/core/location"
import { ProjectReference } from "@opencode-ai/core/project-reference"
import { ConfigReference } from "@opencode-ai/core/config/reference"
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Context, Effect, Layer, Schema, Scope } from "effect"
export type Resolved = ProjectReference.Resolved
export interface Interface {
readonly init: () => Effect.Effect<void>
readonly list: () => Effect.Effect<Resolved[]>
readonly get: (name: string) => Effect.Effect<Resolved | undefined>
readonly ensure: (target?: string) => Effect.Effect<void>
readonly contains: (target?: string) => Effect.Effect<boolean>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Reference") {}
export function resolve(input: {
name: string
reference: ConfigReference.NormalizedEntry
directory: string
worktree: string
}) {
return ProjectReference.resolve({
name: input.name,
reference: input.reference,
directory: input.worktree === "/" ? input.directory : input.worktree,
home: Global.Path.home,
repos: Global.Path.repos,
})
}
export function resolveAll(input: { references: ConfigReference.NormalizedInfo; directory: string; worktree: string }) {
return ProjectReference.resolveAll({
references: input.references,
directory: input.worktree === "/" ? input.directory : input.worktree,
home: Global.Path.home,
repos: Global.Path.repos,
})
}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const config = yield* Config.Service
const scope = yield* Scope.Scope
const state = yield* InstanceState.make(
Effect.fn("Reference.state")(function* (ctx) {
const { Config: ConfigV2 } = yield* Effect.promise(() => import("@opencode-ai/core/config"))
const cfg = yield* config.get()
const base = AbsolutePath.make(ctx.worktree === "/" ? ctx.directory : ctx.worktree)
const layer = ProjectReference.layer.pipe(
Layer.provide(
Layer.mergeAll(
FSUtil.defaultLayer,
Global.defaultLayer,
RepositoryCache.defaultLayer,
Layer.succeed(
Location.Service,
Location.Service.of({ directory: base, project: { id: ctx.project.id, directory: base } }),
),
Layer.succeed(
ConfigV2.Service,
ConfigV2.Service.of({
directories: () => Effect.succeed([]),
get: () =>
Effect.succeed([
new ConfigV2.Loaded({
source: { type: "memory" },
info: Schema.decodeUnknownSync(ConfigV2.Info)({ references: cfg.reference }),
}),
]),
}),
),
),
),
)
return Context.get(yield* Layer.build(layer), ProjectReference.Service)
}),
)
const ensure = Effect.fn("Reference.ensure")(function* (target?: string) {
yield* InstanceState.useEffect(state, (service) => service.ensurePath(target)).pipe(Effect.ignoreCause)
})
return Service.of({
init: Effect.fn("Reference.init")(function* () {
yield* ensure().pipe(Effect.forkIn(scope), Effect.asVoid)
}),
list: Effect.fn("Reference.list")(function* () {
return yield* InstanceState.useEffect(state, (service) => service.list())
}),
get: Effect.fn("Reference.get")(function* (name: string) {
return yield* InstanceState.useEffect(state, (service) => service.get(name))
}),
ensure,
contains: Effect.fn("Reference.contains")(function* (target?: string) {
return yield* InstanceState.useEffect(state, (service) => service.containsManagedPath(target))
}),
})
}),
)
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer))
@@ -1,239 +0,0 @@
import path from "path"
import { Effect, Context, Layer, Scope } from "effect"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Global } from "@opencode-ai/core/global"
import { Config } from "@/config/config"
import { ConfigReference } from "@/config/reference"
import { InstanceState } from "@/effect/instance-state"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { parseRepositoryReference, repositoryCachePath, type RemoteReference } from "@/util/repository"
import { RepositoryCache } from "./repository-cache"
export type Resolved =
| {
name: string
kind: "local"
path: string
}
| {
name: string
kind: "git"
repository: string
reference: RemoteReference
path: string
branch?: string
}
| {
name: string
kind: "invalid"
repository?: string
message: string
}
type State = {
references: Resolved[]
materializeAll: Effect.Effect<void>
materializeByPath: Materializer[]
}
type Materializer = { path: string; run: Effect.Effect<void> }
export interface Interface {
readonly init: () => Effect.Effect<void>
readonly list: () => Effect.Effect<Resolved[]>
readonly get: (name: string) => Effect.Effect<Resolved | undefined>
readonly ensure: (target?: string) => Effect.Effect<void>
readonly contains: (target?: string) => Effect.Effect<boolean>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Reference") {}
export function referencePath(input: { directory: string; worktree: string; value: string }) {
if (input.value.startsWith("~/")) return path.join(Global.Path.home, input.value.slice(2))
return path.isAbsolute(input.value)
? input.value
: path.resolve(input.worktree === "/" ? input.directory : input.worktree, input.value)
}
function resolveGit(
input: { name: string; repository: string } | { name: string; repository: string; branch: string | undefined },
): Resolved {
const parsed = parseRepositoryReference(input.repository)
if (!parsed || parsed.protocol === "file:") {
return {
name: input.name,
kind: "invalid",
repository: input.repository,
message: "Repository must be a git URL, host/path reference, or GitHub owner/repo shorthand",
}
}
return {
name: input.name,
kind: "git",
repository: input.repository,
reference: parsed,
path: repositoryCachePath(parsed),
...("branch" in input ? { branch: input.branch } : {}),
}
}
function branchLabel(branch: string | undefined) {
return branch ?? "default branch"
}
function normalizedTarget(target?: string) {
if (!target) return
return process.platform === "win32" ? FSUtil.normalizePath(target) : target
}
function containsReferencePath(referencePath: string, target: string) {
return FSUtil.contains(normalizedTarget(referencePath) ?? referencePath, target)
}
function uniqueGitReferences(references: Resolved[]) {
const seenPath = new Set<string>()
return references.filter((reference): reference is Extract<Resolved, { kind: "git" }> => {
if (reference.kind !== "git") return false
if (seenPath.has(reference.path)) return false
seenPath.add(reference.path)
return true
})
}
function materializeReference(cache: RepositoryCache.Interface, reference: Extract<Resolved, { kind: "git" }>) {
return cache.ensure({ reference: reference.reference, branch: reference.branch, refresh: true }).pipe(
Effect.asVoid,
Effect.catchCause((cause) =>
Effect.logWarning("failed to materialize reference repository").pipe(
Effect.annotateLogs({ name: reference.name, cause }),
),
),
)
}
const materializers = Effect.fn("Reference.materializers")(function* (
cache: RepositoryCache.Interface,
references: Resolved[],
) {
return yield* Effect.forEach(
uniqueGitReferences(references),
Effect.fnUntraced(function* (reference) {
return { path: reference.path, run: yield* Effect.cached(materializeReference(cache, reference)) }
}),
{ concurrency: "unbounded" },
)
})
function materializeAll(input: { flags: RuntimeFlags.Info; materializers: Materializer[] }) {
if (!input.flags.experimentalReferences) return Effect.void
return Effect.forEach(
input.materializers,
Effect.fnUntraced(function* (item) {
yield* item.run
}),
{ concurrency: 4, discard: true },
)
}
function materializeByPath(materializers: Materializer[], target: string) {
return materializers.find((item) => containsReferencePath(item.path, target))?.run ?? Effect.void
}
function containsGitReferencePath(references: Resolved[], target: string) {
return references.some((reference) => reference.kind === "git" && containsReferencePath(reference.path, target))
}
export function resolve(input: {
name: string
reference: ConfigReference.NormalizedEntry
directory: string
worktree: string
}): Resolved {
if (input.reference.kind === "invalid") {
return { name: input.name, kind: "invalid", message: input.reference.message }
}
if (input.reference.kind === "local") {
return { name: input.name, kind: "local", path: referencePath({ ...input, value: input.reference.path }) }
}
return resolveGit({ name: input.name, repository: input.reference.repository, branch: input.reference.branch })
}
export function resolveAll(input: { references: ConfigReference.NormalizedInfo; directory: string; worktree: string }) {
const seen = new Map<string, { name: string; branch?: string }>()
return Object.entries(input.references).map(([name, reference]) => {
const resolved = resolve({ name, reference, directory: input.directory, worktree: input.worktree })
if (resolved.kind !== "git") return resolved
const existing = seen.get(resolved.path)
if (!existing) {
seen.set(resolved.path, { name, branch: resolved.branch })
return resolved
}
if (existing.branch === resolved.branch) return resolved
return {
name,
kind: "invalid" as const,
repository: resolved.repository,
message: `Reference conflicts with @${existing.name}: both use ${resolved.path}, but @${existing.name} requests ${branchLabel(existing.branch)} and @${name} requests ${branchLabel(resolved.branch)}`,
}
})
}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const config = yield* Config.Service
const cache = yield* RepositoryCache.Service
const scope = yield* Scope.Scope
const flags = yield* RuntimeFlags.Service
const state = yield* InstanceState.make<State>(
Effect.fn("Reference.state")(function* (ctx) {
const cfg = yield* config.get()
const references = resolveAll({
references: ConfigReference.normalize(cfg.reference ?? {}),
directory: ctx.directory,
worktree: ctx.worktree,
})
const materializeByPath = yield* materializers(cache, references)
const materializeAllCached = yield* Effect.cached(materializeAll({ flags, materializers: materializeByPath }))
return { references, materializeAll: materializeAllCached, materializeByPath }
}),
)
return Service.of({
init: Effect.fn("Reference.init")(function* () {
if (!flags.experimentalReferences) return
yield* InstanceState.useEffect(state, (s) => s.materializeAll).pipe(Effect.forkIn(scope), Effect.asVoid)
}),
list: Effect.fn("Reference.list")(function* () {
return yield* InstanceState.use(state, (s) => s.references)
}),
get: Effect.fn("Reference.get")(function* (name: string) {
return yield* InstanceState.use(state, (s) => s.references.find((reference) => reference.name === name))
}),
ensure: Effect.fn("Reference.ensure")(function* (target?: string) {
if (!flags.experimentalReferences) return
const full = normalizedTarget(target)
if (!full) return yield* InstanceState.useEffect(state, (s) => s.materializeAll)
return yield* InstanceState.useEffect(state, (s) => materializeByPath(s.materializeByPath, full))
}),
contains: Effect.fn("Reference.contains")(function* (target?: string) {
if (!flags.experimentalReferences) return false
const full = normalizedTarget(target)
if (!full) return false
return yield* InstanceState.use(state, (s) => containsGitReferencePath(s.references, full))
}),
})
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(Config.defaultLayer),
Layer.provide(RepositoryCache.defaultLayer),
Layer.provide(RuntimeFlags.defaultLayer),
)
export * as Reference from "./reference"
@@ -1,320 +0,0 @@
import path from "path"
import { Context, Effect, Layer, Schema } from "effect"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Flock } from "@opencode-ai/core/util/flock"
import { Git } from "@/git"
import {
repositoryCachePath,
sameRepositoryReference,
parseRepositoryReference,
parseRemoteRepositoryReference,
validateRepositoryBranch,
InvalidRepositoryBranchError,
InvalidRepositoryReferenceError,
UnsupportedLocalRepositoryError,
type RemoteReference,
} from "@/util/repository"
export type Result = {
repository: string
host: string
remote: string
localPath: string
status: "cached" | "cloned" | "refreshed"
head?: string
branch?: string
}
export type EnsureInput = {
reference: RemoteReference
refresh?: boolean
branch?: string
}
export class InvalidRepositoryError extends Schema.TaggedErrorClass<InvalidRepositoryError>()(
"RepositoryCacheInvalidRepositoryError",
{
repository: Schema.String,
message: Schema.String,
},
) {}
export class InvalidBranchError extends Schema.TaggedErrorClass<InvalidBranchError>()(
"RepositoryCacheInvalidBranchError",
{
branch: Schema.String,
message: Schema.String,
},
) {}
export class CloneFailedError extends Schema.TaggedErrorClass<CloneFailedError>()("RepositoryCacheCloneFailedError", {
repository: Schema.String,
message: Schema.String,
}) {}
export class FetchFailedError extends Schema.TaggedErrorClass<FetchFailedError>()("RepositoryCacheFetchFailedError", {
repository: Schema.String,
message: Schema.String,
}) {}
export class CheckoutFailedError extends Schema.TaggedErrorClass<CheckoutFailedError>()(
"RepositoryCacheCheckoutFailedError",
{
repository: Schema.String,
branch: Schema.String,
message: Schema.String,
},
) {}
export class ResetFailedError extends Schema.TaggedErrorClass<ResetFailedError>()("RepositoryCacheResetFailedError", {
repository: Schema.String,
message: Schema.String,
}) {}
export class LockFailedError extends Schema.TaggedErrorClass<LockFailedError>()("RepositoryCacheLockFailedError", {
localPath: Schema.String,
message: Schema.String,
}) {}
export class CacheOperationError extends Schema.TaggedErrorClass<CacheOperationError>()(
"RepositoryCacheOperationError",
{
operation: Schema.String,
path: Schema.String,
message: Schema.String,
},
) {}
export type Error =
| InvalidRepositoryError
| InvalidBranchError
| CloneFailedError
| FetchFailedError
| CheckoutFailedError
| ResetFailedError
| LockFailedError
| CacheOperationError
export interface Interface {
ensure: (input: EnsureInput) => Effect.Effect<Result, Error>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/RepositoryCache") {}
function statusForRepository(input: { reuse: boolean; refresh?: boolean; branchMatches?: boolean }) {
if (!input.reuse) return "cloned" as const
if (input.branchMatches === false) return "refreshed" as const
if (input.refresh) return "refreshed" as const
return "cached" as const
}
function resetTarget(input: {
requestedBranch?: string
remoteHead: { code: number; stdout: string }
branch: { code: number; stdout: string }
}) {
if (input.requestedBranch) return `origin/${input.requestedBranch}`
if (input.remoteHead.code === 0 && input.remoteHead.stdout) {
return input.remoteHead.stdout.replace(/^refs\/remotes\//, "")
}
if (input.branch.code === 0 && input.branch.stdout) {
return `origin/${input.branch.stdout}`
}
return "HEAD"
}
function errorMessage(error: unknown) {
return error instanceof globalThis.Error ? error.message : String(error)
}
export function isError(error: unknown): error is Error {
return (
error instanceof InvalidRepositoryError ||
error instanceof InvalidBranchError ||
error instanceof CloneFailedError ||
error instanceof FetchFailedError ||
error instanceof CheckoutFailedError ||
error instanceof ResetFailedError ||
error instanceof LockFailedError ||
error instanceof CacheOperationError
)
}
export const parseRemoteReference = Effect.fn("RepositoryCache.parseRemoteReference")(function* (repository: string) {
try {
return parseRemoteRepositoryReference(repository)
} catch (error) {
if (error instanceof InvalidRepositoryReferenceError || error instanceof UnsupportedLocalRepositoryError) {
return yield* new InvalidRepositoryError({ repository: error.repository, message: error.message })
}
return yield* new InvalidRepositoryError({
repository,
message: errorMessage(error),
})
}
})
export const validateBranch = Effect.fn("RepositoryCache.validateBranch")(function* (branch: string) {
try {
validateRepositoryBranch(branch)
} catch (error) {
if (error instanceof InvalidRepositoryBranchError) {
return yield* new InvalidBranchError({ branch: error.branch, message: error.message })
}
return yield* new InvalidBranchError({ branch, message: errorMessage(error) })
}
})
const ensureWithServices = Effect.fn("RepositoryCache.ensureWithServices")(function* (
input: EnsureInput,
services: {
fs: FSUtil.Interface
git: Git.Interface
},
) {
if (input.branch) yield* validateBranch(input.branch)
const repository = input.reference.label
const remote = input.reference.remote
const localPath = repositoryCachePath(input.reference)
const cloneTarget = parseRepositoryReference(remote) ?? input.reference
return yield* Effect.acquireUseRelease(
Effect.promise((signal) => Flock.acquire(`repo-clone:${localPath}`, { signal })).pipe(
Effect.catch((error: unknown) =>
Effect.fail(new LockFailedError({ localPath, message: errorMessage(error) || `Failed to lock ${localPath}` })),
),
),
() =>
Effect.gen(function* () {
yield* services.fs.ensureDir(path.dirname(localPath)).pipe(
Effect.catch((error: unknown) =>
Effect.fail(
new CacheOperationError({
operation: "ensure cache directory",
path: localPath,
message: errorMessage(error),
}),
),
),
)
const exists = yield* services.fs.existsSafe(localPath)
const hasGitDir = yield* services.fs.existsSafe(path.join(localPath, ".git"))
const origin = hasGitDir
? yield* services.git.run(["config", "--get", "remote.origin.url"], { cwd: localPath })
: undefined
const originReference = origin?.exitCode === 0 ? parseRepositoryReference(origin.text().trim()) : undefined
const reuse = hasGitDir && Boolean(originReference && sameRepositoryReference(originReference, cloneTarget))
if (exists && !reuse) {
yield* services.fs.remove(localPath, { recursive: true }).pipe(
Effect.catch((error: unknown) =>
Effect.fail(
new CacheOperationError({
operation: "remove stale cache",
path: localPath,
message: errorMessage(error),
}),
),
),
)
}
const currentBranch = hasGitDir ? yield* services.git.branch(localPath) : undefined
const status = statusForRepository({
reuse,
refresh: input.refresh,
branchMatches: input.branch ? currentBranch === input.branch : undefined,
})
if (status === "cloned") {
const clone = yield* services.git.run(
["clone", "--depth", "100", ...(input.branch ? ["--branch", input.branch] : []), "--", remote, localPath],
{ cwd: path.dirname(localPath) },
)
if (clone.exitCode !== 0) {
return yield* new CloneFailedError({
repository,
message: clone.stderr.toString().trim() || clone.text().trim() || `Failed to clone ${repository}`,
})
}
}
if (status === "refreshed") {
const fetch = yield* services.git.run(["fetch", "--all", "--prune"], { cwd: localPath })
if (fetch.exitCode !== 0) {
return yield* new FetchFailedError({
repository,
message: fetch.stderr.toString().trim() || fetch.text().trim() || `Failed to refresh ${repository}`,
})
}
if (input.branch) {
const checkout = yield* services.git.run(["checkout", "-B", input.branch, `origin/${input.branch}`], {
cwd: localPath,
})
if (checkout.exitCode !== 0) {
return yield* new CheckoutFailedError({
repository,
branch: input.branch,
message:
checkout.stderr.toString().trim() || checkout.text().trim() || `Failed to checkout ${input.branch}`,
})
}
}
const remoteHead = yield* services.git.run(["symbolic-ref", "refs/remotes/origin/HEAD"], { cwd: localPath })
const branch = yield* services.git.run(["symbolic-ref", "--quiet", "--short", "HEAD"], { cwd: localPath })
const target = resetTarget({
requestedBranch: input.branch,
remoteHead: { code: remoteHead.exitCode, stdout: remoteHead.text().trim() },
branch: { code: branch.exitCode, stdout: branch.text().trim() },
})
const reset = yield* services.git.run(["reset", "--hard", target], { cwd: localPath })
if (reset.exitCode !== 0) {
return yield* new ResetFailedError({
repository,
message: reset.stderr.toString().trim() || reset.text().trim() || `Failed to reset ${repository}`,
})
}
}
const head = yield* services.git.run(["rev-parse", "HEAD"], { cwd: localPath })
const branch = yield* services.git.branch(localPath)
const headText = head.exitCode === 0 ? head.text().trim() : undefined
return {
repository,
host: input.reference.host,
remote,
localPath,
status,
head: headText,
branch,
} satisfies Result
}),
(lock) => Effect.promise(() => lock.release()).pipe(Effect.ignore),
)
})
export const layer: Layer.Layer<Service, never, FSUtil.Service | Git.Service> = Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* FSUtil.Service
const git = yield* Git.Service
return Service.of({
ensure: Effect.fn("RepositoryCache.ensure")(function* (input) {
return yield* ensureWithServices(input, { fs, git })
}),
})
}),
)
export const defaultLayer: Layer.Layer<Service> = layer.pipe(
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Git.defaultLayer),
)
export * as RepositoryCache from "./repository-cache"
+1 -1
View File
@@ -54,7 +54,7 @@ import { SessionEvent } from "@opencode-ai/core/session/event"
import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { AgentAttachment, FileAttachment, ReferenceAttachment, Source } from "@opencode-ai/core/session/prompt"
import { Reference } from "@/reference/reference"
import { Reference } from "@/reference"
import * as DateTime from "effect/DateTime"
import { eq } from "drizzle-orm"
import { SessionTable } from "@opencode-ai/core/session/sql"
@@ -1,7 +1,7 @@
import { Option, Schema } from "effect"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { MessageV2 } from "../message-v2"
import { Reference } from "@/reference/reference"
import { Reference } from "@/reference"
const Source = Schema.Struct({
value: Schema.String,
+1 -1
View File
@@ -7,7 +7,7 @@ import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { assertExternalDirectoryEffect } from "./external-directory"
import DESCRIPTION from "./glob.txt"
import * as Tool from "./tool"
import { Reference } from "@/reference/reference"
import { Reference } from "@/reference"
export const Parameters = Schema.Struct({
pattern: Schema.String.annotate({ description: "The glob pattern to match files against" }),
+1 -1
View File
@@ -7,7 +7,7 @@ import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { assertExternalDirectoryEffect } from "./external-directory"
import DESCRIPTION from "./grep.txt"
import * as Tool from "./tool"
import { Reference } from "@/reference/reference"
import { Reference } from "@/reference"
const MAX_LINE_LENGTH = 2000
+1 -1
View File
@@ -9,7 +9,7 @@ import { InstanceState } from "@/effect/instance-state"
import { assertExternalDirectoryEffect } from "./external-directory"
import { Instruction } from "../session/instruction"
import { isPdfAttachment, sniffAttachmentMime } from "@/util/media"
import { Reference } from "@/reference/reference"
import { Reference } from "@/reference"
const DEFAULT_READ_LIMIT = 2000
const MAX_LINE_LENGTH = 2000
+1 -1
View File
@@ -47,7 +47,7 @@ import { EventV2Bridge } from "@/event-v2-bridge"
import { Agent } from "../agent/agent"
import { Skill } from "../skill"
import { Permission } from "@/permission"
import { Reference } from "@/reference/reference"
import { Reference } from "@/reference"
import { BackgroundJob } from "@/background/job"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { ProviderV2 } from "@opencode-ai/core/provider"
@@ -0,0 +1,20 @@
import { Effect, Semaphore } from "effect"
const lock = Semaphore.makeUnsafe(1)
export const githubBase = <A, E, R>(url: string, self: Effect.Effect<A, E, R>) =>
lock.withPermit(
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 === undefined) delete process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL
else process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL = previous
}),
),
)
+151 -125
View File
@@ -5,24 +5,20 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Global } from "@opencode-ai/core/global"
import { Config } from "../../src/config/config"
import { ConfigReference } from "../../src/config/reference"
import { ConfigReference } from "@opencode-ai/core/config/reference"
import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { Git } from "../../src/git"
import { Reference } from "../../src/reference/reference"
import { RepositoryCache } from "../../src/reference/repository-cache"
import { Reference } from "../../src/reference"
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
import { disposeAllInstances, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
import { githubBase } from "../fixture/repository"
import { testEffect } from "../lib/effect"
afterEach(async () => {
await disposeAllInstances()
})
const referenceLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
Reference.layer.pipe(
Layer.provide(Config.defaultLayer),
Layer.provide(RepositoryCache.defaultLayer),
Layer.provide(RuntimeFlags.layer(flags)),
)
const referenceLayer = (_flags: Partial<RuntimeFlags.Info> = {}) => Reference.defaultLayer
const it = testEffect(
Layer.mergeAll(FSUtil.defaultLayer, CrossSpawnSpawner.defaultLayer, Git.defaultLayer, referenceLayer()),
@@ -36,18 +32,25 @@ const references = testEffect(
),
)
const githubBase = <A, E, R>(url: string, self: Effect.Effect<A, E, R>) =>
const withReferences = <A, E, R>(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,
Effect.sync(() => process.env.OPENCODE_EXPERIMENTAL_REFERENCES),
() => Effect.sync(() => void (process.env.OPENCODE_EXPERIMENTAL_REFERENCES = "true")).pipe(Effect.andThen(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
if (previous === undefined) delete process.env.OPENCODE_EXPERIMENTAL_REFERENCES
else process.env.OPENCODE_EXPERIMENTAL_REFERENCES = previous
}),
)
const withConfigContent = <A, E, R>(content: string, self: Effect.Effect<A, E, R>) =>
Effect.acquireUseRelease(
Effect.sync(() => process.env.OPENCODE_CONFIG_CONTENT),
() => Effect.sync(() => void (process.env.OPENCODE_CONFIG_CONTENT = content)).pipe(Effect.andThen(self)),
(previous) =>
Effect.sync(() => {
if (previous === undefined) delete process.env.OPENCODE_CONFIG_CONTENT
else process.env.OPENCODE_CONFIG_CONTENT = previous
}),
)
@@ -129,28 +132,47 @@ describe("reference", () => {
)
it.live("keeps invalid repository references visible without materializing", () =>
provideTmpdirInstance(
(_dir) =>
Effect.gen(function* () {
const reference = yield* Reference.Service
const references = yield* reference.list()
const invalid = yield* reference.get("bad")
withReferences(
provideTmpdirInstance(
(_dir) =>
Effect.gen(function* () {
const reference = yield* Reference.Service
const references = yield* reference.list()
const invalid = yield* reference.get("bad")
expect(references.map((item) => item.name)).toEqual(["bad"])
expect(invalid).toMatchObject({
name: "bad",
kind: "invalid",
repository: "not-a-repo",
})
if (invalid?.kind === "invalid") expect(invalid.message).toContain("Repository must be a git URL")
}),
{
config: {
reference: {
bad: "not-a-repo",
expect(references.map((item) => item.name)).toEqual(["bad"])
expect(invalid).toMatchObject({
name: "bad",
kind: "invalid",
repository: "not-a-repo",
})
if (invalid?.kind === "invalid") expect(invalid.message).toContain("Repository must be a git URL")
}),
{
config: {
reference: {
bad: "not-a-repo",
},
},
},
},
),
),
)
references.live("reads references from legacy config content", () =>
withReferences(
withConfigContent(
JSON.stringify({ reference: { docs: { path: "./docs" } } }),
provideTmpdirInstance((dir) =>
Effect.gen(function* () {
expect(yield* (yield* Reference.Service).get("docs")).toMatchObject({
name: "docs",
kind: "local",
path: path.join(dir, "docs"),
})
}),
),
),
),
)
@@ -198,113 +220,117 @@ describe("reference", () => {
)
references.live("materializes configured git references during init", () =>
provideTmpdirInstance(
(_dir) =>
Effect.gen(function* () {
const fs = yield* FSUtil.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))
withReferences(
provideTmpdirInstance(
(_dir) =>
Effect.gen(function* () {
const fs = yield* FSUtil.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")
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])
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")
}),
)
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")
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",
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",
},
},
},
},
),
),
)
references.live("refreshes configured git references on new instance init", () =>
Effect.gen(function* () {
const fs = yield* FSUtil.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))
withReferences(
Effect.gen(function* () {
const fs = yield* FSUtil.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")
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* 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",
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}`])
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",
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",
},
},
},
},
),
)
}),
),
)
}),
),
)
})
+127 -109
View File
@@ -50,8 +50,8 @@ import * as Log from "@opencode-ai/core/util/log"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { Format } from "../../src/format"
import { Reference } from "../../src/reference/reference"
import { RepositoryCache } from "../../src/reference/repository-cache"
import { Reference } from "../../src/reference"
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
import { TestInstance } from "../fixture/fixture"
import { awaitWithTimeout, pollWithTimeout, testEffect } from "../lib/effect"
import { reply, TestLLMServer } from "../lib/llm-server"
@@ -92,6 +92,18 @@ function withSh<A, E, R>(fx: () => Effect.Effect<A, E, R>) {
)
}
function withReferences<A, E, R>(fx: () => Effect.Effect<A, E, R>) {
return Effect.acquireUseRelease(
Effect.sync(() => process.env.OPENCODE_EXPERIMENTAL_REFERENCES),
() => Effect.sync(() => void (process.env.OPENCODE_EXPERIMENTAL_REFERENCES = "true")).pipe(Effect.andThen(fx())),
(previous) =>
Effect.sync(() => {
if (previous === undefined) delete process.env.OPENCODE_EXPERIMENTAL_REFERENCES
else process.env.OPENCODE_EXPERIMENTAL_REFERENCES = previous
}),
)
}
function toolPart(parts: SessionV1.Part[]) {
return parts.find((part): part is SessionV1.ToolPart => part.type === "tool")
}
@@ -1933,48 +1945,50 @@ noLLMServer.instance(
noLLMServer.instance(
"resolves configured reference mentions before workspace paths and agents",
() =>
Effect.gen(function* () {
const { directory: dir } = yield* TestInstance
const docs = path.join(dir, "external-docs")
yield* ensureDir(path.join(docs, "guide"))
yield* ensureDir(path.join(dir, "docs"))
yield* writeText(path.join(docs, "README.md"), "reference readme")
yield* writeText(path.join(docs, "guide", "intro.md"), "reference intro")
yield* writeText(path.join(dir, "docs", "README.md"), "workspace readme")
withReferences(() =>
Effect.gen(function* () {
const { directory: dir } = yield* TestInstance
const docs = path.join(dir, "external-docs")
yield* ensureDir(path.join(docs, "guide"))
yield* ensureDir(path.join(dir, "docs"))
yield* writeText(path.join(docs, "README.md"), "reference readme")
yield* writeText(path.join(docs, "guide", "intro.md"), "reference intro")
yield* writeText(path.join(dir, "docs", "README.md"), "workspace readme")
const prompt = yield* SessionPrompt.Service
const parts = yield* prompt.resolvePromptParts(
"Use @docs and @docs/README.md and @docs/guide and @docs/missing.md and @docs/README.md and @build",
)
const references = parts.filter(
(part): part is SessionV1.TextPartInput =>
part.type === "text" && part.synthetic === true && part.text.startsWith("Referenced configured reference "),
)
const files = parts.filter((part): part is SessionV1.FilePartInput => part.type === "file")
const agents = parts.filter((part): part is SessionV1.AgentPartInput => part.type === "agent")
const bare = references.find((part) => part.text.includes("@docs."))
const missing = references.find((part) => part.text.includes("@docs/missing.md"))
const guide = files.find((part) => part.filename === "docs/guide")
const prompt = yield* SessionPrompt.Service
const parts = yield* prompt.resolvePromptParts(
"Use @docs and @docs/README.md and @docs/guide and @docs/missing.md and @docs/README.md and @build",
)
const references = parts.filter(
(part): part is SessionV1.TextPartInput =>
part.type === "text" && part.synthetic === true && part.text.startsWith("Referenced configured reference "),
)
const files = parts.filter((part): part is SessionV1.FilePartInput => part.type === "file")
const agents = parts.filter((part): part is SessionV1.AgentPartInput => part.type === "agent")
const bare = references.find((part) => part.text.includes("@docs."))
const missing = references.find((part) => part.text.includes("@docs/missing.md"))
const guide = files.find((part) => part.filename === "docs/guide")
expect(references.length).toBe(2)
expect(bare?.metadata?.reference).toMatchObject({
name: "docs",
kind: "local",
path: docs,
})
expect(missing?.text).toContain("Path does not exist inside configured reference @docs")
expect(missing?.metadata?.reference).toMatchObject({
target: "missing.md",
targetPath: path.join(docs, "missing.md"),
})
expect(references.length).toBe(2)
expect(bare?.metadata?.reference).toMatchObject({
name: "docs",
kind: "local",
path: docs,
})
expect(missing?.text).toContain("Path does not exist inside configured reference @docs")
expect(missing?.metadata?.reference).toMatchObject({
target: "missing.md",
targetPath: path.join(docs, "missing.md"),
})
expect(files.length).toBe(2)
expect(files.map((file) => fileURLToPath(file.url)).sort()).toEqual(
[path.join(docs, "README.md"), path.join(docs, "guide")].sort(),
)
expect(guide?.mime).toBe("application/x-directory")
expect(agents.map((agent) => agent.name)).toEqual(["build"])
}),
expect(files.length).toBe(2)
expect(files.map((file) => fileURLToPath(file.url)).sort()).toEqual(
[path.join(docs, "README.md"), path.join(docs, "guide")].sort(),
)
expect(guide?.mime).toBe("application/x-directory")
expect(agents.map((agent) => agent.name)).toEqual(["build"])
}),
),
{
config: {
...cfg,
@@ -1988,32 +2002,34 @@ noLLMServer.instance(
noLLMServer.instance(
"injects metadata for bare configured reference mentions",
() =>
Effect.gen(function* () {
const { directory: dir } = yield* TestInstance
const docs = path.join(dir, "external-docs")
yield* ensureDir(docs)
withReferences(() =>
Effect.gen(function* () {
const { directory: dir } = yield* TestInstance
const docs = path.join(dir, "external-docs")
yield* ensureDir(docs)
const prompt = yield* SessionPrompt.Service
const sessions = yield* Session.Service
const session = yield* sessions.create({})
const message = yield* prompt.prompt({
sessionID: session.id,
noReply: true,
parts: yield* prompt.resolvePromptParts("Use @docs for context"),
})
const prompt = yield* SessionPrompt.Service
const sessions = yield* Session.Service
const session = yield* sessions.create({})
const message = yield* prompt.prompt({
sessionID: session.id,
noReply: true,
parts: yield* prompt.resolvePromptParts("Use @docs for context"),
})
const stored = yield* MessageV2.get({ sessionID: session.id, messageID: message.info.id })
const synthetic = stored.parts.filter(
(part): part is SessionV1.TextPart => part.type === "text" && part.synthetic === true,
)
const reference = synthetic.find((part) => part.text.startsWith("Referenced configured reference @docs."))
const stored = yield* MessageV2.get({ sessionID: session.id, messageID: message.info.id })
const synthetic = stored.parts.filter(
(part): part is SessionV1.TextPart => part.type === "text" && part.synthetic === true,
)
const reference = synthetic.find((part) => part.text.startsWith("Referenced configured reference @docs."))
expect(reference?.metadata?.reference).toMatchObject({ name: "docs", kind: "local", path: docs })
expect(synthetic.some((part) => part.text.includes(`Reference root: ${docs}`))).toBe(true)
expect(synthetic.some((part) => part.text.includes("Inspect the configured reference"))).toBe(true)
expect(reference?.metadata?.reference).toMatchObject({ name: "docs", kind: "local", path: docs })
expect(synthetic.some((part) => part.text.includes(`Reference root: ${docs}`))).toBe(true)
expect(synthetic.some((part) => part.text.includes("Inspect the configured reference"))).toBe(true)
yield* sessions.remove(session.id)
}),
yield* sessions.remove(session.id)
}),
),
{
config: {
...cfg,
@@ -2027,58 +2043,60 @@ noLLMServer.instance(
noLLMServer.instance(
"injects metadata for configured reference file attachments",
() =>
Effect.gen(function* () {
const { directory: dir } = yield* TestInstance
const docs = path.join(dir, "external-docs")
const readme = path.join(docs, "README.md")
yield* ensureDir(docs)
yield* writeText(readme, "reference readme")
withReferences(() =>
Effect.gen(function* () {
const { directory: dir } = yield* TestInstance
const docs = path.join(dir, "external-docs")
const readme = path.join(docs, "README.md")
yield* ensureDir(docs)
yield* writeText(readme, "reference readme")
const prompt = yield* SessionPrompt.Service
const sessions = yield* Session.Service
const session = yield* sessions.create({})
const message = yield* prompt.prompt({
sessionID: session.id,
agent: "build",
noReply: true,
parts: [
{ type: "text", text: "Read @docs/README.md" },
{
type: "file",
mime: "text/plain",
filename: "docs/README.md",
url: pathToFileURL(readme).href,
source: {
const prompt = yield* SessionPrompt.Service
const sessions = yield* Session.Service
const session = yield* sessions.create({})
const message = yield* prompt.prompt({
sessionID: session.id,
agent: "build",
noReply: true,
parts: [
{ type: "text", text: "Read @docs/README.md" },
{
type: "file",
path: "docs/README.md",
text: { value: "@docs/README.md", start: 5, end: 20 },
mime: "text/plain",
filename: "docs/README.md",
url: pathToFileURL(readme).href,
source: {
type: "file",
path: "docs/README.md",
text: { value: "@docs/README.md", start: 5, end: 20 },
},
},
},
],
})
],
})
const stored = yield* MessageV2.get({ sessionID: session.id, messageID: message.info.id })
const synthetic = stored.parts.filter(
(part): part is SessionV1.TextPart => part.type === "text" && part.synthetic === true,
)
const reference = synthetic.find((part) =>
part.text.startsWith("Referenced configured reference @docs/README.md."),
)
const stored = yield* MessageV2.get({ sessionID: session.id, messageID: message.info.id })
const synthetic = stored.parts.filter(
(part): part is SessionV1.TextPart => part.type === "text" && part.synthetic === true,
)
const reference = synthetic.find((part) =>
part.text.startsWith("Referenced configured reference @docs/README.md."),
)
expect(reference?.metadata?.reference).toMatchObject({
name: "docs",
kind: "local",
path: docs,
target: "README.md",
targetPath: readme,
source: { value: "@docs/README.md", start: 5, end: 20 },
})
expect(synthetic.findIndex((part) => part === reference)).toBeLessThan(
synthetic.findIndex((part) => part.text.startsWith("Called the Read tool with the following input:")),
)
expect(reference?.metadata?.reference).toMatchObject({
name: "docs",
kind: "local",
path: docs,
target: "README.md",
targetPath: readme,
source: { value: "@docs/README.md", start: 5, end: 20 },
})
expect(synthetic.findIndex((part) => part === reference)).toBeLessThan(
synthetic.findIndex((part) => part.text.startsWith("Called the Read tool with the following input:")),
)
yield* sessions.remove(session.id)
}),
yield* sessions.remove(session.id)
}),
),
{
config: {
...cfg,
@@ -60,8 +60,8 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { Format } from "../../src/format"
import { Reference } from "../../src/reference/reference"
import { RepositoryCache } from "../../src/reference/repository-cache"
import { Reference } from "../../src/reference"
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
import { RuntimeFlags } from "@/effect/runtime-flags"
void Log.init({ print: false })
+44 -49
View File
@@ -12,20 +12,16 @@ import { Truncate } from "@/tool/truncate"
import { Agent } from "../../src/agent/agent"
import { TestInstance, tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { Reference } from "@/reference/reference"
import { RepositoryCache } from "@/reference/repository-cache"
import { githubBase } from "../fixture/repository"
import { Reference } from "@/reference"
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
import { Config } from "@/config/config"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { Git } from "@/git"
import { Permission } from "../../src/permission"
import type * as Tool from "../../src/tool/tool"
const referenceLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
Reference.layer.pipe(
Layer.provide(Config.defaultLayer),
Layer.provide(RepositoryCache.defaultLayer),
Layer.provide(RuntimeFlags.layer(flags)),
)
const referenceLayer = (_flags: Partial<RuntimeFlags.Info> = {}) => Reference.defaultLayer
const toolLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
Layer.mergeAll(
@@ -66,21 +62,6 @@ const asks = () => {
}
}
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("GlobToolTest.git")(function* (cwd: string, args: string[]) {
return yield* Effect.promise(async () => {
const proc = Bun.spawn(["git", ...args], {
@@ -146,35 +127,37 @@ describe("tool.glob", () => {
references.instance(
"does not ask for external_directory permission inside configured git references",
() =>
Effect.gen(function* () {
yield* TestInstance
const fs = yield* FSUtil.Service
const cache = path.join(Global.Path.repos, "github.com", "opencode-glob-reference", "repo")
yield* fs.remove(cache, { recursive: true }).pipe(Effect.ignore)
yield* Effect.addFinalizer(() => fs.remove(cache, { recursive: true }).pipe(Effect.ignore))
withReferences(
Effect.gen(function* () {
yield* TestInstance
const fs = yield* FSUtil.Service
const cache = path.join(Global.Path.repos, "github.com", "opencode-glob-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-glob-reference")
const remoteRepo = path.join(remoteDir, "repo.git")
yield* fs.writeWithDirs(path.join(source, "src", "index.ts"), "export const value = 1\n")
yield* git(source, ["add", "."])
yield* git(source, ["commit", "-m", "add source"])
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
const source = yield* tmpdirScoped({ git: true })
const remoteRoot = yield* tmpdirScoped()
const remoteDir = path.join(remoteRoot, "opencode-glob-reference")
const remoteRepo = path.join(remoteDir, "repo.git")
yield* fs.writeWithDirs(path.join(source, "src", "index.ts"), "export const value = 1\n")
yield* git(source, ["add", "."])
yield* git(source, ["commit", "-m", "add source"])
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
const { items, next } = asks()
const info = yield* GlobTool
const glob = yield* info.init()
const result = yield* githubBase(
`file://${remoteRoot}/`,
glob.execute({ pattern: "*.ts", path: path.join(cache, "src") }, next),
)
const { items, next } = asks()
const info = yield* GlobTool
const glob = yield* info.init()
const result = yield* githubBase(
`file://${remoteRoot}/`,
glob.execute({ pattern: "*.ts", path: path.join(cache, "src") }, next),
)
expect(result.metadata.count).toBe(1)
expect(result.output).toContain(path.join(cache, "src", "index.ts"))
expect(items.find((item) => item.permission === "external_directory")).toBeUndefined()
}),
expect(result.metadata.count).toBe(1)
expect(result.output).toContain(path.join(cache, "src", "index.ts"))
expect(items.find((item) => item.permission === "external_directory")).toBeUndefined()
}),
),
{
config: {
reference: {
@@ -184,3 +167,15 @@ describe("tool.glob", () => {
},
)
})
function withReferences<A, E, R>(body: Effect.Effect<A, E, R>) {
return Effect.acquireUseRelease(
Effect.sync(() => process.env.OPENCODE_EXPERIMENTAL_REFERENCES),
() => Effect.sync(() => void (process.env.OPENCODE_EXPERIMENTAL_REFERENCES = "true")).pipe(Effect.andThen(body)),
(previous) =>
Effect.sync(() => {
if (previous === undefined) delete process.env.OPENCODE_EXPERIMENTAL_REFERENCES
else process.env.OPENCODE_EXPERIMENTAL_REFERENCES = previous
}),
)
}
+51 -56
View File
@@ -14,8 +14,9 @@ import { Agent } from "../../src/agent/agent"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { testEffect } from "../lib/effect"
import { Reference } from "@/reference/reference"
import { RepositoryCache } from "@/reference/repository-cache"
import { githubBase } from "../fixture/repository"
import { Reference } from "@/reference"
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
import { Permission } from "../../src/permission"
import type * as Tool from "../../src/tool/tool"
import { Config } from "@/config/config"
@@ -23,12 +24,7 @@ import { RuntimeFlags } from "@/effect/runtime-flags"
import { Git } from "@/git"
import { Filesystem } from "@/util/filesystem"
const referenceLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
Reference.layer.pipe(
Layer.provide(Config.defaultLayer),
Layer.provide(RepositoryCache.defaultLayer),
Layer.provide(RuntimeFlags.layer(flags)),
)
const referenceLayer = (_flags: Partial<RuntimeFlags.Info> = {}) => Reference.defaultLayer
const toolLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
Layer.mergeAll(
@@ -59,21 +55,6 @@ const ctx = {
const root = path.join(__dirname, "../..")
const full = (p: string) => (process.platform === "win32" ? Filesystem.normalizePath(p) : p)
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("GrepToolTest.git")(function* (cwd: string, args: string[]) {
return yield* Effect.promise(async () => {
const proc = Bun.spawn(["git", ...args], {
@@ -218,43 +199,45 @@ describe("tool.grep", () => {
references.instance(
"does not ask for external_directory permission inside configured git references",
() =>
Effect.gen(function* () {
yield* TestInstance
const appfs = yield* FSUtil.Service
const cache = path.join(Global.Path.repos, "github.com", "opencode-grep-reference", "repo")
yield* appfs.remove(cache, { recursive: true }).pipe(Effect.ignore)
yield* Effect.addFinalizer(() => appfs.remove(cache, { recursive: true }).pipe(Effect.ignore))
withReferences(
Effect.gen(function* () {
yield* TestInstance
const appfs = yield* FSUtil.Service
const cache = path.join(Global.Path.repos, "github.com", "opencode-grep-reference", "repo")
yield* appfs.remove(cache, { recursive: true }).pipe(Effect.ignore)
yield* Effect.addFinalizer(() => appfs.remove(cache, { recursive: true }).pipe(Effect.ignore))
const source = yield* tmpdirScoped({ git: true })
const remoteRoot = yield* tmpdirScoped()
const remoteDir = path.join(remoteRoot, "opencode-grep-reference")
const remoteRepo = path.join(remoteDir, "repo.git")
yield* appfs.writeWithDirs(path.join(source, "src", "notes.md"), "needle\n")
yield* git(source, ["add", "."])
yield* git(source, ["commit", "-m", "add notes"])
yield* appfs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
const source = yield* tmpdirScoped({ git: true })
const remoteRoot = yield* tmpdirScoped()
const remoteDir = path.join(remoteRoot, "opencode-grep-reference")
const remoteRepo = path.join(remoteDir, "repo.git")
yield* appfs.writeWithDirs(path.join(source, "src", "notes.md"), "needle\n")
yield* git(source, ["add", "."])
yield* git(source, ["commit", "-m", "add notes"])
yield* appfs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
const requests: Array<Omit<PermissionV1.Request, "id" | "sessionID" | "tool">> = []
const next: Tool.Context = {
...ctx,
ask: (req) =>
Effect.sync(() => {
requests.push(req)
}),
}
const requests: Array<Omit<PermissionV1.Request, "id" | "sessionID" | "tool">> = []
const next: Tool.Context = {
...ctx,
ask: (req) =>
Effect.sync(() => {
requests.push(req)
}),
}
const info = yield* GrepTool
const grep = yield* info.init()
const result = yield* githubBase(
`file://${remoteRoot}/`,
grep.execute({ pattern: "needle", path: path.join(cache, "src"), include: "*.md" }, next),
)
const info = yield* GrepTool
const grep = yield* info.init()
const result = yield* githubBase(
`file://${remoteRoot}/`,
grep.execute({ pattern: "needle", path: path.join(cache, "src"), include: "*.md" }, next),
)
expect(result.metadata.matches).toBe(1)
expect(full(result.output)).toContain(full(path.join(cache, "src", "notes.md")))
expect(requests.find((req) => req.permission === "external_directory")).toBeUndefined()
}),
expect(result.metadata.matches).toBe(1)
expect(full(result.output)).toContain(full(path.join(cache, "src", "notes.md")))
expect(requests.find((req) => req.permission === "external_directory")).toBeUndefined()
}),
),
{
config: {
reference: {
@@ -264,3 +247,15 @@ describe("tool.grep", () => {
},
)
})
function withReferences<A, E, R>(body: Effect.Effect<A, E, R>) {
return Effect.acquireUseRelease(
Effect.sync(() => process.env.OPENCODE_EXPERIMENTAL_REFERENCES),
() => Effect.sync(() => void (process.env.OPENCODE_EXPERIMENTAL_REFERENCES = "true")).pipe(Effect.andThen(body)),
(previous) =>
Effect.sync(() => {
if (previous === undefined) delete process.env.OPENCODE_EXPERIMENTAL_REFERENCES
else process.env.OPENCODE_EXPERIMENTAL_REFERENCES = previous
}),
)
}
+48 -52
View File
@@ -24,8 +24,9 @@ import {
tmpdirScoped,
} from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { Reference } from "@/reference/reference"
import { RepositoryCache } from "@/reference/repository-cache"
import { githubBase } from "../fixture/repository"
import { Reference } from "@/reference"
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
const FIXTURES_DIR = path.join(import.meta.dir, "fixtures")
@@ -44,12 +45,7 @@ const ctx = {
ask: () => Effect.void,
}
const referenceLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
Reference.layer.pipe(
Layer.provide(Config.defaultLayer),
Layer.provide(RepositoryCache.defaultLayer),
Layer.provide(RuntimeFlags.layer(flags)),
)
const referenceLayer = (_flags: Partial<RuntimeFlags.Info> = {}) => Reference.defaultLayer
const readLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
Layer.mergeAll(
@@ -102,20 +98,6 @@ 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 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], {
@@ -265,44 +247,58 @@ describe("tool.read external_directory permission", () => {
)
references.live("does not ask for external_directory permission when reading configured references", () =>
Effect.gen(function* () {
const fs = yield* FSUtil.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))
withReferences(
Effect.gen(function* () {
const fs = yield* FSUtil.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 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 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")
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()
}),
expect(result.output).toContain("reference notes")
expect(ext).toBeUndefined()
}),
),
)
})
function withReferences<A, E, R>(body: Effect.Effect<A, E, R>) {
return Effect.acquireUseRelease(
Effect.sync(() => process.env.OPENCODE_EXPERIMENTAL_REFERENCES),
() => Effect.sync(() => void (process.env.OPENCODE_EXPERIMENTAL_REFERENCES = "true")).pipe(Effect.andThen(body)),
(previous) =>
Effect.sync(() => {
if (previous === undefined) delete process.env.OPENCODE_EXPERIMENTAL_REFERENCES
else process.env.OPENCODE_EXPERIMENTAL_REFERENCES = previous
}),
)
}
describe("tool.read env file permissions", () => {
const cases: [string, boolean][] = [
[".env", true],
+2 -2
View File
@@ -29,8 +29,8 @@ import { Format } from "@/format"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import * as Truncate from "@/tool/truncate"
import { InstanceState } from "@/effect/instance-state"
import { Reference } from "@/reference/reference"
import { RepositoryCache } from "@/reference/repository-cache"
import { Reference } from "@/reference"
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
import { ToolJsonSchema } from "@/tool/json-schema"
import { MessageID, SessionID } from "@/session/schema"