mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-21 08:04:45 +00:00
feat(id): brand ProjectID through Drizzle and Zod schemas (#16948)
This commit is contained in:
@@ -86,7 +86,7 @@ export const ImportCommand = cmd({
|
|||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
let exportData:
|
let exportData:
|
||||||
| {
|
| {
|
||||||
info: Session.Info
|
info: SDKSession
|
||||||
messages: Array<{
|
messages: Array<{
|
||||||
info: Message
|
info: Message
|
||||||
parts: Part[]
|
parts: Part[]
|
||||||
@@ -152,7 +152,7 @@ export const ImportCommand = cmd({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const row = { ...Session.toRow(exportData.info), project_id: Instance.project.id }
|
const row = Session.toRow({ ...exportData.info, projectID: Instance.project.id })
|
||||||
Database.use((db) =>
|
Database.use((db) =>
|
||||||
db
|
db
|
||||||
.insert(SessionTable)
|
.insert(SessionTable)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Identifier } from "@/id/id"
|
import { Identifier } from "@/id/id"
|
||||||
|
import { ProjectID } from "@/project/schema"
|
||||||
|
|
||||||
export const WorkspaceInfo = z.object({
|
export const WorkspaceInfo = z.object({
|
||||||
id: Identifier.schema("workspace"),
|
id: Identifier.schema("workspace"),
|
||||||
@@ -8,7 +9,7 @@ export const WorkspaceInfo = z.object({
|
|||||||
name: z.string().nullable(),
|
name: z.string().nullable(),
|
||||||
directory: z.string().nullable(),
|
directory: z.string().nullable(),
|
||||||
extra: z.unknown().nullable(),
|
extra: z.unknown().nullable(),
|
||||||
projectID: z.string(),
|
projectID: ProjectID.zod,
|
||||||
})
|
})
|
||||||
export type WorkspaceInfo = z.infer<typeof WorkspaceInfo>
|
export type WorkspaceInfo = z.infer<typeof WorkspaceInfo>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { sqliteTable, text } from "drizzle-orm/sqlite-core"
|
import { sqliteTable, text } from "drizzle-orm/sqlite-core"
|
||||||
import { ProjectTable } from "../project/project.sql"
|
import { ProjectTable } from "../project/project.sql"
|
||||||
|
import type { ProjectID } from "../project/schema"
|
||||||
|
|
||||||
export const WorkspaceTable = sqliteTable("workspace", {
|
export const WorkspaceTable = sqliteTable("workspace", {
|
||||||
id: text().primaryKey(),
|
id: text().primaryKey(),
|
||||||
@@ -9,6 +10,7 @@ export const WorkspaceTable = sqliteTable("workspace", {
|
|||||||
directory: text(),
|
directory: text(),
|
||||||
extra: text({ mode: "json" }),
|
extra: text({ mode: "json" }),
|
||||||
project_id: text()
|
project_id: text()
|
||||||
|
.$type<ProjectID>()
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => ProjectTable.id, { onDelete: "cascade" }),
|
.references(() => ProjectTable.id, { onDelete: "cascade" }),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Project } from "@/project/project"
|
|||||||
import { BusEvent } from "@/bus/bus-event"
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
import { GlobalBus } from "@/bus/global"
|
import { GlobalBus } from "@/bus/global"
|
||||||
import { Log } from "@/util/log"
|
import { Log } from "@/util/log"
|
||||||
|
import { ProjectID } from "@/project/schema"
|
||||||
import { WorkspaceTable } from "./workspace.sql"
|
import { WorkspaceTable } from "./workspace.sql"
|
||||||
import { getAdaptor } from "./adaptors"
|
import { getAdaptor } from "./adaptors"
|
||||||
import { WorkspaceInfo } from "./types"
|
import { WorkspaceInfo } from "./types"
|
||||||
@@ -48,7 +49,7 @@ export namespace Workspace {
|
|||||||
id: Identifier.schema("workspace").optional(),
|
id: Identifier.schema("workspace").optional(),
|
||||||
type: Info.shape.type,
|
type: Info.shape.type,
|
||||||
branch: Info.shape.branch,
|
branch: Info.shape.branch,
|
||||||
projectID: Info.shape.projectID,
|
projectID: ProjectID.zod,
|
||||||
extra: Info.shape.extra,
|
extra: Info.shape.extra,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Database, eq } from "@/storage/db"
|
|||||||
import { PermissionTable } from "@/session/session.sql"
|
import { PermissionTable } from "@/session/session.sql"
|
||||||
import { fn } from "@/util/fn"
|
import { fn } from "@/util/fn"
|
||||||
import { Log } from "@/util/log"
|
import { Log } from "@/util/log"
|
||||||
|
import { ProjectID } from "@/project/schema"
|
||||||
import { Wildcard } from "@/util/wildcard"
|
import { Wildcard } from "@/util/wildcard"
|
||||||
import os from "os"
|
import os from "os"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
@@ -90,7 +91,7 @@ export namespace PermissionNext {
|
|||||||
export type Reply = z.infer<typeof Reply>
|
export type Reply = z.infer<typeof Reply>
|
||||||
|
|
||||||
export const Approval = z.object({
|
export const Approval = z.object({
|
||||||
projectID: z.string(),
|
projectID: ProjectID.zod,
|
||||||
patterns: z.string().array(),
|
patterns: z.string().array(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
|
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
|
||||||
import { Timestamps } from "../storage/schema.sql"
|
import { Timestamps } from "../storage/schema.sql"
|
||||||
|
import type { ProjectID } from "./schema"
|
||||||
|
|
||||||
export const ProjectTable = sqliteTable("project", {
|
export const ProjectTable = sqliteTable("project", {
|
||||||
id: text().primaryKey(),
|
id: text().$type<ProjectID>().primaryKey(),
|
||||||
worktree: text().notNull(),
|
worktree: text().notNull(),
|
||||||
vcs: text(),
|
vcs: text(),
|
||||||
name: text(),
|
name: text(),
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { existsSync } from "fs"
|
|||||||
import { git } from "../util/git"
|
import { git } from "../util/git"
|
||||||
import { Glob } from "../util/glob"
|
import { Glob } from "../util/glob"
|
||||||
import { which } from "../util/which"
|
import { which } from "../util/which"
|
||||||
|
import { ProjectID } from "./schema"
|
||||||
|
|
||||||
export namespace Project {
|
export namespace Project {
|
||||||
const log = Log.create({ service: "project" })
|
const log = Log.create({ service: "project" })
|
||||||
@@ -33,7 +34,7 @@ export namespace Project {
|
|||||||
|
|
||||||
export const Info = z
|
export const Info = z
|
||||||
.object({
|
.object({
|
||||||
id: z.string(),
|
id: ProjectID.zod,
|
||||||
worktree: z.string(),
|
worktree: z.string(),
|
||||||
vcs: z.literal("git").optional(),
|
vcs: z.literal("git").optional(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
@@ -73,7 +74,7 @@ export namespace Project {
|
|||||||
? { url: row.icon_url ?? undefined, color: row.icon_color ?? undefined }
|
? { url: row.icon_url ?? undefined, color: row.icon_color ?? undefined }
|
||||||
: undefined
|
: undefined
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: ProjectID.make(row.id),
|
||||||
worktree: row.worktree,
|
worktree: row.worktree,
|
||||||
vcs: row.vcs ? Info.shape.vcs.parse(row.vcs) : undefined,
|
vcs: row.vcs ? Info.shape.vcs.parse(row.vcs) : undefined,
|
||||||
name: row.name ?? undefined,
|
name: row.name ?? undefined,
|
||||||
@@ -91,6 +92,7 @@ export namespace Project {
|
|||||||
function readCachedId(dir: string) {
|
function readCachedId(dir: string) {
|
||||||
return Filesystem.readText(path.join(dir, "opencode"))
|
return Filesystem.readText(path.join(dir, "opencode"))
|
||||||
.then((x) => x.trim())
|
.then((x) => x.trim())
|
||||||
|
.then(ProjectID.make)
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +113,7 @@ export namespace Project {
|
|||||||
|
|
||||||
if (!gitBinary) {
|
if (!gitBinary) {
|
||||||
return {
|
return {
|
||||||
id: id ?? "global",
|
id: id ?? ProjectID.global,
|
||||||
worktree: sandbox,
|
worktree: sandbox,
|
||||||
sandbox,
|
sandbox,
|
||||||
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
|
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
|
||||||
@@ -130,7 +132,7 @@ export namespace Project {
|
|||||||
|
|
||||||
if (!worktree) {
|
if (!worktree) {
|
||||||
return {
|
return {
|
||||||
id: id ?? "global",
|
id: id ?? ProjectID.global,
|
||||||
worktree: sandbox,
|
worktree: sandbox,
|
||||||
sandbox,
|
sandbox,
|
||||||
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
|
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
|
||||||
@@ -160,14 +162,14 @@ export namespace Project {
|
|||||||
|
|
||||||
if (!roots) {
|
if (!roots) {
|
||||||
return {
|
return {
|
||||||
id: "global",
|
id: ProjectID.global,
|
||||||
worktree: sandbox,
|
worktree: sandbox,
|
||||||
sandbox,
|
sandbox,
|
||||||
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
|
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
id = roots[0]
|
id = roots[0] ? ProjectID.make(roots[0]) : undefined
|
||||||
if (id) {
|
if (id) {
|
||||||
await Filesystem.write(path.join(dotgit, "opencode"), id).catch(() => undefined)
|
await Filesystem.write(path.join(dotgit, "opencode"), id).catch(() => undefined)
|
||||||
}
|
}
|
||||||
@@ -175,7 +177,7 @@ export namespace Project {
|
|||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return {
|
return {
|
||||||
id: "global",
|
id: ProjectID.global,
|
||||||
worktree: sandbox,
|
worktree: sandbox,
|
||||||
sandbox,
|
sandbox,
|
||||||
vcs: "git",
|
vcs: "git",
|
||||||
@@ -208,7 +210,7 @@ export namespace Project {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: "global",
|
id: ProjectID.global,
|
||||||
worktree: "/",
|
worktree: "/",
|
||||||
sandbox: "/",
|
sandbox: "/",
|
||||||
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
|
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
|
||||||
@@ -228,7 +230,7 @@ export namespace Project {
|
|||||||
updated: Date.now(),
|
updated: Date.now(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if (data.id !== "global") {
|
if (data.id !== ProjectID.global) {
|
||||||
await migrateFromGlobal(data.id, data.worktree)
|
await migrateFromGlobal(data.id, data.worktree)
|
||||||
}
|
}
|
||||||
return fresh
|
return fresh
|
||||||
@@ -308,12 +310,12 @@ export namespace Project {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
async function migrateFromGlobal(id: string, worktree: string) {
|
async function migrateFromGlobal(id: ProjectID, worktree: string) {
|
||||||
const row = 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, ProjectID.global)).get())
|
||||||
if (!row) return
|
if (!row) return
|
||||||
|
|
||||||
const sessions = 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, ProjectID.global)).all(),
|
||||||
)
|
)
|
||||||
if (sessions.length === 0) return
|
if (sessions.length === 0) return
|
||||||
|
|
||||||
@@ -323,14 +325,14 @@ export namespace Project {
|
|||||||
// 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: id })
|
log.info("migrating session", { sessionID: row.id, from: ProjectID.global, to: id })
|
||||||
Database.use((db) => db.update(SessionTable).set({ project_id: id }).where(eq(SessionTable.id, row.id)).run())
|
Database.use((db) => db.update(SessionTable).set({ project_id: id }).where(eq(SessionTable.id, row.id)).run())
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
log.error("failed to migrate sessions from global to project", { error, projectId: id })
|
log.error("failed to migrate sessions from global to project", { error, projectId: id })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setInitialized(id: string) {
|
export function setInitialized(id: ProjectID) {
|
||||||
Database.use((db) =>
|
Database.use((db) =>
|
||||||
db
|
db
|
||||||
.update(ProjectTable)
|
.update(ProjectTable)
|
||||||
@@ -352,7 +354,7 @@ export namespace Project {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function get(id: string): Info | undefined {
|
export function get(id: ProjectID): Info | undefined {
|
||||||
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, id)).get())
|
||||||
if (!row) return undefined
|
if (!row) return undefined
|
||||||
return fromRow(row)
|
return fromRow(row)
|
||||||
@@ -375,12 +377,13 @@ export namespace Project {
|
|||||||
|
|
||||||
export const update = fn(
|
export const update = fn(
|
||||||
z.object({
|
z.object({
|
||||||
projectID: z.string(),
|
projectID: ProjectID.zod,
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
icon: Info.shape.icon.optional(),
|
icon: Info.shape.icon.optional(),
|
||||||
commands: Info.shape.commands.optional(),
|
commands: Info.shape.commands.optional(),
|
||||||
}),
|
}),
|
||||||
async (input) => {
|
async (input) => {
|
||||||
|
const id = ProjectID.make(input.projectID)
|
||||||
const result = Database.use((db) =>
|
const result = Database.use((db) =>
|
||||||
db
|
db
|
||||||
.update(ProjectTable)
|
.update(ProjectTable)
|
||||||
@@ -391,7 +394,7 @@ export namespace Project {
|
|||||||
commands: input.commands,
|
commands: input.commands,
|
||||||
time_updated: Date.now(),
|
time_updated: Date.now(),
|
||||||
})
|
})
|
||||||
.where(eq(ProjectTable.id, input.projectID))
|
.where(eq(ProjectTable.id, id))
|
||||||
.returning()
|
.returning()
|
||||||
.get(),
|
.get(),
|
||||||
)
|
)
|
||||||
@@ -407,7 +410,7 @@ export namespace Project {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
export async function sandboxes(id: string) {
|
export async function sandboxes(id: ProjectID) {
|
||||||
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, id)).get())
|
||||||
if (!row) return []
|
if (!row) return []
|
||||||
const data = fromRow(row)
|
const data = fromRow(row)
|
||||||
@@ -419,7 +422,7 @@ export namespace Project {
|
|||||||
return valid
|
return valid
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addSandbox(id: string, directory: string) {
|
export async function addSandbox(id: ProjectID, directory: string) {
|
||||||
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, id)).get())
|
||||||
if (!row) throw new Error(`Project not found: ${id}`)
|
if (!row) throw new Error(`Project not found: ${id}`)
|
||||||
const sandboxes = [...row.sandboxes]
|
const sandboxes = [...row.sandboxes]
|
||||||
@@ -443,7 +446,7 @@ export namespace Project {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeSandbox(id: string, directory: string) {
|
export async function removeSandbox(id: ProjectID, directory: string) {
|
||||||
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, id)).get())
|
||||||
if (!row) throw new Error(`Project not found: ${id}`)
|
if (!row) throw new Error(`Project not found: ${id}`)
|
||||||
const sandboxes = row.sandboxes.filter((s) => s !== directory)
|
const sandboxes = row.sandboxes.filter((s) => s !== directory)
|
||||||
|
|||||||
16
packages/opencode/src/project/schema.ts
Normal file
16
packages/opencode/src/project/schema.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Schema } from "effect"
|
||||||
|
import z from "zod"
|
||||||
|
|
||||||
|
import { withStatics } from "@/util/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.makeUnsafe("global"),
|
||||||
|
make: (id: string) => schema.makeUnsafe(id),
|
||||||
|
zod: z.string().pipe(z.custom<ProjectID>()),
|
||||||
|
})),
|
||||||
|
)
|
||||||
@@ -4,6 +4,7 @@ import { resolver } from "hono-openapi"
|
|||||||
import { Instance } from "../../project/instance"
|
import { Instance } from "../../project/instance"
|
||||||
import { Project } from "../../project/project"
|
import { Project } from "../../project/project"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
|
import { ProjectID } from "../../project/schema"
|
||||||
import { errors } from "../error"
|
import { errors } from "../error"
|
||||||
import { lazy } from "../../util/lazy"
|
import { lazy } from "../../util/lazy"
|
||||||
import { InstanceBootstrap } from "../../project/bootstrap"
|
import { InstanceBootstrap } from "../../project/bootstrap"
|
||||||
@@ -105,7 +106,7 @@ export const ProjectRoutes = lazy(() =>
|
|||||||
...errors(400, 404),
|
...errors(400, 404),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
validator("param", z.object({ projectID: z.string() })),
|
validator("param", z.object({ projectID: ProjectID.zod })),
|
||||||
validator("json", Project.update.schema.omit({ projectID: true })),
|
validator("json", Project.update.schema.omit({ projectID: true })),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const projectID = c.req.valid("param").projectID
|
const projectID = c.req.valid("param").projectID
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { fn } from "@/util/fn"
|
|||||||
import { Command } from "../command"
|
import { Command } from "../command"
|
||||||
import { Snapshot } from "@/snapshot"
|
import { Snapshot } from "@/snapshot"
|
||||||
import { WorkspaceContext } from "../control-plane/workspace-context"
|
import { WorkspaceContext } from "../control-plane/workspace-context"
|
||||||
|
import { ProjectID } from "../project/schema"
|
||||||
|
|
||||||
import type { Provider } from "@/provider/provider"
|
import type { Provider } from "@/provider/provider"
|
||||||
import { PermissionNext } from "@/permission/next"
|
import { PermissionNext } from "@/permission/next"
|
||||||
@@ -120,7 +121,7 @@ export namespace Session {
|
|||||||
.object({
|
.object({
|
||||||
id: Identifier.schema("session"),
|
id: Identifier.schema("session"),
|
||||||
slug: z.string(),
|
slug: z.string(),
|
||||||
projectID: z.string(),
|
projectID: ProjectID.zod,
|
||||||
workspaceID: z.string().optional(),
|
workspaceID: z.string().optional(),
|
||||||
directory: z.string(),
|
directory: z.string(),
|
||||||
parentID: Identifier.schema("session").optional(),
|
parentID: Identifier.schema("session").optional(),
|
||||||
@@ -162,7 +163,7 @@ export namespace Session {
|
|||||||
|
|
||||||
export const ProjectInfo = z
|
export const ProjectInfo = z
|
||||||
.object({
|
.object({
|
||||||
id: z.string(),
|
id: ProjectID.zod,
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
worktree: z.string(),
|
worktree: z.string(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ProjectTable } from "../project/project.sql"
|
|||||||
import type { MessageV2 } from "./message-v2"
|
import type { MessageV2 } from "./message-v2"
|
||||||
import type { Snapshot } from "../snapshot"
|
import type { Snapshot } from "../snapshot"
|
||||||
import type { PermissionNext } from "../permission/next"
|
import type { PermissionNext } from "../permission/next"
|
||||||
|
import type { ProjectID } from "../project/schema"
|
||||||
import { Timestamps } from "../storage/schema.sql"
|
import { Timestamps } from "../storage/schema.sql"
|
||||||
|
|
||||||
type PartData = Omit<MessageV2.Part, "id" | "sessionID" | "messageID">
|
type PartData = Omit<MessageV2.Part, "id" | "sessionID" | "messageID">
|
||||||
@@ -13,6 +14,7 @@ export const SessionTable = sqliteTable(
|
|||||||
{
|
{
|
||||||
id: text().primaryKey(),
|
id: text().primaryKey(),
|
||||||
project_id: text()
|
project_id: text()
|
||||||
|
.$type<ProjectID>()
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => ProjectTable.id, { onDelete: "cascade" }),
|
.references(() => ProjectTable.id, { onDelete: "cascade" }),
|
||||||
workspace_id: text(),
|
workspace_id: text(),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { InstanceBootstrap } from "../project/bootstrap"
|
|||||||
import { Project } from "../project/project"
|
import { Project } from "../project/project"
|
||||||
import { Database, eq } from "../storage/db"
|
import { Database, eq } from "../storage/db"
|
||||||
import { ProjectTable } from "../project/project.sql"
|
import { ProjectTable } from "../project/project.sql"
|
||||||
|
import type { ProjectID } from "../project/schema"
|
||||||
import { fn } from "../util/fn"
|
import { fn } from "../util/fn"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { Process } from "../util/process"
|
import { Process } from "../util/process"
|
||||||
@@ -310,7 +311,7 @@ export namespace Worktree {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runStartScripts(directory: string, input: { projectID: string; extra?: string }) {
|
async function runStartScripts(directory: string, input: { projectID: ProjectID; extra?: string }) {
|
||||||
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, input.projectID)).get())
|
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, input.projectID)).get())
|
||||||
const project = row ? Project.fromRow(row) : undefined
|
const project = row ? Project.fromRow(row) : undefined
|
||||||
const startup = project?.commands?.start?.trim() ?? ""
|
const startup = project?.commands?.start?.trim() ?? ""
|
||||||
@@ -322,7 +323,7 @@ export namespace Worktree {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
function queueStartScripts(directory: string, input: { projectID: string; extra?: string }) {
|
function queueStartScripts(directory: string, input: { projectID: ProjectID; extra?: string }) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const start = async () => {
|
const start = async () => {
|
||||||
await runStartScripts(directory, input)
|
await runStartScripts(directory, input)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import path from "path"
|
|||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import { pathToFileURL } from "url"
|
import { pathToFileURL } from "url"
|
||||||
import { Global } from "../../src/global"
|
import { Global } from "../../src/global"
|
||||||
|
import { ProjectID } from "../../src/project/schema"
|
||||||
import { Filesystem } from "../../src/util/filesystem"
|
import { Filesystem } from "../../src/util/filesystem"
|
||||||
|
|
||||||
// Get managed config directory from environment (set in preload.ts)
|
// Get managed config directory from environment (set in preload.ts)
|
||||||
@@ -44,7 +45,7 @@ async function check(map: (dir: string) => string) {
|
|||||||
const cfg = await Config.get()
|
const cfg = await Config.get()
|
||||||
expect(cfg.snapshot).toBe(true)
|
expect(cfg.snapshot).toBe(true)
|
||||||
expect(Instance.directory).toBe(Filesystem.resolve(tmp.path))
|
expect(Instance.directory).toBe(Filesystem.resolve(tmp.path))
|
||||||
expect(Instance.project.id).not.toBe("global")
|
expect(Instance.project.id).not.toBe(ProjectID.global)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import path from "path"
|
|||||||
import { tmpdir } from "../fixture/fixture"
|
import { tmpdir } from "../fixture/fixture"
|
||||||
import { Filesystem } from "../../src/util/filesystem"
|
import { Filesystem } from "../../src/util/filesystem"
|
||||||
import { GlobalBus } from "../../src/bus/global"
|
import { GlobalBus } from "../../src/bus/global"
|
||||||
|
import { ProjectID } from "../../src/project/schema"
|
||||||
|
|
||||||
Log.init({ print: false })
|
Log.init({ print: false })
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ describe("Project.fromDirectory", () => {
|
|||||||
const { project } = await p.fromDirectory(tmp.path)
|
const { project } = await p.fromDirectory(tmp.path)
|
||||||
|
|
||||||
expect(project).toBeDefined()
|
expect(project).toBeDefined()
|
||||||
expect(project.id).toBe("global")
|
expect(project.id).toBe(ProjectID.global)
|
||||||
expect(project.vcs).toBe("git")
|
expect(project.vcs).toBe("git")
|
||||||
expect(project.worktree).toBe(tmp.path)
|
expect(project.worktree).toBe(tmp.path)
|
||||||
|
|
||||||
@@ -90,7 +91,7 @@ describe("Project.fromDirectory", () => {
|
|||||||
const { project } = await p.fromDirectory(tmp.path)
|
const { project } = await p.fromDirectory(tmp.path)
|
||||||
|
|
||||||
expect(project).toBeDefined()
|
expect(project).toBeDefined()
|
||||||
expect(project.id).not.toBe("global")
|
expect(project.id).not.toBe(ProjectID.global)
|
||||||
expect(project.vcs).toBe("git")
|
expect(project.vcs).toBe("git")
|
||||||
expect(project.worktree).toBe(tmp.path)
|
expect(project.worktree).toBe(tmp.path)
|
||||||
|
|
||||||
@@ -107,7 +108,7 @@ describe("Project.fromDirectory", () => {
|
|||||||
await withMode("rev-list-fail", async () => {
|
await withMode("rev-list-fail", async () => {
|
||||||
const { project } = await p.fromDirectory(tmp.path)
|
const { project } = await p.fromDirectory(tmp.path)
|
||||||
expect(project.vcs).toBe("git")
|
expect(project.vcs).toBe("git")
|
||||||
expect(project.id).toBe("global")
|
expect(project.id).toBe(ProjectID.global)
|
||||||
expect(project.worktree).toBe(tmp.path)
|
expect(project.worktree).toBe(tmp.path)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -301,7 +302,7 @@ describe("Project.update", () => {
|
|||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
Project.update({
|
Project.update({
|
||||||
projectID: "nonexistent-project-id",
|
projectID: ProjectID.make("nonexistent-project-id"),
|
||||||
name: "Should Fail",
|
name: "Should Fail",
|
||||||
}),
|
}),
|
||||||
).rejects.toThrow("Project not found: nonexistent-project-id")
|
).rejects.toThrow("Project not found: nonexistent-project-id")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { readFileSync, readdirSync } from "fs"
|
|||||||
import { JsonMigration } from "../../src/storage/json-migration"
|
import { JsonMigration } from "../../src/storage/json-migration"
|
||||||
import { Global } from "../../src/global"
|
import { Global } from "../../src/global"
|
||||||
import { ProjectTable } from "../../src/project/project.sql"
|
import { ProjectTable } from "../../src/project/project.sql"
|
||||||
|
import { ProjectID } from "../../src/project/schema"
|
||||||
import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../../src/session/session.sql"
|
import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../../src/session/session.sql"
|
||||||
import { SessionShareTable } from "../../src/share/share.sql"
|
import { SessionShareTable } from "../../src/share/share.sql"
|
||||||
|
|
||||||
@@ -123,7 +124,7 @@ describe("JSON to SQLite migration", () => {
|
|||||||
const db = drizzle({ client: sqlite })
|
const db = drizzle({ client: sqlite })
|
||||||
const projects = db.select().from(ProjectTable).all()
|
const projects = db.select().from(ProjectTable).all()
|
||||||
expect(projects.length).toBe(1)
|
expect(projects.length).toBe(1)
|
||||||
expect(projects[0].id).toBe("proj_test123abc")
|
expect(projects[0].id).toBe(ProjectID.make("proj_test123abc"))
|
||||||
expect(projects[0].worktree).toBe("/test/path")
|
expect(projects[0].worktree).toBe("/test/path")
|
||||||
expect(projects[0].name).toBe("Test Project")
|
expect(projects[0].name).toBe("Test Project")
|
||||||
expect(projects[0].sandboxes).toEqual(["/test/sandbox"])
|
expect(projects[0].sandboxes).toEqual(["/test/sandbox"])
|
||||||
@@ -148,7 +149,7 @@ describe("JSON to SQLite migration", () => {
|
|||||||
const db = drizzle({ client: sqlite })
|
const db = drizzle({ client: sqlite })
|
||||||
const projects = db.select().from(ProjectTable).all()
|
const projects = db.select().from(ProjectTable).all()
|
||||||
expect(projects.length).toBe(1)
|
expect(projects.length).toBe(1)
|
||||||
expect(projects[0].id).toBe("proj_filename") // Uses filename, not JSON id
|
expect(projects[0].id).toBe(ProjectID.make("proj_filename")) // Uses filename, not JSON id
|
||||||
})
|
})
|
||||||
|
|
||||||
test("migrates project with commands", async () => {
|
test("migrates project with commands", async () => {
|
||||||
@@ -169,7 +170,7 @@ describe("JSON to SQLite migration", () => {
|
|||||||
const db = drizzle({ client: sqlite })
|
const db = drizzle({ client: sqlite })
|
||||||
const projects = db.select().from(ProjectTable).all()
|
const projects = db.select().from(ProjectTable).all()
|
||||||
expect(projects.length).toBe(1)
|
expect(projects.length).toBe(1)
|
||||||
expect(projects[0].id).toBe("proj_with_commands")
|
expect(projects[0].id).toBe(ProjectID.make("proj_with_commands"))
|
||||||
expect(projects[0].commands).toEqual({ start: "npm run dev" })
|
expect(projects[0].commands).toEqual({ start: "npm run dev" })
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -190,7 +191,7 @@ describe("JSON to SQLite migration", () => {
|
|||||||
const db = drizzle({ client: sqlite })
|
const db = drizzle({ client: sqlite })
|
||||||
const projects = db.select().from(ProjectTable).all()
|
const projects = db.select().from(ProjectTable).all()
|
||||||
expect(projects.length).toBe(1)
|
expect(projects.length).toBe(1)
|
||||||
expect(projects[0].id).toBe("proj_no_commands")
|
expect(projects[0].id).toBe(ProjectID.make("proj_no_commands"))
|
||||||
expect(projects[0].commands).toBeNull()
|
expect(projects[0].commands).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -220,7 +221,7 @@ describe("JSON to SQLite migration", () => {
|
|||||||
const sessions = db.select().from(SessionTable).all()
|
const sessions = db.select().from(SessionTable).all()
|
||||||
expect(sessions.length).toBe(1)
|
expect(sessions.length).toBe(1)
|
||||||
expect(sessions[0].id).toBe("ses_test456def")
|
expect(sessions[0].id).toBe("ses_test456def")
|
||||||
expect(sessions[0].project_id).toBe("proj_test123abc")
|
expect(sessions[0].project_id).toBe(ProjectID.make("proj_test123abc"))
|
||||||
expect(sessions[0].slug).toBe("test-session")
|
expect(sessions[0].slug).toBe("test-session")
|
||||||
expect(sessions[0].title).toBe("Test Session Title")
|
expect(sessions[0].title).toBe("Test Session Title")
|
||||||
expect(sessions[0].summary_additions).toBe(10)
|
expect(sessions[0].summary_additions).toBe(10)
|
||||||
@@ -426,7 +427,7 @@ describe("JSON to SQLite migration", () => {
|
|||||||
const sessions = db.select().from(SessionTable).all()
|
const sessions = db.select().from(SessionTable).all()
|
||||||
expect(sessions.length).toBe(1)
|
expect(sessions.length).toBe(1)
|
||||||
expect(sessions[0].id).toBe("ses_migrated")
|
expect(sessions[0].id).toBe("ses_migrated")
|
||||||
expect(sessions[0].project_id).toBe(gitBasedProjectID) // Uses directory, not stale JSON
|
expect(sessions[0].project_id).toBe(ProjectID.make(gitBasedProjectID)) // Uses directory, not stale JSON
|
||||||
})
|
})
|
||||||
|
|
||||||
test("uses filename for session id when JSON has different value", async () => {
|
test("uses filename for session id when JSON has different value", async () => {
|
||||||
@@ -458,7 +459,7 @@ describe("JSON to SQLite migration", () => {
|
|||||||
const sessions = db.select().from(SessionTable).all()
|
const sessions = db.select().from(SessionTable).all()
|
||||||
expect(sessions.length).toBe(1)
|
expect(sessions.length).toBe(1)
|
||||||
expect(sessions[0].id).toBe("ses_from_filename") // Uses filename, not JSON id
|
expect(sessions[0].id).toBe("ses_from_filename") // Uses filename, not JSON id
|
||||||
expect(sessions[0].project_id).toBe("proj_test123abc")
|
expect(sessions[0].project_id).toBe(ProjectID.make("proj_test123abc"))
|
||||||
})
|
})
|
||||||
|
|
||||||
test("is idempotent (running twice doesn't duplicate)", async () => {
|
test("is idempotent (running twice doesn't duplicate)", async () => {
|
||||||
@@ -643,7 +644,7 @@ describe("JSON to SQLite migration", () => {
|
|||||||
const db = drizzle({ client: sqlite })
|
const db = drizzle({ client: sqlite })
|
||||||
const projects = db.select().from(ProjectTable).all()
|
const projects = db.select().from(ProjectTable).all()
|
||||||
expect(projects.length).toBe(1)
|
expect(projects.length).toBe(1)
|
||||||
expect(projects[0].id).toBe("proj_test123abc")
|
expect(projects[0].id).toBe(ProjectID.make("proj_test123abc"))
|
||||||
})
|
})
|
||||||
|
|
||||||
test("skips invalid todo entries while preserving source positions", async () => {
|
test("skips invalid todo entries while preserving source positions", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user