diff --git a/packages/core/src/plugin/internal.ts b/packages/core/src/plugin/internal.ts index 4219a85f452..d626eb2d85e 100644 --- a/packages/core/src/plugin/internal.ts +++ b/packages/core/src/plugin/internal.ts @@ -83,6 +83,7 @@ import { ProviderPlugins } from "./provider.js" import { WebSearchPlugins } from "./websearch/index.js" import { PluginRuntime } from "./runtime.js" import { SkillPlugin } from "./skill.js" +import { VcsHgPlugin } from "./vcs/hg.js" import { SystemPromptPlugin } from "./system-prompt.js" import { VariantPlugin } from "./variant.js" import { VcsGitPlugin } from "./vcs/git.js" @@ -241,6 +242,7 @@ const pre = [ PlanPlugin.Plugin, CommandPlugin.Plugin, SkillPlugin.Plugin, + VcsHgPlugin.Plugin, ...SystemPromptPlugin.Plugins, ModelsDevPlugin, ...ProviderPlugins, diff --git a/packages/core/src/vcs/hg.ts b/packages/core/src/plugin/vcs/hg.ts similarity index 86% rename from packages/core/src/vcs/hg.ts rename to packages/core/src/plugin/vcs/hg.ts index 1a4471d37f5..c4f6dddd6c6 100644 --- a/packages/core/src/vcs/hg.ts +++ b/packages/core/src/plugin/vcs/hg.ts @@ -1,13 +1,15 @@ -export * as VcsHg from "./hg.js" +export * as VcsHgPlugin from "./hg.js" import path from "path" import { Effect } from "effect" import { ChildProcess } from "effect/unstable/process" +import { define } from "@opencode-ai/plugin/effect/plugin" import { FileDiff } from "@opencode-ai/schema/file-diff" import { FileStatus, Info, Mode } from "@opencode-ai/schema/vcs" import { FSUtil } from "@opencode-ai/util/fs-util" import { AppProcess } from "@opencode-ai/util/process" -import type { Adapter, DiffOptions } from "../vcs.js" +import { Location } from "../../location.js" +import type { Adapter, DiffOptions } from "../../vcs.js" import { addPatch, chunksByFile, @@ -17,14 +19,40 @@ import { MAX_PATCH_BYTES, MAX_TOTAL_PATCH_BYTES, PATCH_CONTEXT_LINES, -} from "./patch.js" +} from "../../vcs/patch.js" + +export const Plugin = define({ + id: "opencode.vcs.hg", + effect: Effect.fn("VcsHgPlugin")(function* (ctx) { + const location = yield* Location.Service + if (location.vcs?.type !== "hg") return + + const processes = yield* AppProcess.Service + const fs = yield* FSUtil.Service + const adapter = make(processes, fs, { + directory: location.directory, + worktree: location.project.directory, + }) + + yield* ctx.vcs.transform((draft) => { + draft.add({ + id: "hg", + name: "Mercurial", + info: () => adapter.info(), + branches: (input) => adapter.branches({ search: input.search, limit: input.limit }), + status: () => adapter.status(), + diff: (input) => adapter.diff(input.mode, { context: input.context }), + }) + }) + }), +}) /** * Mercurial adapter for the Vcs service. `hg diff --git` emits git-format * patches for tracked changes; untracked (`?`) and missing (`!`) files never * appear in `hg diff`, so their patches are synthesized from file contents. */ -export function make( +function make( proc: AppProcess.Interface, fs: FSUtil.Interface, input: { directory: string; worktree: string }, diff --git a/packages/core/src/vcs.ts b/packages/core/src/vcs.ts index 4fe85545719..bf2c5304f83 100644 --- a/packages/core/src/vcs.ts +++ b/packages/core/src/vcs.ts @@ -10,10 +10,8 @@ import { VcsEvent } from "@opencode-ai/schema/vcs-event" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { FSUtil } from "@opencode-ai/util/fs-util" import { Location } from "./location.js" -import { AppProcess } from "@opencode-ai/util/process" import { Bus } from "./bus.js" import { State } from "./state.js" -import { VcsHg } from "./vcs/hg.js" import { emptyPatch, MAX_TOTAL_PATCH_BYTES, PATCH_CONTEXT_LINES } from "./vcs/patch.js" export { BranchList, FileStatus, Info, Mode } @@ -43,24 +41,14 @@ interface Data { export class Service extends Context.Service()("@opencode/Vcs") {} -// Adapter seam: one working-copy implementation per VCS type, selected by the -// resolved location. Locations without a supported VCS degrade to empty -// results so callers never need to special-case. -const adapter = (proc: AppProcess.Interface, fs: FSUtil.Interface, location: Location.Interface) => { - const scope = { directory: location.directory, worktree: location.project.directory } - if (location.vcs?.type === "hg") return VcsHg.make(proc, fs, scope) -} - const layer = Layer.effect( Service, Effect.gen(function* () { - const proc = yield* AppProcess.Service const fs = yield* FSUtil.Service const location = yield* Location.Service const bus = yield* Bus.Service - const native = adapter(proc, fs, location) const vcs = location.vcs - const current = { info: native ? yield* native.info() : ({ branch: {} } satisfies Info) } + const current: { info: Info } = { info: { branch: {} } } const scope = { directory: location.directory, worktree: location.project.directory, @@ -102,9 +90,7 @@ const layer = Layer.effect( const provider = selected() const next: Info = provider ? yield* protect(provider, "info", provider.info(scope).pipe(Effect.flatMap(decodeInfo)), { branch: {} }) - : native - ? yield* native.info() - : { branch: {} } + : { branch: {} } const changed = current.info.branch.current !== next.branch.current current.info = next if (changed) yield* bus.publish(VcsEvent.BranchUpdated, { branch: next.branch.current }) @@ -140,8 +126,7 @@ const layer = Layer.effect( provider.branches({ ...scope, ...options }).pipe(Effect.flatMap(decodeBranches)), [], ) - if (!native) return [] - return yield* native.branches(options) + return [] }), status: Effect.fn("Vcs.status")(function* () { const provider = selected() @@ -155,12 +140,11 @@ const layer = Layer.effect( ), [], ) - if (!native) return [] - return yield* native.status() + return [] }), diff: Effect.fn("Vcs.diff")(function* (mode: Mode, options?: DiffOptions) { const provider = selected() - if (!provider) return native ? yield* native.diff(mode, options) : [] + if (!provider) return [] const rows = yield* protect( provider, "diff", @@ -189,5 +173,5 @@ const layer = Layer.effect( export const node = makeLocationNode({ service: Service, layer: layer, - deps: [AppProcess.node, FSUtil.node, Location.node, Bus.node], + deps: [FSUtil.node, Location.node, Bus.node], }) diff --git a/packages/core/test/vcs-hg.test.ts b/packages/core/test/vcs-hg.test.ts index 06053ac51f7..9d1e2950088 100644 --- a/packages/core/test/vcs-hg.test.ts +++ b/packages/core/test/vcs-hg.test.ts @@ -4,21 +4,25 @@ import fs from "fs/promises" import path from "path" import { Effect, Fiber, Layer, Stream } from "effect" import { LayerNode } from "@opencode-ai/util/effect/layer-node" +import { FSUtil } from "@opencode-ai/util/fs-util" +import { AppProcess } from "@opencode-ai/util/process" import { Bus } from "@opencode-ai/core/bus" import { Location } from "@opencode-ai/core/location" import { AbsolutePath } from "@opencode-ai/core/schema" import { Vcs } from "@opencode-ai/core/vcs" +import { VcsHgPlugin } from "@opencode-ai/core/plugin/vcs/hg" import { FileSystem } from "@opencode-ai/schema/filesystem" import { VcsEvent } from "@opencode-ai/schema/vcs-event" import { location } from "./fixture/location" import { tmpdir } from "./fixture/tmpdir" import { it } from "./lib/effect" +import { host } from "./plugin/host" const describeHg = Bun.which("hg") ? describe : describe.skip const provide = (directory: string) => Effect.provide( - LayerNode.compile(LayerNode.group([Vcs.node, Bus.node]), [ + LayerNode.compile(LayerNode.group([Vcs.node, Bus.node, Location.node, AppProcess.node, FSUtil.node]), [ [ Location.node, Layer.succeed( @@ -42,7 +46,19 @@ const withTmp = (f: (directory: string) => Effect.Effect) => const withHg = (f: (directory: string) => Effect.Effect) => withTmp((directory) => - Effect.promise(() => hg(directory, "init")).pipe(Effect.andThen(f(directory).pipe(provide(directory)))), + Effect.promise(() => hg(directory, "init")).pipe( + Effect.andThen( + Effect.gen(function* () { + const vcs = yield* Vcs.Service + const context = host() + yield* VcsHgPlugin.Plugin.effect({ + ...context, + vcs: { ...context.vcs, transform: vcs.transform, reload: vcs.reload }, + }) + return yield* f(directory) + }).pipe(provide(directory)), + ), + ), ) async function hg(directory: string, ...args: string[]) {