Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/opencode/src/project/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export interface Interface {
readonly list: () => Effect.Effect<Info[]>
readonly get: (id: ProjectV2.ID) => Effect.Effect<Info | undefined>
readonly update: (input: UpdateInput) => Effect.Effect<Info, NotFoundError>
readonly remove: (id: ProjectV2.ID) => Effect.Effect<void, NotFoundError>
readonly initGit: (input: { directory: string; project: Info }) => Effect.Effect<Info>
readonly setInitialized: (id: ProjectV2.ID) => Effect.Effect<void>
readonly sandboxes: (id: ProjectV2.ID) => Effect.Effect<string[]>
Expand Down Expand Up @@ -363,6 +364,12 @@ const layer = Layer.effect(
return data
})

// Dependent rows (sessions, directories, permissions, workspaces) are removed by ON DELETE CASCADE.
const remove = Effect.fn("Project.remove")(function* (id: ProjectV2.ID) {
const result = yield* db.delete(ProjectTable).where(eq(ProjectTable.id, id)).returning().get().pipe(Effect.orDie)
if (!result) return yield* new NotFoundError({ projectID: id })
})

const initGit = Effect.fn("Project.initGit")(function* (input: { directory: string; project: Info }) {
if (input.project.vcs === "git") return input.project
if (!(yield* Effect.sync(() => which("git")))) throw new Error("Git is not installed")
Expand Down Expand Up @@ -454,6 +461,7 @@ const layer = Layer.effect(
list,
get,
update,
remove,
initGit,
setInitialized,
sandboxes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ export const ProjectApi = HttpApi.make("project")
description: "Update project properties such as name, icon, and commands.",
}),
),
HttpApiEndpoint.delete("remove", `${root}/:projectID`, {
params: { projectID: ProjectV2.ID },
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Project removed"),
error: [ProjectNotFoundError],
}).annotateMerge(
OpenApi.annotations({
identifier: "project.remove",
summary: "Remove project",
description: "Remove a project and its sessions from OpenCode. Files on disk are not touched.",
}),
),
HttpApiEndpoint.get("directories", `${root}/:projectID/directories`, {
params: { projectID: ProjectV2.ID },
query: WorkspaceRoutingQuery,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@ export const projectHandlers = HttpApiBuilder.group(InstanceHttpApi, "project",
)
})

const remove = Effect.fn("ProjectHttpApi.remove")(function* (ctx: { params: { projectID: ProjectV2.ID } }) {
yield* svc.remove(ctx.params.projectID).pipe(
Effect.catchTag("Project.NotFoundError", (error) =>
Effect.fail(
new ProjectNotFoundError({
projectID: error.projectID,
message: `Project not found: ${error.projectID}`,
}),
),
),
)
return true
})

const directories = Effect.fn("ProjectHttpApi.directories")((ctx: { params: { projectID: ProjectV2.ID } }) =>
project.directories({ projectID: ctx.params.projectID }),
)
Expand All @@ -58,6 +72,7 @@ export const projectHandlers = HttpApiBuilder.group(InstanceHttpApi, "project",
.handle("current", current)
.handle("initGit", initGit)
.handle("update", update)
.handle("remove", remove)
.handle("directories", directories)
}),
)
22 changes: 22 additions & 0 deletions packages/opencode/test/project/project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,28 @@ describe("Project.setInitialized", () => {
)
})

describe("Project.remove", () => {
it.live("removes the project", () =>
Effect.gen(function* () {
const project = yield* Project.Service
const tmp = yield* tmpdirScoped({ git: true })
const result = yield* project.fromDirectory(tmp)

yield* project.remove(result.project.id)

expect(yield* project.get(result.project.id)).toBeUndefined()
}),
)

it.live("fails with NotFoundError for an unknown project", () =>
Effect.gen(function* () {
const project = yield* Project.Service
const error = yield* project.remove(ProjectV2.ID.make("missing")).pipe(Effect.flip)
expect(error._tag).toBe("Project.NotFoundError")
}),
)
})

describe("Project.addSandbox and Project.removeSandbox", () => {
it.live("addSandbox adds directory and removeSandbox removes it", () =>
Effect.gen(function* () {
Expand Down
34 changes: 34 additions & 0 deletions packages/sdk/js/src/v2/gen/sdk.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ import type {
ProjectInitGitResponses,
ProjectListErrors,
ProjectListResponses,
ProjectRemoveErrors,
ProjectRemoveResponses,
ProjectUpdateErrors,
ProjectUpdateResponses,
PromptInput,
Expand Down Expand Up @@ -2618,6 +2620,38 @@ export class Project extends HeyApiClient {
})
}

/**
* Remove project
*
* Remove a project and its sessions from OpenCode. Files on disk are not touched.
*/
public remove<ThrowOnError extends boolean = false>(
parameters: {
projectID: string
directory?: string
workspace?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "projectID" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
return (options?.client ?? this.client).delete<ProjectRemoveResponses, ProjectRemoveErrors, ThrowOnError>({
url: "/project/{projectID}",
...options,
...params,
})
}

/**
* Update project
*
Expand Down
34 changes: 34 additions & 0 deletions packages/sdk/js/src/v2/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8789,6 +8789,40 @@ export type ProjectInitGitResponses = {

export type ProjectInitGitResponse = ProjectInitGitResponses[keyof ProjectInitGitResponses]

export type ProjectRemoveData = {
body?: never
path: {
projectID: string
}
query?: {
directory?: string
workspace?: string
}
url: "/project/{projectID}"
}

export type ProjectRemoveErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* ProjectNotFoundError
*/
404: ProjectNotFoundError
}

export type ProjectRemoveError = ProjectRemoveErrors[keyof ProjectRemoveErrors]

export type ProjectRemoveResponses = {
/**
* Project removed
*/
200: boolean
}

export type ProjectRemoveResponse = ProjectRemoveResponses[keyof ProjectRemoveResponses]

export type ProjectUpdateData = {
body?: {
name?: string
Expand Down
Loading