export * as Reference from "./reference" import { Context, Effect, Layer, Schema, Scope, Types } from "effect" 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 class LocalSource extends Schema.Class("Reference.LocalSource")({ type: Schema.Literal("local"), path: AbsolutePath, description: Schema.String.pipe(Schema.optional), hidden: Schema.Boolean.pipe(Schema.optional), }) {} export class GitSource extends Schema.Class("Reference.GitSource")({ type: Schema.Literal("git"), repository: Schema.String, branch: Schema.String.pipe(Schema.optional), description: Schema.String.pipe(Schema.optional), hidden: Schema.Boolean.pipe(Schema.optional), }) {} export const Source = Schema.Union([LocalSource, GitSource]).pipe(Schema.toTaggedUnion("type")) export type Source = typeof Source.Type export const Event = { Updated: EventV2.define({ type: "reference.updated", schema: {} }), } export class Info extends Schema.Class("Reference.Info")({ name: Schema.String, path: AbsolutePath, description: Schema.String.pipe(Schema.optional), hidden: Schema.Boolean.pipe(Schema.optional), source: Source, }) {} 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") {} export 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({ 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, new Info({ name, path: source.path, description: source.description, 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, new Info({ name, path: AbsolutePath.make(target), description: source.description, 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, rebuild: state.rebuild, list: Effect.fn("Reference.list")(function* () { return Array.from(materialized.values()) }), }) }), ) export const locationLayer = layer