2026-05-10 12:27:11 +00:00
import path from "path"
import { Effect , Context , Layer , Scope } from "effect"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Global } from "@opencode-ai/core/global"
import { Config } from "@/config/config"
2026-05-18 15:12:41 +00:00
import { ConfigReference } from "@/config/reference"
2026-05-10 12:27:11 +00:00
import { InstanceState } from "@/effect/instance-state"
2026-05-13 11:37:53 +00:00
import { RuntimeFlags } from "@/effect/runtime-flags"
2026-05-18 15:34:44 +00:00
import { parseRepositoryReference , repositoryCachePath , type RemoteReference } from "@/util/repository"
2026-05-10 12:27:11 +00:00
import { RepositoryCache } from "./repository-cache"
export type Resolved =
| {
name : string
kind : "local"
path : string
}
| {
name : string
kind : "git"
repository : string
2026-05-18 15:34:44 +00:00
reference : RemoteReference
2026-05-10 12:27:11 +00:00
path : string
branch? : string
}
| {
name : string
kind : "invalid"
2026-05-18 15:12:41 +00:00
repository? : string
2026-05-10 12:27:11 +00:00
message : string
}
type State = {
references : Resolved [ ]
materializeAll : Effect.Effect < void >
materializeByPath : { 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" ? AppFileSystem . normalizePath ( target ) : target
}
function containsReferencePath ( referencePath : string , target : string ) {
return AppFileSystem . contains ( normalizedTarget ( referencePath ) ? ? referencePath , target )
}
2026-05-10 12:28:11 +00:00
export function resolve ( input : {
name : string
2026-05-18 15:12:41 +00:00
reference : ConfigReference.NormalizedEntry
2026-05-10 12:28:11 +00:00
directory : string
worktree : string
} ) : Resolved {
2026-05-18 15:12:41 +00:00
if ( input . reference . kind === "invalid" ) {
return { name : input.name , kind : "invalid" , message : input.reference.message }
2026-05-10 12:27:11 +00:00
}
2026-05-18 15:12:41 +00:00
if ( input . reference . kind === "local" ) {
2026-05-10 12:27:11 +00:00
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 } )
}
2026-05-18 15:14:13 +00:00
export function resolveAll ( input : { references : ConfigReference.NormalizedInfo ; directory : string ; worktree : string } ) {
2026-05-10 12:27:11 +00:00
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
2026-05-18 15:55:38 +00:00
const cache = yield * RepositoryCache . Service
2026-05-10 12:27:11 +00:00
const scope = yield * Scope . Scope
2026-05-13 11:37:53 +00:00
const flags = yield * RuntimeFlags . Service
2026-05-10 12:27:11 +00:00
const state = yield * InstanceState . make < State > (
Effect . fn ( "Reference.state" ) ( function * ( ctx ) {
const cfg = yield * config . get ( )
2026-05-10 12:28:11 +00:00
const references = resolveAll ( {
2026-05-18 15:12:41 +00:00
references : ConfigReference.normalize ( cfg . reference ? ? { } ) ,
2026-05-10 12:28:11 +00:00
directory : ctx.directory ,
worktree : ctx.worktree ,
} )
2026-05-10 12:27:11 +00:00
const seenPath = new Set < string > ( )
const gitReferences = 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
} )
const materializeByPath = yield * Effect . forEach (
gitReferences ,
Effect . fnUntraced ( function * ( reference ) {
const run = yield * Effect . cached (
2026-05-18 15:55:38 +00:00
cache . ensure ( { reference : reference.reference , branch : reference.branch , refresh : true } ) . pipe (
2026-05-10 12:27:11 +00:00
Effect . asVoid ,
Effect . catchCause ( ( cause ) = >
2026-05-12 16:31:18 +00:00
Effect . logWarning ( "failed to materialize reference repository" ) . pipe (
Effect . annotateLogs ( { name : reference.name , cause } ) ,
) ,
2026-05-10 12:27:11 +00:00
) ,
) ,
)
return { path : reference.path , run }
} ) ,
{ concurrency : "unbounded" } ,
)
const materializeAll = yield * Effect . cached (
2026-05-13 11:37:53 +00:00
flags . experimentalScout
2026-05-10 12:27:11 +00:00
? Effect . gen ( function * ( ) {
yield * Effect . forEach (
materializeByPath ,
Effect . fnUntraced ( function * ( item ) {
yield * item . run
} ) ,
{ concurrency : 4 , discard : true } ,
)
} )
: Effect . void ,
)
return { references , materializeAll , materializeByPath }
} ) ,
)
return Service . of ( {
init : Effect.fn ( "Reference.init" ) ( function * ( ) {
2026-05-13 11:37:53 +00:00
if ( ! flags . experimentalScout ) return
2026-05-10 12:27:11 +00:00
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 ) {
2026-05-13 11:37:53 +00:00
if ( ! flags . experimentalScout ) return
2026-05-10 12:27:11 +00:00
const full = normalizedTarget ( target )
if ( ! full ) return yield * InstanceState . useEffect ( state , ( s ) = > s . materializeAll )
return yield * InstanceState . useEffect (
state ,
( s ) = > s . materializeByPath . find ( ( item ) = > containsReferencePath ( item . path , full ) ) ? . run ? ? Effect . void ,
)
} ) ,
contains : Effect.fn ( "Reference.contains" ) ( function * ( target? : string ) {
2026-05-13 11:37:53 +00:00
if ( ! flags . experimentalScout ) return false
2026-05-10 12:27:11 +00:00
const full = normalizedTarget ( target )
if ( ! full ) return false
return yield * InstanceState . use ( state , ( s ) = >
s . references . some ( ( reference ) = > reference . kind === "git" && containsReferencePath ( reference . path , full ) ) ,
)
} ) ,
} )
} ) ,
)
export const defaultLayer = layer . pipe (
Layer . provide ( Config . defaultLayer ) ,
2026-05-18 15:55:38 +00:00
Layer . provide ( RepositoryCache . defaultLayer ) ,
2026-05-13 11:37:53 +00:00
Layer . provide ( RuntimeFlags . defaultLayer ) ,
2026-05-10 12:27:11 +00:00
)
export * as Reference from "./reference"