diff --git a/packages/opencode/src/server/routes/instance/httpapi/api.ts b/packages/opencode/src/server/routes/instance/httpapi/api.ts
index 11649d11f8..57b8b37d99 100644
--- a/packages/opencode/src/server/routes/instance/httpapi/api.ts
+++ b/packages/opencode/src/server/routes/instance/httpapi/api.ts
@@ -12,6 +12,7 @@ import { InstanceApi } from "./groups/instance"
import { McpApi } from "./groups/mcp"
import { PermissionApi } from "./groups/permission"
import { ProjectApi } from "./groups/project"
+import { ProjectCopyApi } from "./groups/project-copy"
import { ProviderApi } from "./groups/provider"
import { PtyApi, PtyConnectApi } from "./groups/pty"
import { QuestionApi } from "./groups/question"
@@ -52,6 +53,7 @@ export const InstanceHttpApi = HttpApi.make("opencode-instance")
.addHttpApi(InstanceApi)
.addHttpApi(McpApi)
.addHttpApi(ProjectApi)
+ .addHttpApi(ProjectCopyApi)
.addHttpApi(PtyApi)
.addHttpApi(QuestionApi)
.addHttpApi(PermissionApi)
diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/project-copy.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/project-copy.ts
new file mode 100644
index 0000000000..f196d26f60
--- /dev/null
+++ b/packages/opencode/src/server/routes/instance/httpapi/groups/project-copy.ts
@@ -0,0 +1,83 @@
+import { ProjectCopy } from "@opencode-ai/core/project/copy"
+import { ProjectV2 } from "@opencode-ai/core/project"
+import { Schema } from "effect"
+import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
+import { Authorization } from "../middleware/authorization"
+import { InstanceContextMiddleware } from "../middleware/instance-context"
+import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
+import { described } from "./metadata"
+
+const root = "/project/:projectID/copy"
+
+export const CreatePayload = Schema.Struct({
+ strategy: ProjectCopy.StrategyID,
+ path: ProjectCopy.CreateInput.fields.path,
+})
+export const RemovePayload = Schema.Struct({
+ strategy: ProjectCopy.StrategyID,
+ path: ProjectCopy.RemoveInput.fields.path,
+})
+export const RefreshPayload = Schema.Struct({
+ strategy: Schema.optional(ProjectCopy.StrategyID),
+})
+
+export const ProjectCopyApi = HttpApi.make("projectCopy")
+ .add(
+ HttpApiGroup.make("projectCopy")
+ .add(
+ HttpApiEndpoint.get("strategies", `${root}/strategy`, {
+ params: { projectID: ProjectV2.ID },
+ query: WorkspaceRoutingQuery,
+ success: described(Schema.Array(ProjectCopy.StrategyInfo), "Project copy strategies"),
+ }).annotateMerge(
+ OpenApi.annotations({
+ identifier: "projectCopy.strategies",
+ summary: "List project copy strategies",
+ description: "List mechanisms available for managing local copies of a project.",
+ }),
+ ),
+ HttpApiEndpoint.post("create", root, {
+ params: { projectID: ProjectV2.ID },
+ query: WorkspaceRoutingQuery,
+ payload: CreatePayload,
+ success: described(ProjectCopy.Copy, "Project copy created"),
+ error: HttpApiError.BadRequest,
+ }).annotateMerge(
+ OpenApi.annotations({
+ identifier: "projectCopy.create",
+ summary: "Create project copy",
+ description: "Create a local physical copy of a project using the selected strategy.",
+ }),
+ ),
+ HttpApiEndpoint.delete("remove", root, {
+ params: { projectID: ProjectV2.ID },
+ query: WorkspaceRoutingQuery,
+ payload: RemovePayload,
+ success: described(HttpApiSchema.NoContent, "Project copy removed"),
+ error: HttpApiError.BadRequest,
+ }).annotateMerge(
+ OpenApi.annotations({
+ identifier: "projectCopy.remove",
+ summary: "Remove project copy",
+ description: "Remove a local physical copy of a project using the selected strategy.",
+ }),
+ ),
+ HttpApiEndpoint.post("refresh", `${root}/refresh`, {
+ params: { projectID: ProjectV2.ID },
+ query: WorkspaceRoutingQuery,
+ payload: RefreshPayload,
+ success: described(HttpApiSchema.NoContent, "Project copies refreshed"),
+ error: HttpApiError.BadRequest,
+ }).annotateMerge(
+ OpenApi.annotations({
+ identifier: "projectCopy.refresh",
+ summary: "Refresh project copies",
+ description: "Discover local project copies using one or all configured strategies.",
+ }),
+ ),
+ )
+ .annotateMerge(OpenApi.annotations({ title: "projectCopy", description: "Project copy management routes." }))
+ .middleware(InstanceContextMiddleware)
+ .middleware(WorkspaceRoutingMiddleware)
+ .middleware(Authorization),
+ )
diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/project.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/project.ts
index c6b2fab40a..35dc6ae8a5 100644
--- a/packages/opencode/src/server/routes/instance/httpapi/groups/project.ts
+++ b/packages/opencode/src/server/routes/instance/httpapi/groups/project.ts
@@ -62,6 +62,17 @@ export const ProjectApi = HttpApi.make("project")
description: "Update project properties such as name, icon, and commands.",
}),
),
+ HttpApiEndpoint.get("paths", `${root}/:projectID/paths`, {
+ params: { projectID: ProjectV2.ID },
+ query: WorkspaceRoutingQuery,
+ success: described(ProjectV2.Paths, "Project paths"),
+ }).annotateMerge(
+ OpenApi.annotations({
+ identifier: "project.paths",
+ summary: "List project paths",
+ description: "List known local absolute paths for a project.",
+ }),
+ ),
)
.annotateMerge(
OpenApi.annotations({
diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/project-copy.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/project-copy.ts
new file mode 100644
index 0000000000..45878be287
--- /dev/null
+++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/project-copy.ts
@@ -0,0 +1,47 @@
+import { ProjectCopy } from "@opencode-ai/core/project/copy"
+import { ProjectV2 } from "@opencode-ai/core/project"
+import { Effect } from "effect"
+import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi"
+import { InstanceHttpApi } from "../api"
+import { CreatePayload, RefreshPayload, RemovePayload } from "../groups/project-copy"
+
+function badRequest(effect: Effect.Effect) {
+ return effect.pipe(Effect.mapError(() => new HttpApiError.BadRequest({})))
+}
+
+export const projectCopyHandlers = HttpApiBuilder.group(InstanceHttpApi, "projectCopy", (handlers) =>
+ Effect.gen(function* () {
+ const service = yield* ProjectCopy.Service
+
+ const strategies = Effect.fn("ProjectCopyHttpApi.strategies")((ctx: { params: { projectID: ProjectV2.ID } }) =>
+ service.strategies({ projectID: ctx.params.projectID }),
+ )
+
+ const create = Effect.fn("ProjectCopyHttpApi.create")(function* (ctx: {
+ params: { projectID: ProjectV2.ID }
+ payload: typeof CreatePayload.Type
+ }) {
+ return yield* badRequest(service.create({ ...ctx.payload, projectID: ctx.params.projectID }))
+ })
+
+ const remove = Effect.fn("ProjectCopyHttpApi.remove")(function* (ctx: {
+ params: { projectID: ProjectV2.ID }
+ payload: typeof RemovePayload.Type
+ }) {
+ yield* badRequest(service.remove({ ...ctx.payload, projectID: ctx.params.projectID }))
+ })
+
+ const refresh = Effect.fn("ProjectCopyHttpApi.refresh")(function* (ctx: {
+ params: { projectID: ProjectV2.ID }
+ payload: typeof RefreshPayload.Type
+ }) {
+ yield* badRequest(service.refresh({ ...ctx.payload, projectID: ctx.params.projectID }))
+ })
+
+ return handlers
+ .handle("strategies", strategies)
+ .handle("create", create)
+ .handle("remove", remove)
+ .handle("refresh", refresh)
+ }),
+)
diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/project.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/project.ts
index 8b4fc608fb..fc66da52f8 100644
--- a/packages/opencode/src/server/routes/instance/httpapi/handlers/project.ts
+++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/project.ts
@@ -10,6 +10,7 @@ import { markInstanceForReload } from "../lifecycle"
export const projectHandlers = HttpApiBuilder.group(InstanceHttpApi, "project", (handlers) =>
Effect.gen(function* () {
const svc = yield* Project.Service
+ const project = yield* ProjectV2.Service
const list = Effect.fn("ProjectHttpApi.list")(function* () {
return yield* svc.list()
@@ -48,6 +49,15 @@ export const projectHandlers = HttpApiBuilder.group(InstanceHttpApi, "project",
)
})
- return handlers.handle("list", list).handle("current", current).handle("initGit", initGit).handle("update", update)
+ const paths = Effect.fn("ProjectHttpApi.paths")((ctx: { params: { projectID: ProjectV2.ID } }) =>
+ project.paths({ projectID: ctx.params.projectID }),
+ )
+
+ return handlers
+ .handle("list", list)
+ .handle("current", current)
+ .handle("initGit", initGit)
+ .handle("update", update)
+ .handle("paths", paths)
}),
)
diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts
index 12761ab7ff..89acb7bf6b 100644
--- a/packages/opencode/src/server/routes/instance/httpapi/server.ts
+++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts
@@ -28,6 +28,8 @@ import { Installation } from "@/installation"
import { InstanceLayer } from "@/project/instance-layer"
import { Plugin } from "@/plugin"
import { Project } from "@/project/project"
+import { ProjectV2 } from "@opencode-ai/core/project"
+import { ProjectCopy } from "@opencode-ai/core/project/copy"
import { ProviderAuth } from "@/provider/auth"
import { ModelsDev } from "@opencode-ai/core/models-dev"
import { Provider } from "@/provider/provider"
@@ -76,6 +78,7 @@ import { instanceHandlers } from "./handlers/instance"
import { mcpHandlers } from "./handlers/mcp"
import { permissionHandlers } from "./handlers/permission"
import { projectHandlers } from "./handlers/project"
+import { projectCopyHandlers } from "./handlers/project-copy"
import { providerHandlers } from "./handlers/provider"
import { ptyConnectHandlers, ptyHandlers } from "./handlers/pty"
import { questionHandlers } from "./handlers/question"
@@ -137,6 +140,7 @@ const instanceApiRoutes = HttpApiBuilder.layer(InstanceHttpApi).pipe(
instanceHandlers,
mcpHandlers,
projectHandlers,
+ projectCopyHandlers,
ptyHandlers,
questionHandlers,
permissionHandlers,
@@ -208,6 +212,8 @@ export function createRoutes(
Permission.defaultLayer,
Plugin.defaultLayer,
Project.defaultLayer,
+ ProjectV2.defaultLayer,
+ ProjectCopy.defaultLayer,
ProviderAuth.defaultLayer,
Provider.defaultLayer,
Pty.defaultLayer,
diff --git a/packages/opencode/test/server/project-copy.test.ts b/packages/opencode/test/server/project-copy.test.ts
new file mode 100644
index 0000000000..769469cc2f
--- /dev/null
+++ b/packages/opencode/test/server/project-copy.test.ts
@@ -0,0 +1,93 @@
+import { afterEach, describe, expect } from "bun:test"
+import { $ } from "bun"
+import fs from "fs/promises"
+import path from "path"
+import { Effect, Layer } from "effect"
+import { HttpClientResponse } from "effect/unstable/http"
+import { AppFileSystem } from "@opencode-ai/core/filesystem"
+import { Snapshot } from "@/snapshot"
+import { InstanceBootstrap } from "@/project/bootstrap-service"
+import { InstanceStore } from "@/project/instance-store"
+import { resetDatabase } from "../fixture/db"
+import { disposeAllInstances, TestInstance } from "../fixture/fixture"
+import { testEffect } from "../lib/effect"
+import { httpApiLayer, requestInDirectory } from "./httpapi-layer"
+
+afterEach(async () => {
+ await disposeAllInstances()
+ await resetDatabase()
+})
+
+const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
+const testInstanceStore = InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap))
+const it = testEffect(Layer.mergeAll(AppFileSystem.defaultLayer, Snapshot.defaultLayer, testInstanceStore, httpApiLayer))
+
+function request(directory: string, url: string, init: RequestInit = {}) {
+ return requestInDirectory(url, directory, init)
+}
+
+function json(response: HttpClientResponse.HttpClientResponse) {
+ return response.json.pipe(Effect.map((value) => value as T))
+}
+
+describe("project paths and copies endpoints", () => {
+ it.instance(
+ "lists paths and manages git worktree copies",
+ () =>
+ Effect.gen(function* () {
+ const test = yield* TestInstance
+ const current = yield* request(test.directory, "/project/current")
+ const base = `/project/${(yield* json<{ id: string }>(current)).id}`
+ const createdPath = path.join(test.directory, "..", path.basename(test.directory) + "-http-copy")
+ yield* Effect.addFinalizer(() =>
+ Effect.promise(() => fs.rm(createdPath, { recursive: true, force: true })).pipe(Effect.ignore),
+ )
+
+ const strategies = yield* request(test.directory, `${base}/copy/strategy`)
+ expect(strategies.status).toBe(200)
+ expect(yield* json(strategies)).toEqual([{ id: "git_worktree", name: "Git worktree" }])
+
+ const initial = yield* request(test.directory, `${base}/paths`)
+ expect(initial.status).toBe(200)
+ expect(yield* json>(initial)).toEqual([
+ { path: test.directory, primary: true },
+ ])
+
+ const create = yield* request(test.directory, `${base}/copy`, {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({ strategy: "git_worktree", path: createdPath }),
+ })
+ expect(create.status).toBe(200)
+ const created = yield* json<{ path: string }>(create)
+ expect(created.path).toContain("-http-copy")
+
+ const listed = yield* request(test.directory, `${base}/paths`)
+ expect((yield* json>(listed)).map((item) => item.path)).toContain(
+ created.path,
+ )
+
+ const remove = yield* request(test.directory, `${base}/copy`, {
+ method: "DELETE",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({ strategy: "git_worktree", path: created.path }),
+ })
+ expect(remove.status).toBe(204)
+
+ const externalPath = path.join(test.directory, "..", path.basename(test.directory) + "-http-refresh")
+ yield* Effect.addFinalizer(() =>
+ Effect.promise(() => fs.rm(externalPath, { recursive: true, force: true })).pipe(Effect.ignore),
+ )
+ yield* Effect.promise(() => $`git worktree add --detach ${externalPath} HEAD`.cwd(test.directory).quiet())
+ const refresh = yield* request(test.directory, `${base}/copy/refresh`, {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({ strategy: "git_worktree" }),
+ })
+ expect(refresh.status).toBe(204)
+ const refreshed = yield* request(test.directory, `${base}/paths`)
+ expect((yield* json>(refreshed)).length).toBe(2)
+ }),
+ { git: true },
+ )
+})
diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts
index be1e4abc65..4b7a8ec927 100644
--- a/packages/sdk/js/src/v2/gen/sdk.gen.ts
+++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts
@@ -117,12 +117,22 @@ import type {
PermissionRespondErrors,
PermissionRespondResponses,
PermissionRuleset,
+ ProjectCopyCreateErrors,
+ ProjectCopyCreateResponses,
+ ProjectCopyRefreshErrors,
+ ProjectCopyRefreshResponses,
+ ProjectCopyRemoveErrors,
+ ProjectCopyRemoveResponses,
+ ProjectCopyStrategiesErrors,
+ ProjectCopyStrategiesResponses,
ProjectCurrentErrors,
ProjectCurrentResponses,
ProjectInitGitErrors,
ProjectInitGitResponses,
ProjectListErrors,
ProjectListResponses,
+ ProjectPathsErrors,
+ ProjectPathsResponses,
ProjectUpdateErrors,
ProjectUpdateResponses,
Prompt,
@@ -2373,6 +2383,197 @@ export class Project extends HeyApiClient {
},
})
}
+
+ /**
+ * List project paths
+ *
+ * List known local absolute paths for a project.
+ */
+ public paths(
+ parameters: {
+ projectID: string
+ directory?: string
+ workspace?: string
+ },
+ options?: Options,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "path", key: "projectID" },
+ { in: "query", key: "directory" },
+ { in: "query", key: "workspace" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).get({
+ url: "/project/{projectID}/paths",
+ ...options,
+ ...params,
+ })
+ }
+}
+
+export class ProjectCopy extends HeyApiClient {
+ /**
+ * List project copy strategies
+ *
+ * List mechanisms available for managing local copies of a project.
+ */
+ public strategies(
+ parameters: {
+ projectID: string
+ directory?: string
+ workspace?: string
+ },
+ options?: Options,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "path", key: "projectID" },
+ { in: "query", key: "directory" },
+ { in: "query", key: "workspace" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).get<
+ ProjectCopyStrategiesResponses,
+ ProjectCopyStrategiesErrors,
+ ThrowOnError
+ >({
+ url: "/project/{projectID}/copy/strategy",
+ ...options,
+ ...params,
+ })
+ }
+
+ /**
+ * Remove project copy
+ *
+ * Remove a local physical copy of a project using the selected strategy.
+ */
+ public remove(
+ parameters: {
+ projectID: string
+ directory?: string
+ workspace?: string
+ strategy?: "git_worktree"
+ path?: string
+ },
+ options?: Options,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "path", key: "projectID" },
+ { in: "query", key: "directory" },
+ { in: "query", key: "workspace" },
+ { in: "body", key: "strategy" },
+ { in: "body", key: "path" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).delete({
+ url: "/project/{projectID}/copy",
+ ...options,
+ ...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
+ },
+ })
+ }
+
+ /**
+ * Create project copy
+ *
+ * Create a local physical copy of a project using the selected strategy.
+ */
+ public create(
+ parameters: {
+ projectID: string
+ directory?: string
+ workspace?: string
+ strategy?: "git_worktree"
+ path?: string
+ },
+ options?: Options,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "path", key: "projectID" },
+ { in: "query", key: "directory" },
+ { in: "query", key: "workspace" },
+ { in: "body", key: "strategy" },
+ { in: "body", key: "path" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).post({
+ url: "/project/{projectID}/copy",
+ ...options,
+ ...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
+ },
+ })
+ }
+
+ /**
+ * Refresh project copies
+ *
+ * Discover local project copies using one or all configured strategies.
+ */
+ public refresh(
+ parameters: {
+ projectID: string
+ directory?: string
+ workspace?: string
+ strategy?: "git_worktree"
+ },
+ options?: Options,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "path", key: "projectID" },
+ { in: "query", key: "directory" },
+ { in: "query", key: "workspace" },
+ { in: "body", key: "strategy" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).post({
+ url: "/project/{projectID}/copy/refresh",
+ ...options,
+ ...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
+ },
+ })
+ }
}
export class Pty extends HeyApiClient {
@@ -5119,6 +5320,11 @@ export class OpencodeClient extends HeyApiClient {
return (this._project ??= new Project({ client: this.client }))
}
+ private _projectCopy?: ProjectCopy
+ get projectCopy(): ProjectCopy {
+ return (this._projectCopy ??= new ProjectCopy({ client: this.client }))
+ }
+
private _pty?: Pty
get pty(): Pty {
return (this._pty ??= new Pty({ client: this.client }))
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index 3be97a5cf9..d2cf5372e6 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -76,6 +76,7 @@ export type Event =
| EventPtyDeleted
| EventInstallationUpdated
| EventInstallationUpdateAvailable
+ | EventProjectPathsUpdated
| EventServerConnected
| EventGlobalDisposed
| EventAccountAdded
@@ -1401,6 +1402,13 @@ export type GlobalEvent = {
version: string
}
}
+ | {
+ id: string
+ type: "project.paths.updated"
+ properties: {
+ projectID: string
+ }
+ }
| {
id: string
type: "server.connected"
@@ -3335,6 +3343,20 @@ export type ConfigV2ExperimentalPolicy = {
resource: string
}
+export type ProjectPaths = Array<{
+ path: string
+ primary: boolean
+}>
+
+export type ProjectCopyStrategyInfo = {
+ id: "git_worktree"
+ name: string
+}
+
+export type ProjectCopyCopy = {
+ path: string
+}
+
export type SessionInfo = {
id: string
parentID?: string
@@ -4457,6 +4479,14 @@ export type EventInstallationUpdateAvailable = {
}
}
+export type EventProjectPathsUpdated = {
+ id: string
+ type: "project.paths.updated"
+ properties: {
+ projectID: string
+ }
+}
+
export type EventServerConnected = {
id: string
type: "server.connected"
@@ -6153,6 +6183,164 @@ export type ProjectUpdateResponses = {
export type ProjectUpdateResponse = ProjectUpdateResponses[keyof ProjectUpdateResponses]
+export type ProjectPathsData = {
+ body?: never
+ path: {
+ projectID: string
+ }
+ query?: {
+ directory?: string
+ workspace?: string
+ }
+ url: "/project/{projectID}/paths"
+}
+
+export type ProjectPathsErrors = {
+ /**
+ * Bad request
+ */
+ 400: BadRequestError
+}
+
+export type ProjectPathsError = ProjectPathsErrors[keyof ProjectPathsErrors]
+
+export type ProjectPathsResponses = {
+ /**
+ * Project paths
+ */
+ 200: ProjectPaths
+}
+
+export type ProjectPathsResponse = ProjectPathsResponses[keyof ProjectPathsResponses]
+
+export type ProjectCopyStrategiesData = {
+ body?: never
+ path: {
+ projectID: string
+ }
+ query?: {
+ directory?: string
+ workspace?: string
+ }
+ url: "/project/{projectID}/copy/strategy"
+}
+
+export type ProjectCopyStrategiesErrors = {
+ /**
+ * Bad request
+ */
+ 400: BadRequestError
+}
+
+export type ProjectCopyStrategiesError = ProjectCopyStrategiesErrors[keyof ProjectCopyStrategiesErrors]
+
+export type ProjectCopyStrategiesResponses = {
+ /**
+ * Project copy strategies
+ */
+ 200: Array
+}
+
+export type ProjectCopyStrategiesResponse = ProjectCopyStrategiesResponses[keyof ProjectCopyStrategiesResponses]
+
+export type ProjectCopyRemoveData = {
+ body?: {
+ strategy: "git_worktree"
+ path: string
+ }
+ path: {
+ projectID: string
+ }
+ query?: {
+ directory?: string
+ workspace?: string
+ }
+ url: "/project/{projectID}/copy"
+}
+
+export type ProjectCopyRemoveErrors = {
+ /**
+ * BadRequest | InvalidRequestError
+ */
+ 400: EffectHttpApiErrorBadRequest | InvalidRequestError
+}
+
+export type ProjectCopyRemoveError = ProjectCopyRemoveErrors[keyof ProjectCopyRemoveErrors]
+
+export type ProjectCopyRemoveResponses = {
+ /**
+ * Project copy removed
+ */
+ 204: void
+}
+
+export type ProjectCopyRemoveResponse = ProjectCopyRemoveResponses[keyof ProjectCopyRemoveResponses]
+
+export type ProjectCopyCreateData = {
+ body?: {
+ strategy: "git_worktree"
+ path: string
+ }
+ path: {
+ projectID: string
+ }
+ query?: {
+ directory?: string
+ workspace?: string
+ }
+ url: "/project/{projectID}/copy"
+}
+
+export type ProjectCopyCreateErrors = {
+ /**
+ * BadRequest | InvalidRequestError
+ */
+ 400: EffectHttpApiErrorBadRequest | InvalidRequestError
+}
+
+export type ProjectCopyCreateError = ProjectCopyCreateErrors[keyof ProjectCopyCreateErrors]
+
+export type ProjectCopyCreateResponses = {
+ /**
+ * Project copy created
+ */
+ 200: ProjectCopyCopy
+}
+
+export type ProjectCopyCreateResponse = ProjectCopyCreateResponses[keyof ProjectCopyCreateResponses]
+
+export type ProjectCopyRefreshData = {
+ body?: {
+ strategy?: "git_worktree"
+ }
+ path: {
+ projectID: string
+ }
+ query?: {
+ directory?: string
+ workspace?: string
+ }
+ url: "/project/{projectID}/copy/refresh"
+}
+
+export type ProjectCopyRefreshErrors = {
+ /**
+ * BadRequest | InvalidRequestError
+ */
+ 400: EffectHttpApiErrorBadRequest | InvalidRequestError
+}
+
+export type ProjectCopyRefreshError = ProjectCopyRefreshErrors[keyof ProjectCopyRefreshErrors]
+
+export type ProjectCopyRefreshResponses = {
+ /**
+ * Project copies refreshed
+ */
+ 204: void
+}
+
+export type ProjectCopyRefreshResponse = ProjectCopyRefreshResponses[keyof ProjectCopyRefreshResponses]
+
export type PtyShellsData = {
body?: never
path?: never