core: remove dependency on remeda to simplify dependencies
This commit is contained in:
parent
bf0754caeb
commit
acdcf7fa88
2 changed files with 127 additions and 107 deletions
|
|
@ -1,5 +1,4 @@
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import fs from "fs/promises"
|
|
||||||
import { Filesystem } from "../util/filesystem"
|
import { Filesystem } from "../util/filesystem"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
|
|
@ -13,7 +12,6 @@ import { fn } from "@opencode-ai/util/fn"
|
||||||
import { BusEvent } from "@/bus/bus-event"
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
import { iife } from "@/util/iife"
|
import { iife } from "@/util/iife"
|
||||||
import { GlobalBus } from "@/bus/global"
|
import { GlobalBus } from "@/bus/global"
|
||||||
import { existsSync } from "fs"
|
|
||||||
|
|
||||||
export namespace Project {
|
export namespace Project {
|
||||||
const log = Log.create({ service: "project" })
|
const log = Log.create({ service: "project" })
|
||||||
|
|
@ -61,7 +59,7 @@ export namespace Project {
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
worktree: row.worktree,
|
worktree: row.worktree,
|
||||||
vcs: row.vcs as Info["vcs"],
|
vcs: row.vcs ? Info.shape.vcs.parse(row.vcs) : undefined,
|
||||||
name: row.name ?? undefined,
|
name: row.name ?? undefined,
|
||||||
icon,
|
icon,
|
||||||
time: {
|
time: {
|
||||||
|
|
@ -76,63 +74,60 @@ export namespace Project {
|
||||||
export async function fromDirectory(directory: string) {
|
export async function fromDirectory(directory: string) {
|
||||||
log.info("fromDirectory", { directory })
|
log.info("fromDirectory", { directory })
|
||||||
|
|
||||||
const { id, sandbox, worktree, vcs } = await iife(async () => {
|
const data = await iife(async () => {
|
||||||
const matches = Filesystem.up({ targets: [".git"], start: directory })
|
const matches = Filesystem.up({ targets: [".git"], start: directory })
|
||||||
const git = await matches.next().then((x) => x.value)
|
const git = await matches.next().then((x) => x.value)
|
||||||
await matches.return()
|
await matches.return()
|
||||||
if (git) {
|
if (git) {
|
||||||
let sandbox = path.dirname(git)
|
const sandbox = path.dirname(git)
|
||||||
|
const bin = Bun.which("git")
|
||||||
|
|
||||||
const gitBinary = Bun.which("git")
|
const cached = await Bun.file(path.join(git, "opencode"))
|
||||||
|
|
||||||
// cached id calculation
|
|
||||||
let id = await Bun.file(path.join(git, "opencode"))
|
|
||||||
.text()
|
.text()
|
||||||
.then((x) => x.trim())
|
.then((x) => x.trim())
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
|
|
||||||
if (!gitBinary) {
|
if (!bin) {
|
||||||
return {
|
return {
|
||||||
id: id ?? "global",
|
id: cached ?? "global",
|
||||||
worktree: sandbox,
|
worktree: sandbox,
|
||||||
sandbox: sandbox,
|
sandbox: sandbox,
|
||||||
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
|
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// generate id from root commit
|
const roots = cached
|
||||||
if (!id) {
|
? undefined
|
||||||
const roots = await $`git rev-list --max-parents=0 --all`
|
: await $`git rev-list --max-parents=0 --all`
|
||||||
.quiet()
|
.quiet()
|
||||||
.nothrow()
|
.nothrow()
|
||||||
.cwd(sandbox)
|
.cwd(sandbox)
|
||||||
.text()
|
.text()
|
||||||
.then((x) =>
|
.then((x) =>
|
||||||
x
|
x
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((x) => x.trim())
|
.map((x) => x.trim())
|
||||||
.toSorted(),
|
.toSorted(),
|
||||||
)
|
)
|
||||||
.catch(() => undefined)
|
|
||||||
|
|
||||||
if (!roots) {
|
|
||||||
return {
|
|
||||||
id: "global",
|
|
||||||
worktree: sandbox,
|
|
||||||
sandbox: sandbox,
|
|
||||||
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
id = roots[0]
|
|
||||||
if (id) {
|
|
||||||
void Bun.file(path.join(git, "opencode"))
|
|
||||||
.write(id)
|
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
|
|
||||||
|
if (!cached && !roots) {
|
||||||
|
return {
|
||||||
|
id: "global",
|
||||||
|
worktree: sandbox,
|
||||||
|
sandbox: sandbox,
|
||||||
|
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const id = cached ?? roots?.[0]
|
||||||
|
if (!cached && id) {
|
||||||
|
void Bun.file(path.join(git, "opencode"))
|
||||||
|
.write(id)
|
||||||
|
.catch(() => undefined)
|
||||||
|
}
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return {
|
return {
|
||||||
id: "global",
|
id: "global",
|
||||||
|
|
@ -159,33 +154,31 @@ export namespace Project {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sandbox = top
|
const tree = await $`git rev-parse --git-common-dir`
|
||||||
|
|
||||||
const worktree = await $`git rev-parse --git-common-dir`
|
|
||||||
.quiet()
|
.quiet()
|
||||||
.nothrow()
|
.nothrow()
|
||||||
.cwd(sandbox)
|
.cwd(top)
|
||||||
.text()
|
.text()
|
||||||
.then((x) => {
|
.then((x) => {
|
||||||
const dirname = path.dirname(x.trim())
|
const dirname = path.dirname(x.trim())
|
||||||
if (dirname === ".") return sandbox
|
if (dirname === ".") return top
|
||||||
return dirname
|
return dirname
|
||||||
})
|
})
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
|
|
||||||
if (!worktree) {
|
if (!tree) {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
sandbox,
|
sandbox: top,
|
||||||
worktree: sandbox,
|
worktree: top,
|
||||||
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
|
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
sandbox,
|
sandbox: top,
|
||||||
worktree,
|
worktree: tree,
|
||||||
vcs: "git",
|
vcs: "git",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -198,21 +191,21 @@ export namespace Project {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
|
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, data.id)).get())
|
||||||
const existing = await iife(async () => {
|
const existing = await iife(async () => {
|
||||||
if (row) return fromRow(row)
|
if (row) return fromRow(row)
|
||||||
const fresh: Info = {
|
const fresh: Info = {
|
||||||
id,
|
id: data.id,
|
||||||
worktree,
|
worktree: data.worktree,
|
||||||
vcs: vcs as Info["vcs"],
|
vcs: data.vcs as Info["vcs"],
|
||||||
sandboxes: [],
|
sandboxes: [],
|
||||||
time: {
|
time: {
|
||||||
created: Date.now(),
|
created: Date.now(),
|
||||||
updated: Date.now(),
|
updated: Date.now(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if (id !== "global") {
|
if (data.id !== "global") {
|
||||||
await migrateFromGlobal(id, worktree)
|
await migrateFromGlobal(data.id, data.worktree)
|
||||||
}
|
}
|
||||||
return fresh
|
return fresh
|
||||||
})
|
})
|
||||||
|
|
@ -221,19 +214,27 @@ export namespace Project {
|
||||||
|
|
||||||
const result: Info = {
|
const result: Info = {
|
||||||
...existing,
|
...existing,
|
||||||
worktree,
|
worktree: data.worktree,
|
||||||
vcs: vcs as Info["vcs"],
|
vcs: data.vcs as Info["vcs"],
|
||||||
time: {
|
time: {
|
||||||
...existing.time,
|
...existing.time,
|
||||||
updated: Date.now(),
|
updated: Date.now(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if (sandbox !== result.worktree && !result.sandboxes.includes(sandbox)) result.sandboxes.push(sandbox)
|
if (data.sandbox !== result.worktree && !result.sandboxes.includes(data.sandbox))
|
||||||
result.sandboxes = result.sandboxes.filter((x) => existsSync(x))
|
result.sandboxes.push(data.sandbox)
|
||||||
|
const sandboxes: string[] = []
|
||||||
|
for (const x of result.sandboxes) {
|
||||||
|
const stat = await Bun.file(x)
|
||||||
|
.stat()
|
||||||
|
.catch(() => undefined)
|
||||||
|
if (stat) sandboxes.push(x)
|
||||||
|
}
|
||||||
|
result.sandboxes = sandboxes
|
||||||
const insert = {
|
const insert = {
|
||||||
id: result.id,
|
id: result.id,
|
||||||
worktree: result.worktree,
|
worktree: result.worktree,
|
||||||
vcs: result.vcs,
|
vcs: result.vcs ?? null,
|
||||||
name: result.name,
|
name: result.name,
|
||||||
icon_url: result.icon?.url,
|
icon_url: result.icon?.url,
|
||||||
icon_color: result.icon?.color,
|
icon_color: result.icon?.color,
|
||||||
|
|
@ -244,7 +245,7 @@ export namespace Project {
|
||||||
}
|
}
|
||||||
const updateSet = {
|
const updateSet = {
|
||||||
worktree: result.worktree,
|
worktree: result.worktree,
|
||||||
vcs: result.vcs,
|
vcs: result.vcs ?? null,
|
||||||
name: result.name,
|
name: result.name,
|
||||||
icon_url: result.icon?.url,
|
icon_url: result.icon?.url,
|
||||||
icon_color: result.icon?.color,
|
icon_color: result.icon?.color,
|
||||||
|
|
@ -261,7 +262,7 @@ export namespace Project {
|
||||||
properties: result,
|
properties: result,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return { project: result, sandbox }
|
return { project: result, sandbox: data.sandbox }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function discover(input: Info) {
|
export async function discover(input: Info) {
|
||||||
|
|
@ -294,38 +295,36 @@ export namespace Project {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
async function migrateFromGlobal(newProjectID: string, worktree: string) {
|
async function migrateFromGlobal(id: string, worktree: string) {
|
||||||
const globalRow = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, "global")).get())
|
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, "global")).get())
|
||||||
if (!globalRow) return
|
if (!row) return
|
||||||
|
|
||||||
const globalSessions = Database.use((db) =>
|
const sessions = Database.use((db) =>
|
||||||
db.select().from(SessionTable).where(eq(SessionTable.project_id, "global")).all(),
|
db.select().from(SessionTable).where(eq(SessionTable.project_id, "global")).all(),
|
||||||
)
|
)
|
||||||
if (globalSessions.length === 0) return
|
if (sessions.length === 0) return
|
||||||
|
|
||||||
log.info("migrating sessions from global", { newProjectID, worktree, count: globalSessions.length })
|
log.info("migrating sessions from global", { newProjectID: id, worktree, count: sessions.length })
|
||||||
|
|
||||||
await work(10, globalSessions, async (row) => {
|
await work(10, sessions, async (row) => {
|
||||||
// Skip sessions that belong to a different directory
|
// Skip sessions that belong to a different directory
|
||||||
if (row.directory && row.directory !== worktree) return
|
if (row.directory && row.directory !== worktree) return
|
||||||
|
|
||||||
log.info("migrating session", { sessionID: row.id, from: "global", to: newProjectID })
|
log.info("migrating session", { sessionID: row.id, from: "global", to: id })
|
||||||
Database.use((db) =>
|
Database.use((db) => db.update(SessionTable).set({ project_id: id }).where(eq(SessionTable.id, row.id)).run())
|
||||||
db.update(SessionTable).set({ project_id: newProjectID }).where(eq(SessionTable.id, row.id)).run(),
|
|
||||||
)
|
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
log.error("failed to migrate sessions from global to project", { error, projectId: newProjectID })
|
log.error("failed to migrate sessions from global to project", { error, projectId: id })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setInitialized(projectID: string) {
|
export function setInitialized(id: string) {
|
||||||
Database.use((db) =>
|
Database.use((db) =>
|
||||||
db
|
db
|
||||||
.update(ProjectTable)
|
.update(ProjectTable)
|
||||||
.set({
|
.set({
|
||||||
time_initialized: Date.now(),
|
time_initialized: Date.now(),
|
||||||
})
|
})
|
||||||
.where(eq(ProjectTable.id, projectID))
|
.where(eq(ProjectTable.id, id))
|
||||||
.run(),
|
.run(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -340,8 +339,8 @@ export namespace Project {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function get(projectID: string): Info | undefined {
|
export function get(id: string): Info | undefined {
|
||||||
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get())
|
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
|
||||||
if (!row) return undefined
|
if (!row) return undefined
|
||||||
return fromRow(row)
|
return fromRow(row)
|
||||||
}
|
}
|
||||||
|
|
@ -379,32 +378,34 @@ export namespace Project {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
export async function sandboxes(projectID: string) {
|
export async function sandboxes(id: string) {
|
||||||
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get())
|
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
|
||||||
if (!row) return []
|
if (!row) return []
|
||||||
const data = fromRow(row)
|
const data = fromRow(row)
|
||||||
const valid: string[] = []
|
const valid: string[] = []
|
||||||
for (const dir of data.sandboxes) {
|
for (const dir of data.sandboxes) {
|
||||||
const stat = await fs.stat(dir).catch(() => undefined)
|
const stat = await Bun.file(dir)
|
||||||
|
.stat()
|
||||||
|
.catch(() => undefined)
|
||||||
if (stat?.isDirectory()) valid.push(dir)
|
if (stat?.isDirectory()) valid.push(dir)
|
||||||
}
|
}
|
||||||
return valid
|
return valid
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addSandbox(projectID: string, directory: string) {
|
export async function addSandbox(id: string, directory: string) {
|
||||||
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get())
|
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
|
||||||
if (!row) throw new Error(`Project not found: ${projectID}`)
|
if (!row) throw new Error(`Project not found: ${id}`)
|
||||||
const sandboxes = [...row.sandboxes]
|
const sandboxes = [...row.sandboxes]
|
||||||
if (!sandboxes.includes(directory)) sandboxes.push(directory)
|
if (!sandboxes.includes(directory)) sandboxes.push(directory)
|
||||||
const result = Database.use((db) =>
|
const result = Database.use((db) =>
|
||||||
db
|
db
|
||||||
.update(ProjectTable)
|
.update(ProjectTable)
|
||||||
.set({ sandboxes, time_updated: Date.now() })
|
.set({ sandboxes, time_updated: Date.now() })
|
||||||
.where(eq(ProjectTable.id, projectID))
|
.where(eq(ProjectTable.id, id))
|
||||||
.returning()
|
.returning()
|
||||||
.get(),
|
.get(),
|
||||||
)
|
)
|
||||||
if (!result) throw new Error(`Project not found: ${projectID}`)
|
if (!result) throw new Error(`Project not found: ${id}`)
|
||||||
const data = fromRow(result)
|
const data = fromRow(result)
|
||||||
GlobalBus.emit("event", {
|
GlobalBus.emit("event", {
|
||||||
payload: {
|
payload: {
|
||||||
|
|
@ -415,19 +416,19 @@ export namespace Project {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeSandbox(projectID: string, directory: string) {
|
export async function removeSandbox(id: string, directory: string) {
|
||||||
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get())
|
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
|
||||||
if (!row) throw new Error(`Project not found: ${projectID}`)
|
if (!row) throw new Error(`Project not found: ${id}`)
|
||||||
const sandboxes = row.sandboxes.filter((s: string) => s !== directory)
|
const sandboxes = row.sandboxes.filter((s) => s !== directory)
|
||||||
const result = Database.use((db) =>
|
const result = Database.use((db) =>
|
||||||
db
|
db
|
||||||
.update(ProjectTable)
|
.update(ProjectTable)
|
||||||
.set({ sandboxes, time_updated: Date.now() })
|
.set({ sandboxes, time_updated: Date.now() })
|
||||||
.where(eq(ProjectTable.id, projectID))
|
.where(eq(ProjectTable.id, id))
|
||||||
.returning()
|
.returning()
|
||||||
.get(),
|
.get(),
|
||||||
)
|
)
|
||||||
if (!result) throw new Error(`Project not found: ${projectID}`)
|
if (!result) throw new Error(`Project not found: ${id}`)
|
||||||
const data = fromRow(result)
|
const data = fromRow(result)
|
||||||
GlobalBus.emit("event", {
|
GlobalBus.emit("event", {
|
||||||
payload: {
|
payload: {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import { Snapshot } from "../snapshot"
|
||||||
import { MessageV2 } from "./message-v2"
|
import { MessageV2 } from "./message-v2"
|
||||||
import { Session } from "."
|
import { Session } from "."
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { splitWhen } from "remeda"
|
|
||||||
import { Database, eq } from "../storage/db"
|
import { Database, eq } from "../storage/db"
|
||||||
import { SessionDiffTable, MessageTable, PartTable } from "./session.sql"
|
import { SessionDiffTable, MessageTable, PartTable } from "./session.sql"
|
||||||
import { Bus } from "../bus"
|
import { Bus } from "../bus"
|
||||||
|
|
@ -97,26 +96,46 @@ export namespace SessionRevert {
|
||||||
export async function cleanup(session: Session.Info) {
|
export async function cleanup(session: Session.Info) {
|
||||||
if (!session.revert) return
|
if (!session.revert) return
|
||||||
const sessionID = session.id
|
const sessionID = session.id
|
||||||
let msgs = await Session.messages({ sessionID })
|
const msgs = await Session.messages({ sessionID })
|
||||||
const messageID = session.revert.messageID
|
const messageID = session.revert.messageID
|
||||||
const [preserve, remove] = splitWhen(msgs, (x) => x.info.id === messageID)
|
const preserve = [] as MessageV2.WithParts[]
|
||||||
msgs = preserve
|
const remove = [] as MessageV2.WithParts[]
|
||||||
|
let target: MessageV2.WithParts | undefined
|
||||||
|
for (const msg of msgs) {
|
||||||
|
if (msg.info.id < messageID) {
|
||||||
|
preserve.push(msg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (msg.info.id > messageID) {
|
||||||
|
remove.push(msg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (session.revert.partID) {
|
||||||
|
preserve.push(msg)
|
||||||
|
target = msg
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
remove.push(msg)
|
||||||
|
}
|
||||||
for (const msg of remove) {
|
for (const msg of remove) {
|
||||||
Database.use((db) => db.delete(MessageTable).where(eq(MessageTable.id, msg.info.id)).run())
|
Database.use((db) => db.delete(MessageTable).where(eq(MessageTable.id, msg.info.id)).run())
|
||||||
await Bus.publish(MessageV2.Event.Removed, { sessionID: sessionID, messageID: msg.info.id })
|
await Bus.publish(MessageV2.Event.Removed, { sessionID: sessionID, messageID: msg.info.id })
|
||||||
}
|
}
|
||||||
const last = preserve.at(-1)
|
if (session.revert.partID && target) {
|
||||||
if (session.revert.partID && last) {
|
|
||||||
const partID = session.revert.partID
|
const partID = session.revert.partID
|
||||||
const [preserveParts, removeParts] = splitWhen(last.parts, (x) => x.id === partID)
|
const removeStart = target.parts.findIndex((part) => part.id === partID)
|
||||||
last.parts = preserveParts
|
if (removeStart >= 0) {
|
||||||
for (const part of removeParts) {
|
const preserveParts = target.parts.slice(0, removeStart)
|
||||||
Database.use((db) => db.delete(PartTable).where(eq(PartTable.id, part.id)).run())
|
const removeParts = target.parts.slice(removeStart)
|
||||||
await Bus.publish(MessageV2.Event.PartRemoved, {
|
target.parts = preserveParts
|
||||||
sessionID: sessionID,
|
for (const part of removeParts) {
|
||||||
messageID: last.info.id,
|
Database.use((db) => db.delete(PartTable).where(eq(PartTable.id, part.id)).run())
|
||||||
partID: part.id,
|
await Bus.publish(MessageV2.Event.PartRemoved, {
|
||||||
})
|
sessionID: sessionID,
|
||||||
|
messageID: target.info.id,
|
||||||
|
partID: part.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await Session.clearRevert(sessionID)
|
await Session.clearRevert(sessionID)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue