feat(project): support project metadata updates
This commit is contained in:
parent
6bbf50b940
commit
9afd059fa1
13 changed files with 111 additions and 2 deletions
|
|
@ -3,7 +3,7 @@ export * as Project from "./project"
|
|||
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
import { asc, desc } from "drizzle-orm"
|
||||
import { asc, desc, eq } from "drizzle-orm"
|
||||
import path from "path"
|
||||
import { AbsolutePath } from "./schema"
|
||||
import { Database } from "./database/database"
|
||||
|
|
@ -37,6 +37,13 @@ export type DirectoriesInput = typeof DirectoriesInput.Type
|
|||
export const Directories = ProjectSchema.Directories
|
||||
export type Directories = typeof Directories.Type
|
||||
|
||||
export const UpdateInput = ProjectSchema.UpdateInput
|
||||
export type UpdateInput = typeof UpdateInput.Type
|
||||
|
||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Project.NotFoundError", {
|
||||
projectID: ID,
|
||||
}) {}
|
||||
|
||||
export interface Resolved {
|
||||
readonly previous?: ID
|
||||
readonly id: ID
|
||||
|
|
@ -58,6 +65,7 @@ export const root = Effect.fn("Project.root")(function* (
|
|||
export interface Interface {
|
||||
readonly list: () => Effect.Effect<ReadonlyArray<Info>>
|
||||
readonly directories: (input: DirectoriesInput) => Effect.Effect<Directories>
|
||||
readonly update: (projectID: ID, input: UpdateInput) => Effect.Effect<Info, NotFoundError>
|
||||
readonly resolve: (input: AbsolutePath) => Effect.Effect<Resolved>
|
||||
/**
|
||||
* Temporary bridge method for writing the resolved project ID to the repo-local cache.
|
||||
|
|
@ -121,6 +129,25 @@ const layer = Layer.effect(
|
|||
return yield* projectDirectories.list(input.projectID)
|
||||
})
|
||||
|
||||
const update = Effect.fn("Project.update")(function* (projectID: ID, input: UpdateInput) {
|
||||
const row = yield* db
|
||||
.update(ProjectTable)
|
||||
.set({
|
||||
name: input.name,
|
||||
icon_url: input.icon?.url,
|
||||
icon_url_override: input.icon?.override,
|
||||
icon_color: input.icon?.color,
|
||||
commands: input.commands,
|
||||
time_updated: Date.now(),
|
||||
})
|
||||
.where(eq(ProjectTable.id, projectID))
|
||||
.returning()
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!row) return yield* new NotFoundError({ projectID })
|
||||
return fromRow(row)
|
||||
})
|
||||
|
||||
const cached = Effect.fnUntraced(function* (dir: string) {
|
||||
return yield* fs.readFileString(path.join(dir, "opencode")).pipe(
|
||||
Effect.map((value) => value.trim()),
|
||||
|
|
@ -229,7 +256,7 @@ const layer = Layer.effect(
|
|||
yield* fs.writeFileString(path.join(input.store, "opencode"), input.id).pipe(Effect.ignore)
|
||||
})
|
||||
|
||||
return Service.of({ list, directories, resolve, commit })
|
||||
return Service.of({ list, directories, update, resolve, commit })
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ export type DirectoriesInput = typeof DirectoriesInput.Type
|
|||
export const Directories = Project.Directories
|
||||
export type Directories = typeof Directories.Type
|
||||
|
||||
export const UpdateInput = Project.UpdateInput
|
||||
export type UpdateInput = typeof UpdateInput.Type
|
||||
|
||||
export const Vcs = Schema.Union([
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("git"),
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ describe("node build", () => {
|
|||
return Project.Service.of({
|
||||
list: () => Effect.succeed([]),
|
||||
directories: () => Effect.succeed([]),
|
||||
update: () => Effect.die("unused"),
|
||||
resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory }),
|
||||
commit: () => Effect.void,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ const projectLayer = Layer.succeed(
|
|||
Project.Service.of({
|
||||
list: () => Effect.succeed([]),
|
||||
directories: () => Effect.succeed([]),
|
||||
update: () => Effect.die("unused"),
|
||||
resolve: () =>
|
||||
Effect.succeed({
|
||||
id: Project.ID.make("project"),
|
||||
|
|
|
|||
|
|
@ -66,6 +66,31 @@ describe("ProjectV2.list", () => {
|
|||
)
|
||||
})
|
||||
|
||||
describe("ProjectV2.update", () => {
|
||||
it.effect("updates project metadata", () =>
|
||||
Effect.gen(function* () {
|
||||
const db = (yield* Database.Service).db
|
||||
const project = yield* ProjectV2.Service
|
||||
const id = ProjectV2.ID.make("updated")
|
||||
yield* db
|
||||
.insert(ProjectTable)
|
||||
.values({ id, worktree: abs("/updated"), sandboxes: [], time_created: 1, time_updated: 1 })
|
||||
.run()
|
||||
|
||||
const result = yield* project.update(id, {
|
||||
name: "Updated",
|
||||
icon: { color: "#ffffff" },
|
||||
commands: { start: "bun dev" },
|
||||
})
|
||||
|
||||
expect(result.name).toBe("Updated")
|
||||
expect(result.icon).toEqual({ color: "#ffffff" })
|
||||
expect(result.commands).toEqual({ start: "bun dev" })
|
||||
expect(result.time.updated).toBeGreaterThan(1)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
function remoteID(remote: string) {
|
||||
return ProjectV2.ID.make(Hash.fast(`git-remote:${remote}`))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ const projects = Layer.succeed(
|
|||
list: () => Effect.succeed([]),
|
||||
resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }),
|
||||
directories: () => Effect.succeed([]),
|
||||
update: () => Effect.die("unused"),
|
||||
commit: () => Effect.void,
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ const projects = Layer.succeed(
|
|||
list: () => Effect.succeed([]),
|
||||
resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }),
|
||||
directories: () => Effect.succeed([]),
|
||||
update: () => Effect.die("unused"),
|
||||
commit: () => Effect.void,
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ const projects = Layer.succeed(
|
|||
list: () => Effect.succeed([]),
|
||||
resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }),
|
||||
directories: () => Effect.succeed([]),
|
||||
update: () => Effect.die("unused"),
|
||||
commit: () => Effect.void,
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ const projects = Layer.succeed(
|
|||
list: () => Effect.succeed([]),
|
||||
resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }),
|
||||
directories: () => Effect.succeed([]),
|
||||
update: () => Effect.die("unused"),
|
||||
commit: () => Effect.void,
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Schema } from "effect"
|
||||
import { Skill } from "@opencode-ai/schema/skill"
|
||||
import { Project } from "@opencode-ai/schema/project"
|
||||
|
||||
export class InvalidRequestError extends Schema.TaggedErrorClass<InvalidRequestError>()(
|
||||
"InvalidRequestError",
|
||||
|
|
@ -62,6 +63,15 @@ export class ProviderNotFoundError extends Schema.TaggedErrorClass<ProviderNotFo
|
|||
{ httpApiStatus: 404 },
|
||||
) {}
|
||||
|
||||
export class ProjectNotFoundError extends Schema.TaggedErrorClass<ProjectNotFoundError>()(
|
||||
"ProjectNotFoundError",
|
||||
{
|
||||
projectID: Project.ID,
|
||||
message: Schema.String,
|
||||
},
|
||||
{ httpApiStatus: 404 },
|
||||
) {}
|
||||
|
||||
export class SessionNotFoundError extends Schema.TaggedErrorClass<SessionNotFoundError>()(
|
||||
"SessionNotFoundError",
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { Project } from "@opencode-ai/schema/project"
|
|||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { LocationQuery, locationQueryOpenApi } from "./location.js"
|
||||
import { ProjectNotFoundError } from "../errors.js"
|
||||
|
||||
const root = "/api/project"
|
||||
|
||||
|
|
@ -17,6 +18,20 @@ export const ProjectGroup = HttpApiGroup.make("server.project")
|
|||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.patch("project.update", `${root}/:projectID`, {
|
||||
params: { projectID: Project.ID },
|
||||
payload: Project.UpdateInput,
|
||||
success: Project.Info,
|
||||
error: ProjectNotFoundError,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.project.update",
|
||||
summary: "Update project",
|
||||
description: "Update project metadata.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("project.current", `${root}/current`, {
|
||||
query: LocationQuery,
|
||||
|
|
|
|||
|
|
@ -56,5 +56,12 @@ export const Info = Schema.Struct({
|
|||
}).annotate({ identifier: "Project" })
|
||||
export interface Info extends Schema.Schema.Type<typeof Info> {}
|
||||
|
||||
export const UpdateInput = Schema.Struct({
|
||||
name: optional(Schema.String),
|
||||
icon: optional(Icon),
|
||||
commands: optional(Commands),
|
||||
}).annotate({ identifier: "Project.UpdateInput" })
|
||||
export interface UpdateInput extends Schema.Schema.Type<typeof UpdateInput> {}
|
||||
|
||||
const Updated = ephemeral({ type: "project.updated", schema: Info.fields })
|
||||
export const Event = { Updated, Definitions: inventory(Updated) }
|
||||
|
|
|
|||
|
|
@ -3,10 +3,26 @@ import { Project } from "@opencode-ai/core/project"
|
|||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { ProjectNotFoundError } from "@opencode-ai/protocol/errors"
|
||||
|
||||
export const ProjectHandler = HttpApiBuilder.group(Api, "server.project", (handlers) =>
|
||||
handlers
|
||||
.handle("project.list", () => Project.Service.use((project) => project.list()))
|
||||
.handle("project.update", (ctx) =>
|
||||
Effect.gen(function* () {
|
||||
const project = yield* Project.Service
|
||||
return yield* project.update(ctx.params.projectID, ctx.payload).pipe(
|
||||
Effect.catchTag(
|
||||
"Project.NotFoundError",
|
||||
() =>
|
||||
new ProjectNotFoundError({
|
||||
projectID: ctx.params.projectID,
|
||||
message: `Project not found: ${ctx.params.projectID}`,
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.handle("project.current", () =>
|
||||
Location.Service.use((location) =>
|
||||
Effect.succeed({ id: location.project.id, directory: location.project.directory }),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue