From decb1668580cb0b63c4b73f4c14bcaf289773046 Mon Sep 17 00:00:00 2001 From: James Long Date: Sun, 31 May 2026 15:58:58 -0400 Subject: [PATCH] add endpoints --- .../src/server/routes/instance/httpapi/api.ts | 2 + .../instance/httpapi/groups/project-copy.ts | 83 +++++++ .../routes/instance/httpapi/groups/project.ts | 11 + .../instance/httpapi/handlers/project-copy.ts | 47 ++++ .../instance/httpapi/handlers/project.ts | 12 +- .../server/routes/instance/httpapi/server.ts | 6 + .../opencode/test/server/project-copy.test.ts | 93 ++++++++ packages/sdk/js/src/v2/gen/sdk.gen.ts | 206 ++++++++++++++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 188 ++++++++++++++++ 9 files changed, 647 insertions(+), 1 deletion(-) create mode 100644 packages/opencode/src/server/routes/instance/httpapi/groups/project-copy.ts create mode 100644 packages/opencode/src/server/routes/instance/httpapi/handlers/project-copy.ts create mode 100644 packages/opencode/test/server/project-copy.test.ts 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