reference.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. export * as Reference from "./reference"
  2. import { Context, Effect, Layer, Schema, Scope } from "effect"
  3. import { castDraft } from "immer"
  4. import { Global } from "./global"
  5. import { EventV2 } from "./event"
  6. import { Repository } from "./repository"
  7. import { RepositoryCache } from "./repository-cache"
  8. import { AbsolutePath } from "./schema"
  9. import { State } from "./state"
  10. export class LocalSource extends Schema.Class<LocalSource>("Reference.LocalSource")({
  11. type: Schema.Literal("local"),
  12. path: AbsolutePath,
  13. description: Schema.String.pipe(Schema.optional),
  14. hidden: Schema.Boolean.pipe(Schema.optional),
  15. }) {}
  16. export class GitSource extends Schema.Class<GitSource>("Reference.GitSource")({
  17. type: Schema.Literal("git"),
  18. repository: Schema.String,
  19. branch: Schema.String.pipe(Schema.optional),
  20. description: Schema.String.pipe(Schema.optional),
  21. hidden: Schema.Boolean.pipe(Schema.optional),
  22. }) {}
  23. export const Source = Schema.Union([LocalSource, GitSource]).pipe(Schema.toTaggedUnion("type"))
  24. export type Source = typeof Source.Type
  25. export const Event = {
  26. Updated: EventV2.define({ type: "reference.updated", schema: {} }),
  27. }
  28. export class Info extends Schema.Class<Info>("Reference.Info")({
  29. name: Schema.String,
  30. path: AbsolutePath,
  31. description: Schema.String.pipe(Schema.optional),
  32. hidden: Schema.Boolean.pipe(Schema.optional),
  33. source: Source,
  34. }) {}
  35. type Data = {
  36. sources: Map<string, Source>
  37. }
  38. type Editor = {
  39. add(name: string, source: Source): void
  40. remove(name: string): void
  41. list(): readonly [string, Source][]
  42. }
  43. export interface Interface {
  44. readonly transform: State.Interface<Data, Editor>["transform"]
  45. readonly list: () => Effect.Effect<Info[]>
  46. }
  47. export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Reference") {}
  48. export const layer = Layer.effect(
  49. Service,
  50. Effect.gen(function* () {
  51. const global = yield* Global.Service
  52. const events = yield* EventV2.Service
  53. const cache = yield* RepositoryCache.Service
  54. const scope = yield* Scope.Scope
  55. const materialized = new Map<string, Info>()
  56. const state = State.create<Data, Editor>({
  57. initial: () => ({ sources: new Map() }),
  58. editor: (draft) => ({
  59. add: (name, source) => draft.sources.set(name, castDraft(source)),
  60. remove: (name) => draft.sources.delete(name),
  61. list: () => Array.from(draft.sources.entries()) as [string, Source][],
  62. }),
  63. finalize: (editor) =>
  64. Effect.gen(function* () {
  65. materialized.clear()
  66. const seen = new Map<string, string | undefined>()
  67. for (const [name, source] of editor.list()) {
  68. if (source.type === "local") {
  69. materialized.set(
  70. name,
  71. new Info({
  72. name,
  73. path: source.path,
  74. description: source.description,
  75. hidden: source.hidden,
  76. source,
  77. }),
  78. )
  79. continue
  80. }
  81. const repository = Repository.parse(source.repository)
  82. if (!repository || !Repository.isRemote(repository)) continue
  83. if (source.branch) {
  84. try {
  85. Repository.validateBranch(source.branch)
  86. } catch {
  87. continue
  88. }
  89. }
  90. const target = Repository.cachePath(global.repos, repository)
  91. if (seen.has(target) && seen.get(target) !== source.branch) continue
  92. seen.set(target, source.branch)
  93. materialized.set(
  94. name,
  95. new Info({
  96. name,
  97. path: AbsolutePath.make(target),
  98. description: source.description,
  99. hidden: source.hidden,
  100. source,
  101. }),
  102. )
  103. yield* cache.ensure({ reference: repository, branch: source.branch, refresh: true }).pipe(
  104. Effect.catchCause((cause) =>
  105. Effect.logWarning("failed to materialize reference", {
  106. name,
  107. repository: source.repository,
  108. cause,
  109. }),
  110. ),
  111. Effect.forkIn(scope),
  112. )
  113. }
  114. yield* events.publish(Event.Updated, {})
  115. }),
  116. })
  117. return Service.of({
  118. transform: state.transform,
  119. list: Effect.fn("Reference.list")(function* () {
  120. return Array.from(materialized.values())
  121. }),
  122. })
  123. }),
  124. )
  125. export const locationLayer = layer