export * as Reference from "./reference" import { makeLocationNode } from "./effect/app-node" import { Context, Effect, Layer, Scope, Types } from "effect" import { Reference } from "@opencode-ai/schema/reference" import { Global } from "./global" import { EventV2 } from "./event" import { Repository } from "./repository" import { RepositoryCache } from "./repository-cache" import { AbsolutePath } from "./schema" import { State } from "./state" export const LocalSource = Reference.LocalSource export type LocalSource = Reference.LocalSource export const GitSource = Reference.GitSource export type GitSource = Reference.GitSource export const Source = Reference.Source export type Source = Reference.Source export const Event = Reference.Event export const Info = Reference.Info export type Info = Reference.Info type Data = { sources: Map> } type Draft = { add(name: string, source: Source): void remove(name: string): void list(): readonly [string, Source][] } export interface Interface extends State.Transformable { readonly list: () => Effect.Effect } export class Service extends Context.Service()("@opencode/v2/Reference") {} const layer = Layer.effect( Service, Effect.gen(function* () { const global = yield* Global.Service const events = yield* EventV2.Service const cache = yield* RepositoryCache.Service const scope = yield* Scope.Scope const materialized = new Map() const state = State.create({ name: "reference", initial: () => ({ sources: new Map() }), draft: (draft) => ({ add: (name, source) => draft.sources.set(name, source as Types.DeepMutable), remove: (name) => draft.sources.delete(name), list: () => Array.from(draft.sources.entries()) as [string, Source][], }), finalize: (draft) => Effect.gen(function* () { materialized.clear() const seen = new Map() for (const [name, source] of draft.list()) { if (source.type === "local") { materialized.set( name, Info.make({ name, path: source.path, ...(source.description === undefined ? {} : { description: source.description }), ...(source.hidden === undefined ? {} : { hidden: source.hidden }), source, }), ) continue } const repository = Repository.parse(source.repository) if (!repository || !Repository.isRemote(repository)) continue if (source.branch) { try { Repository.validateBranch(source.branch) } catch { continue } } const target = Repository.cachePath(global.repos, repository) if (seen.has(target) && seen.get(target) !== source.branch) continue seen.set(target, source.branch) materialized.set( name, Info.make({ name, path: AbsolutePath.make(target), ...(source.description === undefined ? {} : { description: source.description }), ...(source.hidden === undefined ? {} : { hidden: source.hidden }), source, }), ) yield* cache.ensure({ reference: repository, branch: source.branch, refresh: true }).pipe( Effect.catchCause((cause) => Effect.logWarning("failed to materialize reference", { name, repository: source.repository, cause, }), ), Effect.forkIn(scope), ) } yield* events.publish(Event.Updated, {}) }), }) return Service.of({ transform: state.transform, reload: state.reload, list: Effect.fn("Reference.list")(function* () { return Array.from(materialized.values()) }), }) }), ) export const node = makeLocationNode({ service: Service, layer, deps: [Global.node, EventV2.node, RepositoryCache.node], })