diff --git a/packages/client/src/effect/api/api.ts b/packages/client/src/effect/api/api.ts index 63f91911d89..57906fd630b 100644 --- a/packages/client/src/effect/api/api.ts +++ b/packages/client/src/effect/api/api.ts @@ -2225,7 +2225,7 @@ export interface WorktreeApi { export type VcsInitInput = { readonly location?: { readonly directory?: string | undefined } | undefined - readonly provider?: "git" | undefined + readonly provider?: string | undefined } export type VcsInitOutput = void export type VcsInitOperation = (input?: VcsInitInput) => Effect.Effect diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 7447d4a333a..62dd9897b81 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -6252,11 +6252,11 @@ export type WorktreeRefreshOutput = void export type VcsInitInput = { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined - readonly provider?: "git" | undefined + readonly provider?: string | undefined }["location"] readonly provider?: { readonly location?: { readonly directory?: string | undefined } | undefined - readonly provider?: "git" | undefined + readonly provider?: string | undefined }["provider"] } diff --git a/packages/core/src/plugin/vcs/git.ts b/packages/core/src/plugin/vcs/git.ts index 6314d86e0b0..dd5fc2b1657 100644 --- a/packages/core/src/plugin/vcs/git.ts +++ b/packages/core/src/plugin/vcs/git.ts @@ -23,7 +23,8 @@ export const Plugin = define({ id: "opencode.vcs.git", effect: Effect.fn("VcsGitPlugin")(function* (ctx) { const location = yield* Location.Service - if (location.vcs?.type !== "git") return + // Markerless locations need this definition to initialize their first repository. + if (location.vcs && location.vcs.type !== "git") return const processes = yield* AppProcess.Service const adapter = make(processes, { @@ -35,6 +36,13 @@ export const Plugin = define({ editor.add({ id: "git", name: "Git", + init: (input) => + Effect.gen(function* () { + const result = yield* processes.run( + ChildProcess.make(gitExecutable, ["init"], { cwd: input.worktree, stdin: "ignore" }), + ) + if (result.exitCode !== 0) return yield* Effect.fail(new Error("Git initialization failed")) + }), info: () => adapter.info(), base: () => adapter.base(), branches: (input) => adapter.branches({ search: input.search, limit: input.limit }), diff --git a/packages/core/src/plugin/vcs/hg.ts b/packages/core/src/plugin/vcs/hg.ts index fbf5a09410a..7249253220c 100644 --- a/packages/core/src/plugin/vcs/hg.ts +++ b/packages/core/src/plugin/vcs/hg.ts @@ -26,7 +26,8 @@ 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 + // Markerless locations need this definition to initialize their first repository. + if (location.vcs && location.vcs.type !== "hg") return const processes = yield* AppProcess.Service const fs = yield* FSUtil.Service @@ -39,6 +40,18 @@ export const Plugin = define({ editor.add({ id: "hg", name: "Mercurial", + init: (input) => + Effect.gen(function* () { + const result = yield* processes.run( + ChildProcess.make("hg", ["init"], { + cwd: input.worktree, + env: { HGPLAIN: "1" }, + extendEnv: true, + stdin: "ignore", + }), + ) + if (result.exitCode !== 0) return yield* Effect.fail(new Error("Mercurial initialization failed")) + }), info: () => adapter.info(), branches: (input) => adapter.branches({ search: input.search, limit: input.limit }), status: () => adapter.status(), diff --git a/packages/core/src/project.ts b/packages/core/src/project.ts index 856d7cb4703..2ce76ef2b79 100644 --- a/packages/core/src/project.ts +++ b/packages/core/src/project.ts @@ -36,10 +36,6 @@ export class NotFoundError extends Schema.TaggedError()("Project. projectID: ID, }) {} -export class InitializeGitError extends Schema.TaggedError()("Project.InitializeGitError", { - kind: Schema.Literals(["missing", "conflict", "failed"]), -}) {} - export interface Resolved { readonly previous?: ID readonly id: ID @@ -70,7 +66,6 @@ export interface Interface { readonly activate: (projectID: ID) => Effect.Effect /** Resolves and persists the owning Project. */ readonly resolve: (input: AbsolutePath, options?: { readonly discovery?: boolean }) => Effect.Effect - readonly initializeGit: (directory: AbsolutePath) => Effect.Effect } export class Service extends Context.Service()("@opencode/Project") {} @@ -387,17 +382,7 @@ const layer = Layer.effect( }) }) - const initializeGit = Effect.fn("Project.initializeGit")(function* (directory: AbsolutePath) { - if (!(yield* fs.isDir(directory))) return yield* new InitializeGitError({ kind: "missing" }) - if ((yield* resolve(directory)).vcs) return yield* new InitializeGitError({ kind: "conflict" }) - const result = yield* proc - .run(ChildProcess.make("git", ["init"], { cwd: directory, stdin: "ignore" })) - .pipe(Effect.mapError(() => new InitializeGitError({ kind: "failed" }))) - if (result.exitCode !== 0) return yield* new InitializeGitError({ kind: "failed" }) - return yield* resolve(directory) - }) - - return Service.of({ list, update, activate, resolve, initializeGit }) + return Service.of({ list, update, activate, resolve }) }), ) diff --git a/packages/core/src/vcs.ts b/packages/core/src/vcs.ts index 3861a28e1b7..dd6e613fdd9 100644 --- a/packages/core/src/vcs.ts +++ b/packages/core/src/vcs.ts @@ -10,6 +10,7 @@ import { VcsEvent } from "@opencode/schema/vcs-event" import { makeLocationNode } from "@opencode/util/effect/app-node" import { FSUtil } from "@opencode/util/fs-util" import { Location } from "./location.js" +import { Project } from "./project.js" import { Bus } from "./bus.js" import { State } from "./state.js" import { emptyPatch, MAX_TOTAL_PATCH_BYTES, PATCH_CONTEXT_LINES } from "./vcs/patch.js" @@ -20,6 +21,10 @@ export class DiffError extends Schema.TaggedError()("Vcs.DiffError", message: Schema.String, }) {} +export class InitializeError extends Schema.TaggedError()("Vcs.InitializeError", { + kind: Schema.Literals(["missing", "conflict", "unsupported", "failed"]), +}) {} + export interface DiffOptions { readonly context?: number readonly base?: string @@ -40,6 +45,7 @@ export interface Adapter { export interface Interface extends Adapter, State.Transformable { readonly base: () => Effect.Effect + readonly initialize: (providerID: string) => Effect.Effect } interface Data { @@ -124,7 +130,9 @@ const layer = Layer.effect( const provider = selected() const next: Info = provider ? { - ...(yield* protect(provider, "info", provider.info(scope).pipe(Effect.flatMap(decodeInfo)), { branch: {} })), + ...(yield* protect(provider, "info", provider.info(scope).pipe(Effect.flatMap(decodeInfo)), { + branch: {}, + })), provider: provider.id, } : { branch: {} } @@ -166,6 +174,23 @@ const layer = Layer.effect( return Service.of({ transform: state.transform, reload: state.reload, + initialize: Effect.fn("Vcs.initialize")(function* (providerID: string) { + if (!(yield* fs.isDir(location.project.directory))) return yield* new InitializeError({ kind: "missing" }) + if (yield* Project.root(fs, location.project.directory)) return yield* new InitializeError({ kind: "conflict" }) + const provider = state.get().providers.get(providerID) + if (!provider?.init) return yield* new InitializeError({ kind: "unsupported" }) + yield* provider + .init(scope) + .pipe( + Effect.catchCause((cause) => + Cause.hasInterrupts(cause) + ? Effect.failCause(cause).pipe(Effect.orDie) + : Effect.logWarning("vcs initialization failed", { provider: providerID, cause }).pipe( + Effect.andThen(Effect.fail(new InitializeError({ kind: "failed" }))), + ), + ), + ) + }), info: Effect.fn("Vcs.info")(function* () { return current.info }), diff --git a/packages/core/test/effect/layer-node/node-build.test.ts b/packages/core/test/effect/layer-node/node-build.test.ts index 22fcf53388e..d2aeca97d72 100644 --- a/packages/core/test/effect/layer-node/node-build.test.ts +++ b/packages/core/test/effect/layer-node/node-build.test.ts @@ -101,7 +101,6 @@ describe("node build", () => { list: () => Effect.succeed([]), update: () => Effect.die("not implemented"), activate: () => Effect.void, - initializeGit: () => Effect.die("not implemented"), resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory, canonical: directory }), }) }), diff --git a/packages/core/test/lib/project.ts b/packages/core/test/lib/project.ts index 4a8abbef6a5..30760c58e0f 100644 --- a/packages/core/test/lib/project.ts +++ b/packages/core/test/lib/project.ts @@ -14,7 +14,6 @@ export const globalProjectNode = makeGlobalNode({ list: () => Effect.succeed([]), update: () => Effect.die("not implemented"), activate: () => Effect.void, - initializeGit: () => Effect.die("not implemented"), resolve: (directory) => { const project = { id: Project.ID.global, directory, canonical: directory } return upsertProject(database.db, project).pipe(Effect.orDie, Effect.as(project)) diff --git a/packages/core/test/location.test.ts b/packages/core/test/location.test.ts index 395b8ad1e7e..8843145f494 100644 --- a/packages/core/test/location.test.ts +++ b/packages/core/test/location.test.ts @@ -15,7 +15,6 @@ const projectLayer = Layer.succeed( list: () => Effect.succeed([]), update: () => Effect.die("not implemented"), activate: () => Effect.void, - initializeGit: () => Effect.die("not implemented"), resolve: () => Effect.succeed({ id: Project.ID.make("project"), diff --git a/packages/core/test/plugin/vcs-init.test.ts b/packages/core/test/plugin/vcs-init.test.ts new file mode 100644 index 00000000000..16477970d52 --- /dev/null +++ b/packages/core/test/plugin/vcs-init.test.ts @@ -0,0 +1,48 @@ +import { expect } from "bun:test" +import { Plugin } from "@opencode/plugin" +import type { VcsDefinition } from "@opencode/plugin/effect/vcs" +import { PluginPromise } from "@opencode/core/plugin/promise" +import { State } from "@opencode/core/state" +import { Effect } from "effect" +import { it } from "../lib/effect" +import { host } from "./host" + +it.live("Promise VCS initializer reaches the registered provider with its location scope", () => + Effect.gen(function* () { + const calls: string[] = [] + const state = State.create({ + initial: () => new Map(), + editor: (providers) => ({ + add: (definition: VcsDefinition) => providers.set(definition.id, definition), + default: { get: () => undefined, set: (_selection: string) => {} }, + }), + }) + const context = host() + const plugin = PluginPromise.fromPromise( + Plugin.define({ + id: "init-vcs", + async setup(ctx) { + await ctx.vcs.transform((editor) => + editor.add({ + id: "custom", + name: "Custom VCS", + init: async (scope, { signal }) => { + expect(signal.aborted).toBe(false) + calls.push(scope.worktree) + }, + info: async () => ({ branch: {} }), + branches: async () => [], + status: async () => [], + diff: async () => [], + }), + ) + }, + }), + ) + yield* plugin.effect(host({ vcs: { ...context.vcs, transform: state.transform, reload: state.reload } })) + const init = state.get().get("custom")?.init + if (!init) return yield* Effect.die("Initializer was not registered") + yield* init({ directory: "/workspace", worktree: "/workspace", canonical: "/workspace" }) + expect(calls).toEqual(["/workspace"]) + }), +) diff --git a/packages/core/test/vcs.test.ts b/packages/core/test/vcs.test.ts index ec791f2d614..8d52edac784 100644 --- a/packages/core/test/vcs.test.ts +++ b/packages/core/test/vcs.test.ts @@ -103,6 +103,29 @@ const provider = (input: Partial = {}) => }) satisfies VcsDefinition describe("Vcs", () => { + it.live("routes initialization through the requested provider before a repository exists", () => + withTmp((directory) => + Effect.gen(function* () { + const vcs = yield* Vcs.Service + const calls: string[] = [] + yield* vcs.transform((editor) => { + editor.add(provider({ id: "read-only" })) + editor.add( + provider({ + id: "custom", + init: (input) => Effect.sync(() => void calls.push(input.worktree)), + }), + ) + }) + expect(yield* vcs.initialize("missing").pipe(Effect.flip)).toMatchObject({ kind: "unsupported" }) + expect(yield* vcs.initialize("read-only").pipe(Effect.flip)).toMatchObject({ kind: "unsupported" }) + expect(calls).toEqual([]) + yield* vcs.initialize("custom") + expect(calls).toEqual([directory]) + }).pipe(provide(directory)), + ), + ) + it.live("returns empty results outside version control", () => withTmp((directory) => Effect.gen(function* () { diff --git a/packages/plugin/src/effect/vcs.ts b/packages/plugin/src/effect/vcs.ts index cea85ae8b22..34c108c5ebe 100644 --- a/packages/plugin/src/effect/vcs.ts +++ b/packages/plugin/src/effect/vcs.ts @@ -26,6 +26,7 @@ export interface VcsDiffInput extends VcsScope { export interface VcsDefinition { readonly id: string readonly name: string + readonly init?: (input: VcsScope) => Effect.Effect readonly info: (input: VcsScope) => Effect.Effect readonly base?: (input: VcsScope) => Effect.Effect readonly branches: (input: VcsBranchesInput) => Effect.Effect diff --git a/packages/plugin/src/promise/adapter.ts b/packages/plugin/src/promise/adapter.ts index 15ccb58a0d0..93dd4777769 100644 --- a/packages/plugin/src/promise/adapter.ts +++ b/packages/plugin/src/promise/adapter.ts @@ -517,10 +517,12 @@ export function fromPromise(plugin: Plugin) { host.vcs.transform((editor) => { callback({ add: (definition) => { + const init = definition.init?.bind(definition) const base = definition.base?.bind(definition) editor.add({ id: definition.id, name: definition.name, + init: init ? (input) => attempt((signal) => init(input, { signal })) : undefined, info: (input) => attempt((signal) => definition.info(input, { signal })), base: base ? (input) => attempt((signal) => base(input, { signal })) : undefined, branches: (input) => attempt((signal) => definition.branches(input, { signal })), diff --git a/packages/plugin/src/promise/vcs.ts b/packages/plugin/src/promise/vcs.ts index c139cb91af2..9ee82975bb3 100644 --- a/packages/plugin/src/promise/vcs.ts +++ b/packages/plugin/src/promise/vcs.ts @@ -25,6 +25,7 @@ export interface VcsDiffInput extends VcsScope { export interface VcsDefinition { readonly id: string readonly name: string + readonly init?: (input: VcsScope, context: { readonly signal: AbortSignal }) => Promise readonly info: (input: VcsScope, context: { readonly signal: AbortSignal }) => Promise readonly base?: (input: VcsScope, context: { readonly signal: AbortSignal }) => Promise readonly branches: (input: VcsBranchesInput, context: { readonly signal: AbortSignal }) => Promise diff --git a/packages/protocol/src/groups/vcs.ts b/packages/protocol/src/groups/vcs.ts index 113f559dd1b..03acee5d724 100644 --- a/packages/protocol/src/groups/vcs.ts +++ b/packages/protocol/src/groups/vcs.ts @@ -9,7 +9,7 @@ import { ConflictError, InvalidRequestError, ServiceUnavailableError } from "../ const InitQuery = Schema.Struct({ ...LocationQuery.fields, - provider: Schema.optional(Schema.Literal("git")), + provider: Schema.optional(Schema.String), }) const BranchesQuery = Schema.Struct({ @@ -36,9 +36,9 @@ export const VcsGroup = HttpApiGroup.make("server.vcs") .annotateMerge( OpenApi.annotations({ identifier: "vcs.init", - summary: "Initialize Git repository", + summary: "Initialize VCS repository", description: - "Initialize the selected repository type in a markerless project's directory and refresh its location services. Only git is supported; omitting provider defaults to git.", + "Initialize a repository using the selected VCS provider in a markerless project's directory and refresh its location services. Omitting provider defaults to git; built-in git and hg providers support initialization.", }), ), ) diff --git a/packages/server/src/handlers/vcs.ts b/packages/server/src/handlers/vcs.ts index ece76e4ed04..b5590b6cd66 100644 --- a/packages/server/src/handlers/vcs.ts +++ b/packages/server/src/handlers/vcs.ts @@ -1,5 +1,6 @@ import { Vcs } from "@opencode/core/vcs" import { Project } from "@opencode/core/project" +import { Plugin } from "@opencode/core/plugin" import { Location } from "@opencode/core/location" import { LocationServiceMap } from "@opencode/core/location-services" import { ConflictError, InvalidRequestError, ServiceUnavailableError } from "@opencode/protocol/errors" @@ -13,19 +14,29 @@ export const VcsHandler = HttpApiBuilder.group(Api, "server.vcs", (handlers) => const project = yield* Project.Service const locations = yield* LocationServiceMap.Service return handlers - .handle("vcs.init", () => + .handle("vcs.init", (ctx) => Effect.gen(function* () { const location = yield* Location.Service const directory = location.project.directory - yield* project.initializeGit(directory).pipe( + const providerID = ctx.query.provider ?? "git" + yield* Plugin.awaitActivation + const vcs = yield* Vcs.Service + yield* vcs.initialize(providerID).pipe( Effect.mapError((error) => { if (error.kind === "missing") return new InvalidRequestError({ message: "Project directory does not exist", field: "location" }) if (error.kind === "conflict") return new ConflictError({ message: "Project already has version control", resource: directory }) - return new ServiceUnavailableError({ service: "git", message: "Git initialization failed" }) + if (error.kind === "unsupported") + return new InvalidRequestError({ + message: "VCS provider does not support initialization", + field: "provider", + }) + return new ServiceUnavailableError({ service: providerID, message: "VCS initialization failed" }) }), ) + if (!(yield* project.resolve(directory)).vcs) + return yield* new ServiceUnavailableError({ service: providerID, message: "VCS initialization failed" }) yield* locations.invalidate( Location.Ref.make({ directory: location.directory, workspaceID: location.workspaceID }), ) diff --git a/packages/server/src/workerd.ts b/packages/server/src/workerd.ts index 949ee3fd4d0..43de238dfd4 100644 --- a/packages/server/src/workerd.ts +++ b/packages/server/src/workerd.ts @@ -96,6 +96,7 @@ const vcsLayer = Layer.succeed( Vcs.Service, Vcs.Service.of({ base: () => Effect.succeed(null), + initialize: () => Effect.fail(new Vcs.InitializeError({ kind: "unsupported" })), transform: () => Effect.succeed({ dispose: Effect.void }), reload: () => Effect.void, info: () => Effect.succeed({ branch: {} }), diff --git a/packages/server/test/vcs.test.ts b/packages/server/test/vcs.test.ts index 476c4195181..b10916ce1ce 100644 --- a/packages/server/test/vcs.test.ts +++ b/packages/server/test/vcs.test.ts @@ -1,6 +1,6 @@ import path from "node:path" import { $ } from "bun" -import { expect } from "bun:test" +import { describe, expect } from "bun:test" import { SdkPlugins } from "@opencode/core/plugin/sdk" import { Effect, Layer, Schedule } from "effect" import { tmpdir } from "../../core/test/fixture/tmpdir" @@ -88,10 +88,11 @@ it.live( const server = yield* startServer(path.join(tmp.path, "config")) const url = new URL("/api/vcs/init", server.base) url.searchParams.set("location[directory]", tmp.path) - url.searchParams.set("provider", "hg") + url.searchParams.set("provider", "unknown") const unsupported = yield* Effect.promise(() => fetch(url, { method: "POST", headers: server.headers })) expect(unsupported.status).toBe(400) expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, ".git", "HEAD")).exists())).toBe(false) + expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, ".hg", "requires")).exists())).toBe(false) url.searchParams.set("provider", "git") const initialized = yield* Effect.promise(() => fetch(url, { method: "POST", headers: server.headers })) expect(initialized.status).toBe(204) @@ -100,6 +101,51 @@ it.live( 15_000, ) +const describeHg = Bun.which("hg") ? describe : describe.skip + +describeHg("Mercurial initialization", () => { + it.live( + "initializes and serves an untracked-file diff through the Hg provider", + () => + Effect.gen(function* () { + const tmp = yield* Effect.acquireDisposable(Effect.promise(() => tmpdir("opencode-vcs-hg-init-"))) + yield* Effect.promise(() => Bun.write(path.join(tmp.path, "hello.txt"), "hello\n")) + const server = yield* startServer(path.join(tmp.path, "config")) + const url = new URL("/api/vcs/init", server.base) + url.searchParams.set("location[directory]", tmp.path) + url.searchParams.set("provider", "hg") + const initialized = yield* Effect.promise(() => fetch(url, { method: "POST", headers: server.headers })) + expect(initialized.status).toBe(204) + expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, ".hg", "requires")).exists())).toBe(true) + url.pathname = "/api/vcs" + const info = yield* Effect.tryPromise({ + try: async () => { + const response = await fetch(url, { headers: server.headers }) + const body: unknown = await response.json() + if (!isRecord(body) || !isRecord(body.data) || body.data.provider !== "hg") + throw new Error("Mercurial provider not ready") + return body + }, + catch: (cause) => cause, + }).pipe(Effect.retry(Schedule.spaced("10 millis")), Effect.timeout("2 seconds")) + expect(info).toMatchObject({ data: { provider: "hg" } }) + url.pathname = "/api/vcs/diff" + url.searchParams.set("mode", "working") + const diff = yield* Effect.promise(() => fetch(url, { headers: server.headers })) + expect(diff.status).toBe(200) + expect(yield* Effect.promise(() => diff.json())).toMatchObject({ + data: expect.arrayContaining([expect.objectContaining({ file: "hello.txt" })]), + }) + url.pathname = "/api/vcs/init" + url.searchParams.delete("mode") + url.searchParams.set("provider", "git") + const repeated = yield* Effect.promise(() => fetch(url, { method: "POST", headers: server.headers })) + expect(repeated.status).toBe(409) + }), + 15_000, + ) +}) + it.live( "serves lazy review bases, committed diffs, and unavailable-base errors", () => diff --git a/services/www/src/docs/content/build/plugins/effect.mdx b/services/www/src/docs/content/build/plugins/effect.mdx index e20d15d8320..6f7031c7bd6 100644 --- a/services/www/src/docs/content/build/plugins/effect.mdx +++ b/services/www/src/docs/content/build/plugins/effect.mdx @@ -1061,6 +1061,11 @@ Provider callbacks receive the current location, working-copy root, canonical pr store. Diff callbacks also receive the selected mode, requested context, and output byte budget. Repository discovery continues to use OpenCode's built-in Git and Mercurial detectors. +Providers can optionally implement `init(scope)` to create a repository in `scope.worktree`. Initialization selects +the provider by ID (Git by default), not by the detected repository type. Register a provider with `init` even when +the location has no repository yet; OpenCode checks that the directory exists and has no repository before calling +it, then rediscovers the project. New repository formats still need discovery support beyond this callback. + Schemas: [`Vcs.Info`](/api#schema-Vcs.Info), [`Vcs.FileStatus`](/api#schema-Vcs.FileStatus), [`FileDiff.Info`](/api#schema-FileDiff.Info). @@ -1073,10 +1078,15 @@ interface VcsEditor { } } -interface VcsDomain extends VcsApi { +interface VcsDomain extends Omit, "init"> { readonly transform: Transform readonly reload: () => Effect.Effect } + +interface VcsDefinition { + readonly init?: (scope: VcsScope) => Effect.Effect + // Existing info, branches, status, and diff callbacks are required. +} ``` ### Websearch diff --git a/services/www/src/docs/content/build/plugins/index.mdx b/services/www/src/docs/content/build/plugins/index.mdx index fa81169d4bc..31aa7cbff00 100644 --- a/services/www/src/docs/content/build/plugins/index.mdx +++ b/services/www/src/docs/content/build/plugins/index.mdx @@ -1029,6 +1029,11 @@ Provider callbacks receive the current location, working-copy root, canonical pr store. Diff callbacks also receive the selected mode, requested context, and output byte budget. Repository discovery continues to use OpenCode's built-in Git and Mercurial detectors. +Providers can optionally implement `init(scope, { signal })` to create a repository in `scope.worktree`. Initialization +selects the provider by ID (Git by default), not by the detected repository type. Register a provider with `init` even +when the location has no repository yet; OpenCode checks that the directory exists and has no repository before calling +it, then rediscovers the project. New repository formats still need discovery support beyond this callback. + #### Reference Schemas: [`Vcs.Info`](/api#schema-Vcs.Info), [`Vcs.FileStatus`](/api#schema-Vcs.FileStatus), @@ -1051,6 +1056,11 @@ interface VcsEditor { set(providerID: string): void } } + +interface VcsDefinition { + init?(scope: VcsScope, context: { signal: AbortSignal }): Promise + // Existing info, branches, status, and diff callbacks are required. +} ``` ### Worktrees