reference.ts 3.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. export * as ConfigReferencePlugin from "./reference.js"
  2. import { define } from "@opencode-ai/plugin/effect/plugin"
  3. import { Document } from "@opencode-ai/schema/config"
  4. import { ConfigReference } from "@opencode-ai/schema/config/reference"
  5. import path from "path"
  6. import { Effect, Stream } from "effect"
  7. import { Config } from "../../config.js"
  8. import { Reference } from "../../reference.js"
  9. import { AbsolutePath } from "../../schema.js"
  10. import { Global } from "@opencode-ai/util/global"
  11. import { Location } from "../../location.js"
  12. export const Plugin = define({
  13. id: "opencode.config.reference",
  14. effect: Effect.fn(function* (ctx) {
  15. const config = yield* Config.Service
  16. const location = yield* Location.Service
  17. const global = yield* Global.Service
  18. const loaded = { entries: yield* config.entries() }
  19. yield* ctx.reference.transform((draft) => {
  20. const entries = new Map<string, Reference.Source>()
  21. for (const doc of loaded.entries.filter((entry): entry is Document => entry.type === "document")) {
  22. const directory = doc.path ? path.dirname(doc.path) : location.directory
  23. for (const [name, entry] of Object.entries(doc.info.references ?? {})) {
  24. if (!validAlias(name)) continue
  25. const description = typeof entry === "string" ? undefined : entry.description
  26. const hidden = typeof entry === "string" ? undefined : entry.hidden
  27. entries.set(
  28. name,
  29. local(entry)
  30. ? Reference.LocalSource.make({
  31. type: "local",
  32. path: AbsolutePath.make(
  33. localPath(directory, global.home, typeof entry === "string" ? entry : entry.path),
  34. ),
  35. ...(description === undefined ? {} : { description }),
  36. ...(hidden === undefined ? {} : { hidden }),
  37. })
  38. : Reference.GitSource.make({
  39. type: "git",
  40. repository: typeof entry === "string" ? entry : entry.repository,
  41. ...(entry.branch === undefined ? {} : { branch: entry.branch }),
  42. ...(description === undefined ? {} : { description }),
  43. ...(hidden === undefined ? {} : { hidden }),
  44. }),
  45. )
  46. }
  47. }
  48. for (const [name, source] of entries) draft.add(name, source)
  49. })
  50. yield* ctx.event.subscribe("config.updated").pipe(
  51. Stream.runForEach(() =>
  52. config.entries().pipe(
  53. Effect.tap((entries) => Effect.sync(() => (loaded.entries = entries))),
  54. Effect.andThen(ctx.reference.reload()),
  55. ),
  56. ),
  57. Effect.forkScoped({ startImmediately: true }),
  58. )
  59. }),
  60. })
  61. function validAlias(name: string) {
  62. return name.length > 0 && !/[\/\s`,]/.test(name)
  63. }
  64. function local(entry: ConfigReference.Entry): entry is string | ConfigReference.Local {
  65. return typeof entry === "string"
  66. ? entry.startsWith(".") || entry.startsWith("/") || entry.startsWith("~")
  67. : "path" in entry
  68. }
  69. function localPath(directory: string, home: string, value: string) {
  70. if (value.startsWith("~/")) return path.join(home, value.slice(2))
  71. return path.isAbsolute(value) ? value : path.resolve(directory, value)
  72. }