refactor(core): move database schema ownership (#29068)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
parent
6bcb9cb9bb
commit
7f571d36ea
@@ -5,7 +5,6 @@ import { File } from "../file"
|
||||
import { Snapshot } from "../snapshot"
|
||||
import * as Project from "./project"
|
||||
import * as Vcs from "./vcs"
|
||||
import { Bus } from "../bus"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { FileWatcher } from "@/file/watcher"
|
||||
import { ShareNext } from "@/share/share-next"
|
||||
@@ -57,7 +56,6 @@ export const layer = Layer.effect(
|
||||
|
||||
export const defaultLayer: Layer.Layer<Service> = layer.pipe(
|
||||
Layer.provide([
|
||||
Bus.layer,
|
||||
Config.defaultLayer,
|
||||
File.defaultLayer,
|
||||
FileWatcher.defaultLayer,
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface Interface {
|
||||
readonly load: (input: LoadInput) => Effect.Effect<InstanceContext>
|
||||
readonly reload: (input: LoadInput) => Effect.Effect<InstanceContext>
|
||||
readonly dispose: (ctx: InstanceContext) => Effect.Effect<void>
|
||||
readonly disposeDirectory: (directory: string) => Effect.Effect<void>
|
||||
readonly disposeAll: () => Effect.Effect<void>
|
||||
readonly provide: <A, E, R>(input: LoadInput, effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>
|
||||
}
|
||||
@@ -151,6 +152,15 @@ export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootst
|
||||
yield* disposeEntry(ctx.directory, entry, ctx).pipe(Effect.asVoid)
|
||||
})
|
||||
|
||||
const disposeDirectory = Effect.fn("InstanceStore.disposeDirectory")(function* (input: string) {
|
||||
const directory = AppFileSystem.resolve(input)
|
||||
const entry = cache.get(directory)
|
||||
if (!entry) return
|
||||
const exit = yield* Deferred.await(entry.deferred).pipe(Effect.exit)
|
||||
if (Exit.isFailure(exit)) return yield* removeEntry(directory, entry).pipe(Effect.asVoid)
|
||||
yield* disposeEntry(directory, entry, exit.value).pipe(Effect.asVoid)
|
||||
})
|
||||
|
||||
const disposeAllOnce = Effect.fnUntraced(function* () {
|
||||
yield* Effect.logInfo("disposing all instances")
|
||||
yield* Effect.forEach(
|
||||
@@ -185,6 +195,7 @@ export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootst
|
||||
load,
|
||||
reload,
|
||||
dispose,
|
||||
disposeDirectory,
|
||||
disposeAll,
|
||||
provide,
|
||||
})
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
|
||||
import { Timestamps } from "../storage/schema.sql"
|
||||
import type { ProjectID } from "./schema"
|
||||
|
||||
export const ProjectTable = sqliteTable("project", {
|
||||
id: text().$type<ProjectID>().primaryKey(),
|
||||
worktree: text().notNull(),
|
||||
vcs: text(),
|
||||
name: text(),
|
||||
icon_url: text(),
|
||||
icon_url_override: text(),
|
||||
icon_color: text(),
|
||||
...Timestamps,
|
||||
time_initialized: integer(),
|
||||
sandboxes: text({ mode: "json" }).notNull().$type<string[]>(),
|
||||
commands: text({ mode: "json" }).$type<{ start?: string }>(),
|
||||
})
|
||||
@@ -1,26 +1,25 @@
|
||||
import { and, eq, sql } from "drizzle-orm"
|
||||
import { Database } from "@/storage/db"
|
||||
import { ProjectTable } from "./project.sql"
|
||||
import { PermissionTable, SessionTable } from "../session/session.sql"
|
||||
import { WorkspaceTable } from "../control-plane/workspace.sql"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||
import { PermissionTable, SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { WorkspaceTable } from "@opencode-ai/core/control-plane/workspace.sql"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { which } from "../util/which"
|
||||
import { ProjectID } from "./schema"
|
||||
import { Bus } from "@/bus"
|
||||
import { Command } from "@/command"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { Effect, Layer, Scope, Context, Stream, Types, Schema } from "effect"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { AppProcess } from "@opencode-ai/core/process"
|
||||
import { Project as ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { AbsolutePath, NonNegativeInt, optionalOmitUndefined } from "@opencode-ai/core/schema"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
|
||||
const log = Log.create({ service: "project" })
|
||||
|
||||
@@ -45,7 +44,7 @@ const ProjectTime = Schema.Struct({
|
||||
})
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
id: ProjectID,
|
||||
id: ProjectV2.ID,
|
||||
worktree: Schema.String,
|
||||
vcs: optionalOmitUndefined(ProjectVcs),
|
||||
name: optionalOmitUndefined(Schema.String),
|
||||
@@ -57,7 +56,7 @@ export const Info = Schema.Struct({
|
||||
export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
|
||||
|
||||
export const Event = {
|
||||
Updated: BusEvent.define("project.updated", Info),
|
||||
Updated: EventV2.define({ type: "project.updated", schema: Info.fields }),
|
||||
}
|
||||
|
||||
type Row = typeof ProjectTable.$inferSelect
|
||||
@@ -92,7 +91,7 @@ function mergePermissionRules<T extends readonly unknown[]>(oldRules: T, newRule
|
||||
}
|
||||
|
||||
export const UpdateInput = Schema.Struct({
|
||||
projectID: ProjectID,
|
||||
projectID: ProjectV2.ID,
|
||||
name: Schema.optional(Schema.String),
|
||||
icon: Schema.optional(ProjectIcon),
|
||||
commands: Schema.optional(ProjectCommands),
|
||||
@@ -107,7 +106,7 @@ export const UpdatePayload = Schema.Struct({
|
||||
export type UpdatePayload = Types.DeepMutable<Schema.Schema.Type<typeof UpdatePayload>>
|
||||
|
||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Project.NotFoundError", {
|
||||
projectID: ProjectID,
|
||||
projectID: ProjectV2.ID,
|
||||
}) {}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -124,13 +123,13 @@ export interface Interface {
|
||||
readonly fromDirectory: (directory: string) => Effect.Effect<{ project: Info; sandbox: string }>
|
||||
readonly discover: (input: Info) => Effect.Effect<void>
|
||||
readonly list: () => Effect.Effect<Info[]>
|
||||
readonly get: (id: ProjectID) => Effect.Effect<Info | undefined>
|
||||
readonly get: (id: ProjectV2.ID) => Effect.Effect<Info | undefined>
|
||||
readonly update: (input: UpdateInput) => Effect.Effect<Info, NotFoundError>
|
||||
readonly initGit: (input: { directory: string; project: Info }) => Effect.Effect<Info>
|
||||
readonly setInitialized: (id: ProjectID) => Effect.Effect<void>
|
||||
readonly sandboxes: (id: ProjectID) => Effect.Effect<string[]>
|
||||
readonly addSandbox: (id: ProjectID, directory: string) => Effect.Effect<void>
|
||||
readonly removeSandbox: (id: ProjectID, directory: string) => Effect.Effect<void>
|
||||
readonly setInitialized: (id: ProjectV2.ID) => Effect.Effect<void>
|
||||
readonly sandboxes: (id: ProjectV2.ID) => Effect.Effect<string[]>
|
||||
readonly addSandbox: (id: ProjectV2.ID, directory: string) => Effect.Effect<void>
|
||||
readonly removeSandbox: (id: ProjectV2.ID, directory: string) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Project") {}
|
||||
@@ -144,8 +143,9 @@ export const layer = Layer.effect(
|
||||
const proc = yield* AppProcess.Service
|
||||
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
||||
const projectV2 = yield* ProjectV2.Service
|
||||
const bus = yield* Bus.Service
|
||||
const events = yield* EventV2Bridge.Service
|
||||
const flags = yield* RuntimeFlags.Service
|
||||
const { db } = yield* Database.Service
|
||||
|
||||
const git = Effect.fnUntraced(
|
||||
function* (args: string[], opts?: { cwd?: string }) {
|
||||
@@ -163,9 +163,6 @@ export const layer = Layer.effect(
|
||||
Effect.catch(() => Effect.succeed({ code: 1, text: "", stderr: "" } satisfies GitResult)),
|
||||
)
|
||||
|
||||
const db = <T>(fn: (d: Parameters<typeof Database.use>[0] extends (trx: infer D) => any ? D : never) => T) =>
|
||||
Effect.sync(() => Database.use(fn))
|
||||
|
||||
const emitUpdated = (data: Info) =>
|
||||
Effect.sync(() =>
|
||||
GlobalBus.emit("event", {
|
||||
@@ -180,20 +177,22 @@ export const layer = Layer.effect(
|
||||
const scope = yield* Scope.Scope
|
||||
|
||||
const migrateProjectId = Effect.fn("Project.migrateProjectId")(function* (
|
||||
oldID: ProjectID | undefined,
|
||||
newID: ProjectID,
|
||||
oldID: ProjectV2.ID | undefined,
|
||||
newID: ProjectV2.ID,
|
||||
) {
|
||||
if (!oldID) return
|
||||
if (oldID === ProjectID.global) return
|
||||
if (oldID === ProjectV2.ID.global) return
|
||||
if (oldID === newID) return
|
||||
|
||||
yield* Effect.sync(() =>
|
||||
Database.transaction(
|
||||
(d) => {
|
||||
const oldProject = d.select().from(ProjectTable).where(eq(ProjectTable.id, oldID)).get()
|
||||
const newProject = d.select().from(ProjectTable).where(eq(ProjectTable.id, newID)).get()
|
||||
yield* db
|
||||
.transaction(
|
||||
(d) =>
|
||||
Effect.gen(function* () {
|
||||
const oldProject = yield* d.select().from(ProjectTable).where(eq(ProjectTable.id, oldID)).get()
|
||||
const newProject = yield* d.select().from(ProjectTable).where(eq(ProjectTable.id, newID)).get()
|
||||
if (oldProject && !newProject) {
|
||||
d.insert(ProjectTable)
|
||||
yield* d
|
||||
.insert(ProjectTable)
|
||||
.values({
|
||||
...oldProject,
|
||||
id: newID,
|
||||
@@ -202,10 +201,11 @@ export const layer = Layer.effect(
|
||||
.run()
|
||||
}
|
||||
|
||||
const oldPermission = d.select().from(PermissionTable).where(eq(PermissionTable.project_id, oldID)).get()
|
||||
const newPermission = d.select().from(PermissionTable).where(eq(PermissionTable.project_id, newID)).get()
|
||||
const oldPermission = yield* d.select().from(PermissionTable).where(eq(PermissionTable.project_id, oldID)).get()
|
||||
const newPermission = yield* d.select().from(PermissionTable).where(eq(PermissionTable.project_id, newID)).get()
|
||||
if (oldPermission && newPermission) {
|
||||
d.update(PermissionTable)
|
||||
yield* d
|
||||
.update(PermissionTable)
|
||||
.set({
|
||||
data: mergePermissionRules(oldPermission.data, newPermission.data),
|
||||
time_created: Math.min(oldPermission.time_created, newPermission.time_created),
|
||||
@@ -213,23 +213,24 @@ export const layer = Layer.effect(
|
||||
})
|
||||
.where(eq(PermissionTable.project_id, newID))
|
||||
.run()
|
||||
d.delete(PermissionTable).where(eq(PermissionTable.project_id, oldID)).run()
|
||||
yield* d.delete(PermissionTable).where(eq(PermissionTable.project_id, oldID)).run()
|
||||
}
|
||||
if (oldPermission && !newPermission) {
|
||||
d.update(PermissionTable).set({ project_id: newID }).where(eq(PermissionTable.project_id, oldID)).run()
|
||||
yield* d.update(PermissionTable).set({ project_id: newID }).where(eq(PermissionTable.project_id, oldID)).run()
|
||||
}
|
||||
|
||||
d.update(SessionTable)
|
||||
yield* d
|
||||
.update(SessionTable)
|
||||
.set({ project_id: newID, time_updated: sql`${SessionTable.time_updated}` })
|
||||
.where(eq(SessionTable.project_id, oldID))
|
||||
.run()
|
||||
d.update(WorkspaceTable).set({ project_id: newID }).where(eq(WorkspaceTable.project_id, oldID)).run()
|
||||
yield* d.update(WorkspaceTable).set({ project_id: newID }).where(eq(WorkspaceTable.project_id, oldID)).run()
|
||||
|
||||
if (oldProject) d.delete(ProjectTable).where(eq(ProjectTable.id, oldID)).run()
|
||||
},
|
||||
if (oldProject) yield* d.delete(ProjectTable).where(eq(ProjectTable.id, oldID)).run()
|
||||
}),
|
||||
{ behavior: "immediate" },
|
||||
),
|
||||
)
|
||||
)
|
||||
.pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
const fromDirectory = Effect.fn("Project.fromDirectory")(function* (directory: string) {
|
||||
@@ -239,9 +240,9 @@ export const layer = Layer.effect(
|
||||
const worktree = data.id === ProjectV2.ID.make("global") && !data.vcs ? "/" : data.directory
|
||||
|
||||
// Phase 2: upsert
|
||||
const projectID = ProjectID.make(data.id)
|
||||
yield* migrateProjectId(data.previous ? ProjectID.make(data.previous) : undefined, projectID)
|
||||
const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get())
|
||||
const projectID = ProjectV2.ID.make(data.id)
|
||||
yield* migrateProjectId(data.previous ? ProjectV2.ID.make(data.previous) : undefined, projectID)
|
||||
const row = yield* db.select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get().pipe(Effect.orDie)
|
||||
const existing = row
|
||||
? fromRow(row)
|
||||
: {
|
||||
@@ -256,12 +257,12 @@ export const layer = Layer.effect(
|
||||
|
||||
const result: Info = {
|
||||
...existing,
|
||||
worktree: projectID === ProjectID.global ? worktree : existing.worktree,
|
||||
worktree: projectID === ProjectV2.ID.global ? worktree : existing.worktree,
|
||||
vcs: data.vcs?.type ?? fakeVcs,
|
||||
time: { ...existing.time, updated: Date.now() },
|
||||
}
|
||||
if (
|
||||
projectID !== ProjectID.global &&
|
||||
projectID !== ProjectV2.ID.global &&
|
||||
data.directory !== result.worktree &&
|
||||
!result.sandboxes.includes(data.directory)
|
||||
)
|
||||
@@ -276,8 +277,7 @@ export const layer = Layer.effect(
|
||||
{ concurrency: "unbounded" },
|
||||
).pipe(Effect.map((arr) => arr.filter((x): x is string => x !== undefined)))
|
||||
|
||||
yield* db((d) =>
|
||||
d
|
||||
yield* db
|
||||
.insert(ProjectTable)
|
||||
.values({
|
||||
id: result.id,
|
||||
@@ -308,21 +308,20 @@ export const layer = Layer.effect(
|
||||
commands: result.commands,
|
||||
},
|
||||
})
|
||||
.run(),
|
||||
)
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
|
||||
if (projectID !== ProjectID.global) {
|
||||
yield* db((d) =>
|
||||
d
|
||||
if (projectID !== ProjectV2.ID.global) {
|
||||
yield* db
|
||||
.update(SessionTable)
|
||||
.set({ project_id: projectID })
|
||||
.where(and(eq(SessionTable.project_id, ProjectID.global), eq(SessionTable.directory, data.directory)))
|
||||
.run(),
|
||||
)
|
||||
.where(and(eq(SessionTable.project_id, ProjectV2.ID.global), eq(SessionTable.directory, data.directory)))
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
}
|
||||
|
||||
yield* emitUpdated(result)
|
||||
if (projectID !== ProjectID.global && data.vcs?.type === "git") {
|
||||
if (projectID !== ProjectV2.ID.global && data.vcs?.type === "git") {
|
||||
yield* projectV2.commit({ store: data.vcs.store, id: data.id })
|
||||
}
|
||||
return { project: result, sandbox: data.vcs ? data.directory : worktree }
|
||||
@@ -353,17 +352,16 @@ export const layer = Layer.effect(
|
||||
})
|
||||
|
||||
const list = Effect.fn("Project.list")(function* () {
|
||||
return yield* db((d) => d.select().from(ProjectTable).all().map(fromRow))
|
||||
return (yield* db.select().from(ProjectTable).all().pipe(Effect.orDie)).map(fromRow)
|
||||
})
|
||||
|
||||
const get = Effect.fn("Project.get")(function* (id: ProjectID) {
|
||||
const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
|
||||
const get = Effect.fn("Project.get")(function* (id: ProjectV2.ID) {
|
||||
const row = yield* db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get().pipe(Effect.orDie)
|
||||
return row ? fromRow(row) : undefined
|
||||
})
|
||||
|
||||
const update = Effect.fn("Project.update")(function* (input: UpdateInput) {
|
||||
const result = yield* db((d) =>
|
||||
d
|
||||
const result = yield* db
|
||||
.update(ProjectTable)
|
||||
.set({
|
||||
name: input.name,
|
||||
@@ -375,8 +373,8 @@ export const layer = Layer.effect(
|
||||
})
|
||||
.where(eq(ProjectTable.id, input.projectID))
|
||||
.returning()
|
||||
.get(),
|
||||
)
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!result) return yield* new NotFoundError({ projectID: input.projectID })
|
||||
const data = fromRow(result)
|
||||
yield* emitUpdated(data)
|
||||
@@ -394,20 +392,18 @@ export const layer = Layer.effect(
|
||||
return project
|
||||
})
|
||||
|
||||
const setInitialized = Effect.fn("Project.setInitialized")(function* (id: ProjectID) {
|
||||
yield* db((d) =>
|
||||
d.update(ProjectTable).set({ time_initialized: Date.now() }).where(eq(ProjectTable.id, id)).run(),
|
||||
)
|
||||
const setInitialized = Effect.fn("Project.setInitialized")(function* (id: ProjectV2.ID) {
|
||||
yield* db.update(ProjectTable).set({ time_initialized: Date.now() }).where(eq(ProjectTable.id, id)).run().pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
const initState = yield* InstanceState.make(
|
||||
Effect.fn("Project.initState")(function* (ctx) {
|
||||
yield* (yield* bus.subscribe(Command.Event.Executed)).pipe(
|
||||
Stream.runForEach((payload) =>
|
||||
payload.properties.name === Command.Default.INIT ? setInitialized(ctx.project.id) : Effect.void,
|
||||
),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
const unsubscribe = yield* events.listen((event) => {
|
||||
if (event.type !== Command.Event.Executed.type || event.location?.directory !== ctx.directory) return Effect.void
|
||||
const data = event.data as EventV2.Data<typeof Command.Event.Executed>
|
||||
return data.name === Command.Default.INIT ? setInitialized(ctx.project.id) : Effect.void
|
||||
})
|
||||
yield* Effect.addFinalizer(() => unsubscribe)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -415,8 +411,8 @@ export const layer = Layer.effect(
|
||||
yield* InstanceState.get(initState)
|
||||
})
|
||||
|
||||
const sandboxes = Effect.fn("Project.sandboxes")(function* (id: ProjectID) {
|
||||
const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
|
||||
const sandboxes = Effect.fn("Project.sandboxes")(function* (id: ProjectV2.ID) {
|
||||
const row = yield* db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get().pipe(Effect.orDie)
|
||||
if (!row) return []
|
||||
const data = fromRow(row)
|
||||
return yield* Effect.forEach(
|
||||
@@ -430,35 +426,33 @@ export const layer = Layer.effect(
|
||||
).pipe(Effect.map((arr) => arr.filter((x): x is string => x !== undefined)))
|
||||
})
|
||||
|
||||
const addSandbox = Effect.fn("Project.addSandbox")(function* (id: ProjectID, directory: string) {
|
||||
const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
|
||||
const addSandbox = Effect.fn("Project.addSandbox")(function* (id: ProjectV2.ID, directory: string) {
|
||||
const row = yield* db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get().pipe(Effect.orDie)
|
||||
if (!row) throw new Error(`Project not found: ${id}`)
|
||||
const sboxes = [...row.sandboxes]
|
||||
if (!sboxes.includes(directory)) sboxes.push(directory)
|
||||
const result = yield* db((d) =>
|
||||
d
|
||||
const result = yield* db
|
||||
.update(ProjectTable)
|
||||
.set({ sandboxes: sboxes, time_updated: Date.now() })
|
||||
.where(eq(ProjectTable.id, id))
|
||||
.returning()
|
||||
.get(),
|
||||
)
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!result) throw new Error(`Project not found: ${id}`)
|
||||
yield* emitUpdated(fromRow(result))
|
||||
})
|
||||
|
||||
const removeSandbox = Effect.fn("Project.removeSandbox")(function* (id: ProjectID, directory: string) {
|
||||
const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
|
||||
const removeSandbox = Effect.fn("Project.removeSandbox")(function* (id: ProjectV2.ID, directory: string) {
|
||||
const row = yield* db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get().pipe(Effect.orDie)
|
||||
if (!row) throw new Error(`Project not found: ${id}`)
|
||||
const sboxes = row.sandboxes.filter((s) => s !== directory)
|
||||
const result = yield* db((d) =>
|
||||
d
|
||||
const result = yield* db
|
||||
.update(ProjectTable)
|
||||
.set({ sandboxes: sboxes, time_updated: Date.now() })
|
||||
.where(eq(ProjectTable.id, id))
|
||||
.returning()
|
||||
.get(),
|
||||
)
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!result) throw new Error(`Project not found: ${id}`)
|
||||
yield* emitUpdated(fromRow(result))
|
||||
})
|
||||
@@ -480,36 +474,15 @@ export const layer = Layer.effect(
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(
|
||||
Layer.provide(Bus.defaultLayer),
|
||||
Layer.provide(EventV2Bridge.defaultLayer),
|
||||
Layer.provide(ProjectV2.defaultLayer),
|
||||
Layer.provide(AppProcess.defaultLayer),
|
||||
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(Database.defaultLayer),
|
||||
Layer.provide(RuntimeFlags.defaultLayer),
|
||||
)
|
||||
|
||||
export const use = serviceUse(Service)
|
||||
|
||||
export function list() {
|
||||
return Database.use((db) =>
|
||||
db
|
||||
.select()
|
||||
.from(ProjectTable)
|
||||
.all()
|
||||
.map((row) => fromRow(row)),
|
||||
)
|
||||
}
|
||||
|
||||
export function get(id: ProjectID): Info | undefined {
|
||||
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
|
||||
if (!row) return undefined
|
||||
return fromRow(row)
|
||||
}
|
||||
|
||||
export function setInitialized(id: ProjectID) {
|
||||
Database.use((db) =>
|
||||
db.update(ProjectTable).set({ time_initialized: Date.now() }).where(eq(ProjectTable.id, id)).run(),
|
||||
)
|
||||
}
|
||||
|
||||
export * as Project from "./project"
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Schema } from "effect"
|
||||
|
||||
import { withStatics } from "@opencode-ai/core/schema"
|
||||
|
||||
const projectIdSchema = Schema.String.pipe(Schema.brand("ProjectID"))
|
||||
|
||||
export type ProjectID = typeof projectIdSchema.Type
|
||||
|
||||
export const ProjectID = projectIdSchema.pipe(
|
||||
withStatics((schema: typeof projectIdSchema) => ({
|
||||
global: schema.make("global"),
|
||||
})),
|
||||
)
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Effect, Layer, Context, Schema, Stream, Scope } from "effect"
|
||||
import { formatPatch, structuredPatch } from "diff"
|
||||
import { Bus } from "@/bus"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { FileWatcher } from "@/file/watcher"
|
||||
import { Git } from "@/git"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
|
||||
const log = Log.create({ service: "vcs" })
|
||||
const PATCH_CONTEXT_LINES = 2_147_483_647
|
||||
@@ -239,12 +239,12 @@ export const Mode = Schema.Literals(["git", "branch"])
|
||||
export type Mode = Schema.Schema.Type<typeof Mode>
|
||||
|
||||
export const Event = {
|
||||
BranchUpdated: BusEvent.define(
|
||||
"vcs.branch.updated",
|
||||
Schema.Struct({
|
||||
BranchUpdated: EventV2.define({
|
||||
type: "vcs.branch.updated",
|
||||
schema: {
|
||||
branch: Schema.optional(Schema.String),
|
||||
}),
|
||||
),
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
@@ -305,11 +305,11 @@ interface State {
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Vcs") {}
|
||||
|
||||
export const layer: Layer.Layer<Service, never, Git.Service | Bus.Service> = Layer.effect(
|
||||
export const layer: Layer.Layer<Service, never, Git.Service | EventV2Bridge.Service> = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const git = yield* Git.Service
|
||||
const bus = yield* Bus.Service
|
||||
const events = yield* EventV2Bridge.Service
|
||||
const scope = yield* Scope.Scope
|
||||
|
||||
const state = yield* InstanceState.make<State>(
|
||||
@@ -327,20 +327,20 @@ export const layer: Layer.Layer<Service, never, Git.Service | Bus.Service> = Lay
|
||||
const value = { current, root }
|
||||
log.info("initialized", { branch: value.current, default_branch: value.root?.name })
|
||||
|
||||
yield* (yield* bus.subscribe(FileWatcher.Event.Updated)).pipe(
|
||||
Stream.filter((evt) => evt.properties.file.endsWith("HEAD")),
|
||||
Stream.runForEach((_evt) =>
|
||||
Effect.gen(function* () {
|
||||
const next = yield* get()
|
||||
if (next !== value.current) {
|
||||
log.info("branch changed", { from: value.current, to: next })
|
||||
value.current = next
|
||||
yield* bus.publish(Event.BranchUpdated, { branch: next })
|
||||
}
|
||||
}),
|
||||
),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
const unsubscribe = yield* events.listen((event) => {
|
||||
if (event.type !== FileWatcher.Event.Updated.type || event.location?.directory !== ctx.directory) return Effect.void
|
||||
const data = event.data as EventV2.Data<typeof FileWatcher.Event.Updated>
|
||||
if (!data.file.endsWith("HEAD")) return Effect.void
|
||||
return Effect.gen(function* () {
|
||||
const next = yield* get()
|
||||
if (next !== value.current) {
|
||||
log.info("branch changed", { from: value.current, to: next })
|
||||
value.current = next
|
||||
yield* events.publish(Event.BranchUpdated, { branch: next })
|
||||
}
|
||||
})
|
||||
})
|
||||
yield* Effect.addFinalizer(() => unsubscribe)
|
||||
|
||||
return value
|
||||
}),
|
||||
@@ -429,6 +429,9 @@ export const layer: Layer.Layer<Service, never, Git.Service | Bus.Service> = Lay
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(Git.defaultLayer), Layer.provide(Bus.layer))
|
||||
export const defaultLayer = layer.pipe(
|
||||
Layer.provide(Git.defaultLayer),
|
||||
Layer.provide(EventV2Bridge.defaultLayer),
|
||||
)
|
||||
|
||||
export * as Vcs from "./vcs"
|
||||
|
||||
Reference in New Issue
Block a user