refactor(core): centralize project and workspace schemas
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
|
||||
import { ProjectTable } from "../project/sql"
|
||||
import { Project } from "../project"
|
||||
import { ProjectV2 } from "../project"
|
||||
import { WorkspaceV2 } from "../workspace"
|
||||
|
||||
export const WorkspaceTable = sqliteTable("workspace", {
|
||||
@@ -11,7 +11,7 @@ export const WorkspaceTable = sqliteTable("workspace", {
|
||||
directory: text(),
|
||||
extra: text({ mode: "json" }),
|
||||
project_id: text()
|
||||
.$type<Project.ID>()
|
||||
.$type<ProjectV2.ID>()
|
||||
.notNull()
|
||||
.references(() => ProjectTable.id, { onDelete: "cascade" }),
|
||||
time_used: integer()
|
||||
|
||||
@@ -5,8 +5,9 @@ import { layer as sqliteLayer } from "#sqlite"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { Global } from "../global"
|
||||
import { Flag } from "../flag/flag"
|
||||
import path from "path"
|
||||
import { isAbsolute, join } from "path"
|
||||
import { DatabaseMigration } from "./migration"
|
||||
import { InstallationChannel } from "../installation/version"
|
||||
|
||||
const makeDatabase = EffectDrizzleSqlite.makeWithDefaults()
|
||||
type DatabaseShape = Effect.Success<typeof makeDatabase>
|
||||
@@ -24,24 +25,40 @@ const layer = Layer.effect(
|
||||
yield* db.run("PRAGMA cache_size = -64000")
|
||||
yield* db.run("PRAGMA foreign_keys = ON")
|
||||
yield* db.run("PRAGMA wal_checkpoint(PASSIVE)")
|
||||
yield* Effect.log("Applying database migrations")
|
||||
yield* DatabaseMigration.apply(db)
|
||||
|
||||
return db
|
||||
}),
|
||||
}).pipe(Effect.orDie),
|
||||
)
|
||||
|
||||
export function layerFromPath(filename: string) {
|
||||
return layer.pipe(Layer.provide(sqliteLayer({ filename })))
|
||||
return layer.pipe(Layer.provide(sqliteLayer({ filename })), Layer.orDie)
|
||||
}
|
||||
|
||||
export const memoryLayer = layerFromPath(":memory:")
|
||||
|
||||
export function path() {
|
||||
if (Flag.OPENCODE_DB) {
|
||||
if (Flag.OPENCODE_DB === ":memory:" || isAbsolute(Flag.OPENCODE_DB)) return Flag.OPENCODE_DB
|
||||
return join(Global.Path.data, Flag.OPENCODE_DB)
|
||||
}
|
||||
if (
|
||||
["latest", "beta", "prod"].includes(InstallationChannel) ||
|
||||
process.env.OPENCODE_DISABLE_CHANNEL_DB === "1" ||
|
||||
process.env.OPENCODE_DISABLE_CHANNEL_DB === "true"
|
||||
)
|
||||
return join(Global.Path.data, "opencode.db")
|
||||
return join(Global.Path.data, `opencode-${InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-")}.db`)
|
||||
}
|
||||
|
||||
export const defaultLayer = Layer.unwrap(
|
||||
Effect.gen(function* () {
|
||||
return layerFromPath(
|
||||
!Flag.OPENCODE_DB
|
||||
? path.join(Global.Path.data, "opencode.db")
|
||||
: Flag.OPENCODE_DB === ":memory:" || path.isAbsolute(Flag.OPENCODE_DB)
|
||||
? Flag.OPENCODE_DB
|
||||
: path.join(Global.Path.data, Flag.OPENCODE_DB),
|
||||
)
|
||||
return layerFromPath(path())
|
||||
}),
|
||||
).pipe(Layer.provide(Global.defaultLayer))
|
||||
|
||||
export function init(options: { path?: string } = {}) {
|
||||
const filename = options.path ?? path()
|
||||
return Effect.runSync(Service.use(() => Effect.void).pipe(Effect.provide(layerFromPath(filename))))
|
||||
}
|
||||
|
||||
@@ -19,19 +19,27 @@ export function apply(db: Database) {
|
||||
|
||||
export function applyOnly(db: Database, input: Migration[]) {
|
||||
return Effect.gen(function* () {
|
||||
yield* db.run(sql`CREATE TABLE IF NOT EXISTS ${sql.identifier("migration")} (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL)`)
|
||||
let completed = new Set((yield* db.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`)).map((row) => row.id))
|
||||
yield* db.run(
|
||||
sql`CREATE TABLE IF NOT EXISTS ${sql.identifier("migration")} (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL)`,
|
||||
)
|
||||
let completed = new Set(
|
||||
(yield* db.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`)).map((row) => row.id),
|
||||
)
|
||||
if (completed.size === 0) {
|
||||
// Existing installs used Drizzle's migration journal. Seed the new
|
||||
// journal once so TypeScript migrations don't replay old SQL.
|
||||
if (yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${"__drizzle_migrations"}`)) {
|
||||
if (
|
||||
yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${"__drizzle_migrations"}`)
|
||||
) {
|
||||
yield* db.run(sql`
|
||||
INSERT OR IGNORE INTO ${sql.identifier("migration")} (id, time_completed)
|
||||
SELECT name, ${Date.now()}
|
||||
FROM ${sql.identifier("__drizzle_migrations")}
|
||||
WHERE name IS NOT NULL
|
||||
`)
|
||||
completed = new Set((yield* db.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`)).map((row) => row.id))
|
||||
completed = new Set(
|
||||
(yield* db.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`)).map((row) => row.id),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +48,9 @@ export function applyOnly(db: Database, input: Migration[]) {
|
||||
yield* db.transaction((tx) =>
|
||||
Effect.gen(function* () {
|
||||
yield* migration.up(tx)
|
||||
yield* tx.run(sql`INSERT INTO ${sql.identifier("migration")} (id, time_completed) VALUES (${migration.id}, ${Date.now()})`)
|
||||
yield* tx.run(
|
||||
sql`INSERT INTO ${sql.identifier("migration")} (id, time_completed) VALUES (${migration.id}, ${Date.now()})`,
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * as Project from "./project"
|
||||
export * as ProjectV2 from "./project"
|
||||
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import path from "path"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
|
||||
import { Timestamps } from "../database/schema.sql"
|
||||
import { Project } from "../project"
|
||||
import { ProjectV2 } from "../project"
|
||||
|
||||
export const ProjectTable = sqliteTable("project", {
|
||||
id: text().$type<Project.ID>().primaryKey(),
|
||||
id: text().$type<ProjectV2.ID>().primaryKey(),
|
||||
worktree: text().notNull(),
|
||||
vcs: text(),
|
||||
name: text(),
|
||||
|
||||
@@ -3,7 +3,7 @@ export * from "./session/schema"
|
||||
|
||||
import { DateTime, Effect, Layer, Schema, Context } from "effect"
|
||||
import { and, asc, desc, eq, gt, gte, isNull, like, lt, or, type SQL } from "drizzle-orm"
|
||||
import { Project } from "./project"
|
||||
import { ProjectV2 } from "./project"
|
||||
import { WorkspaceV2 } from "./workspace"
|
||||
import { ModelV2 } from "./model"
|
||||
import { Location } from "./location"
|
||||
@@ -32,7 +32,7 @@ type Cursor = {
|
||||
|
||||
type ListInput = {
|
||||
workspaceID?: WorkspaceV2.ID
|
||||
projectID?: Project.ID
|
||||
projectID?: ProjectV2.ID
|
||||
path?: string
|
||||
roots?: boolean
|
||||
start?: number
|
||||
@@ -137,7 +137,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
|
||||
function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.Info {
|
||||
return new SessionSchema.Info({
|
||||
id: SessionSchema.ID.make(row.id),
|
||||
projectID: Project.ID.make(row.project_id),
|
||||
projectID: ProjectV2.ID.make(row.project_id),
|
||||
workspaceID: row.workspace_id ? WorkspaceV2.ID.make(row.workspace_id) : undefined,
|
||||
title: row.title,
|
||||
parentID: row.parent_id ? SessionSchema.ID.make(row.parent_id) : undefined,
|
||||
|
||||
@@ -3,7 +3,7 @@ export * as SessionSchema from "./schema"
|
||||
import { Schema } from "effect"
|
||||
import { Location } from "../location"
|
||||
import { ModelV2 } from "../model"
|
||||
import { Project } from "../project"
|
||||
import { ProjectV2 } from "../project"
|
||||
import { RelativePath, optionalOmitUndefined, withStatics } from "../schema"
|
||||
import { WorkspaceV2 } from "../workspace"
|
||||
import { Identifier } from "../util/identifier"
|
||||
@@ -28,14 +28,14 @@ export const LegacyInfo = Schema.Struct({
|
||||
id: ID,
|
||||
location: Location.Ref,
|
||||
subpath: RelativePath, // derived from location
|
||||
project: Project.ID, // derived from location
|
||||
project: ProjectV2.ID, // derived from location
|
||||
})
|
||||
export type LegacyInfo = typeof LegacyInfo.Type
|
||||
|
||||
export class Info extends Schema.Class<Info>("Session.Info")({
|
||||
id: ID,
|
||||
parentID: optionalOmitUndefined(ID),
|
||||
projectID: Project.ID,
|
||||
projectID: ProjectV2.ID,
|
||||
workspaceID: optionalOmitUndefined(WorkspaceV2.ID),
|
||||
path: optionalOmitUndefined(Schema.String),
|
||||
agent: optionalOmitUndefined(Schema.String),
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ProjectTable } from "../project/sql"
|
||||
import type { SessionMessage } from "./message"
|
||||
import type { Snapshot } from "../snapshot"
|
||||
import { PermissionV2 } from "../permission"
|
||||
import { Project } from "../project"
|
||||
import { ProjectV2 } from "../project"
|
||||
import type { SessionSchema } from "./schema"
|
||||
import type { MessageID, PartID } from "./legacy"
|
||||
import { WorkspaceV2 } from "../workspace"
|
||||
@@ -16,7 +16,7 @@ export const SessionTable = sqliteTable(
|
||||
{
|
||||
id: text().$type<SessionSchema.ID>().primaryKey(),
|
||||
project_id: text()
|
||||
.$type<Project.ID>()
|
||||
.$type<ProjectV2.ID>()
|
||||
.notNull()
|
||||
.references(() => ProjectTable.id, { onDelete: "cascade" }),
|
||||
workspace_id: text().$type<WorkspaceV2.ID>(),
|
||||
|
||||
@@ -6,6 +6,13 @@ import { Identifier } from "./util/identifier"
|
||||
|
||||
export const ID = Schema.String.pipe(
|
||||
Schema.brand("WorkspaceV2.ID"),
|
||||
withStatics((schema) => ({ create: () => schema.make("wrk_" + Identifier.ascending()) })),
|
||||
withStatics((schema) => ({
|
||||
ascending: (id?: string) => {
|
||||
if (!id) return schema.make("wrk_" + Identifier.ascending())
|
||||
if (!id.startsWith("wrk")) throw new Error(`ID ${id} does not start with wrk`)
|
||||
return schema.make(id)
|
||||
},
|
||||
create: () => schema.make("wrk_" + Identifier.ascending()),
|
||||
})),
|
||||
)
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
@@ -3,16 +3,16 @@ import { $ } from "bun"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Effect } from "effect"
|
||||
import { Project } from "@opencode-ai/core/project"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Hash } from "@opencode-ai/core/util/hash"
|
||||
import { tmpdir } from "./fixture/tmpdir"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const it = testEffect(Project.defaultLayer)
|
||||
const it = testEffect(ProjectV2.defaultLayer)
|
||||
|
||||
function remoteID(remote: string) {
|
||||
return Project.ID.make(Hash.fast(`git-remote:${remote}`))
|
||||
return ProjectV2.ID.make(Hash.fast(`git-remote:${remote}`))
|
||||
}
|
||||
|
||||
function abs(value: string) {
|
||||
@@ -44,11 +44,11 @@ describe("ProjectV2.resolve", () => {
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
const project = yield* Project.Service
|
||||
const project = yield* ProjectV2.Service
|
||||
|
||||
const result = yield* project.resolve(abs(tmp.path))
|
||||
|
||||
expect(result.id).toBe(Project.ID.make("global"))
|
||||
expect(result.id).toBe(ProjectV2.ID.make("global"))
|
||||
expect(path.resolve(result.directory)).toBe(path.resolve(tmp.path))
|
||||
expect(result.previous).toBeUndefined()
|
||||
expect(result.vcs).toBeUndefined()
|
||||
@@ -62,11 +62,11 @@ describe("ProjectV2.resolve", () => {
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(tmp.path))
|
||||
const project = yield* Project.Service
|
||||
const project = yield* ProjectV2.Service
|
||||
|
||||
const result = yield* project.resolve(abs(tmp.path))
|
||||
|
||||
expect(result.id).toBe(Project.ID.make("global"))
|
||||
expect(result.id).toBe(ProjectV2.ID.make("global"))
|
||||
expect(result.directory).toBe(yield* real(tmp.path))
|
||||
expect(result.previous).toBeUndefined()
|
||||
expect(result.vcs?.type).toBe("git")
|
||||
@@ -80,11 +80,11 @@ describe("ProjectV2.resolve", () => {
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(tmp.path, { commit: true }))
|
||||
const project = yield* Project.Service
|
||||
const project = yield* ProjectV2.Service
|
||||
|
||||
const result = yield* project.resolve(abs(tmp.path))
|
||||
|
||||
expect(result.id).toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
|
||||
expect(result.id).toBe(ProjectV2.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
|
||||
expect(result.directory).toBe(yield* real(tmp.path))
|
||||
expect(result.previous).toBeUndefined()
|
||||
expect(result.vcs?.type).toBe("git")
|
||||
@@ -98,12 +98,12 @@ describe("ProjectV2.resolve", () => {
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:Acme/App.git" }))
|
||||
const project = yield* Project.Service
|
||||
const project = yield* ProjectV2.Service
|
||||
|
||||
const result = yield* project.resolve(abs(tmp.path))
|
||||
|
||||
expect(result.id).toBe(remoteID("github.com/Acme/App"))
|
||||
expect(result.id).not.toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
|
||||
expect(result.id).not.toBe(ProjectV2.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
|
||||
expect(result.directory).toBe(yield* real(tmp.path))
|
||||
expect(result.vcs?.type).toBe("git")
|
||||
}),
|
||||
@@ -121,7 +121,7 @@ describe("ProjectV2.resolve", () => {
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(ssh.path, { commit: true, remote: "git@github.com:owner/repo.git" }))
|
||||
yield* Effect.promise(() => initRepo(https.path, { commit: true, remote: "https://github.com/owner/repo.git" }))
|
||||
const project = yield* Project.Service
|
||||
const project = yield* ProjectV2.Service
|
||||
|
||||
const a = yield* project.resolve(abs(ssh.path))
|
||||
const b = yield* project.resolve(abs(https.path))
|
||||
@@ -138,11 +138,11 @@ describe("ProjectV2.resolve", () => {
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: `file://${tmp.path}` }))
|
||||
const project = yield* Project.Service
|
||||
const project = yield* ProjectV2.Service
|
||||
|
||||
const result = yield* project.resolve(abs(tmp.path))
|
||||
|
||||
expect(result.id).toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
|
||||
expect(result.id).toBe(ProjectV2.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -154,11 +154,11 @@ describe("ProjectV2.resolve", () => {
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:owner/repo.git" }))
|
||||
yield* Effect.promise(() => Bun.write(path.join(tmp.path, ".git", "opencode"), "old-id"))
|
||||
const project = yield* Project.Service
|
||||
const project = yield* ProjectV2.Service
|
||||
|
||||
const result = yield* project.resolve(abs(tmp.path))
|
||||
|
||||
expect(result.previous).toBe(Project.ID.make("old-id"))
|
||||
expect(result.previous).toBe(ProjectV2.ID.make("old-id"))
|
||||
expect(result.id).toBe(remoteID("github.com/owner/repo"))
|
||||
}),
|
||||
)
|
||||
@@ -170,7 +170,7 @@ describe("ProjectV2.resolve", () => {
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:owner/repo.git" }))
|
||||
const project = yield* Project.Service
|
||||
const project = yield* ProjectV2.Service
|
||||
|
||||
yield* project.resolve(abs(tmp.path))
|
||||
|
||||
@@ -186,7 +186,7 @@ describe("ProjectV2.resolve", () => {
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(tmp.path, { commit: true }))
|
||||
yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "a", "b"), { recursive: true }))
|
||||
const project = yield* Project.Service
|
||||
const project = yield* ProjectV2.Service
|
||||
|
||||
const result = yield* project.resolve(abs(path.join(tmp.path, "a", "b")))
|
||||
|
||||
@@ -207,12 +207,12 @@ describe("ProjectV2.resolve", () => {
|
||||
yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:owner/repo.git" }))
|
||||
yield* Effect.promise(() => Bun.write(path.join(tmp.path, ".git", "opencode"), "old-id"))
|
||||
yield* Effect.promise(() => $`git worktree add ${worktree} -b test-${Date.now()}`.cwd(tmp.path).quiet())
|
||||
const project = yield* Project.Service
|
||||
const project = yield* ProjectV2.Service
|
||||
|
||||
const result = yield* project.resolve(abs(worktree))
|
||||
|
||||
expect(result.directory).toBe(yield* real(worktree))
|
||||
expect(result.previous).toBe(Project.ID.make("old-id"))
|
||||
expect(result.previous).toBe(ProjectV2.ID.make("old-id"))
|
||||
expect(result.id).toBe(remoteID("github.com/owner/repo"))
|
||||
expect(result.vcs?.type).toBe("git")
|
||||
}),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ProjectID } from "@/project/schema"
|
||||
import type { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import type { WorkspaceAdapter, WorkspaceAdapterEntry } from "../types"
|
||||
import { WorktreeAdapter } from "./worktree"
|
||||
|
||||
@@ -6,9 +6,9 @@ const BUILTIN: Record<string, WorkspaceAdapter> = {
|
||||
worktree: WorktreeAdapter,
|
||||
}
|
||||
|
||||
const state = new Map<ProjectID, Map<string, WorkspaceAdapter>>()
|
||||
const state = new Map<ProjectV2.ID, Map<string, WorkspaceAdapter>>()
|
||||
|
||||
export function getAdapter(projectID: ProjectID, type: string): WorkspaceAdapter {
|
||||
export function getAdapter(projectID: ProjectV2.ID, type: string): WorkspaceAdapter {
|
||||
const custom = state.get(projectID)?.get(type)
|
||||
if (custom) return custom
|
||||
|
||||
@@ -18,7 +18,7 @@ export function getAdapter(projectID: ProjectID, type: string): WorkspaceAdapter
|
||||
throw new Error(`Unknown workspace adapter: ${type}`)
|
||||
}
|
||||
|
||||
export function listAdapters(projectID: ProjectID): WorkspaceAdapterEntry[] {
|
||||
export function listAdapters(projectID: ProjectV2.ID): WorkspaceAdapterEntry[] {
|
||||
return registeredAdapters(projectID).map(([type, adapter]) => ({
|
||||
type,
|
||||
name: adapter.name,
|
||||
@@ -26,15 +26,15 @@ export function listAdapters(projectID: ProjectID): WorkspaceAdapterEntry[] {
|
||||
}))
|
||||
}
|
||||
|
||||
export function registeredAdapters(projectID: ProjectID): [string, WorkspaceAdapter][] {
|
||||
export function registeredAdapters(projectID: ProjectV2.ID): [string, WorkspaceAdapter][] {
|
||||
const adapters = new Map(Object.entries(BUILTIN))
|
||||
for (const [type, adapter] of state.get(projectID)?.entries() ?? []) adapters.set(type, adapter)
|
||||
return [...adapters.entries()]
|
||||
}
|
||||
|
||||
// Plugins can be loaded per-project so we need to scope them. If you
|
||||
// want to install a global one pass `ProjectID.global`
|
||||
export function registerAdapter(projectID: ProjectID, type: string, adapter: WorkspaceAdapter) {
|
||||
// want to install a global one pass `ProjectV2.ID.global`
|
||||
export function registerAdapter(projectID: ProjectV2.ID, type: string, adapter: WorkspaceAdapter) {
|
||||
const adapters = state.get(projectID) ?? new Map<string, WorkspaceAdapter>()
|
||||
adapters.set(type, adapter)
|
||||
state.set(projectID, adapters)
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Schema } from "effect"
|
||||
|
||||
import { Identifier } from "@/id/id"
|
||||
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import { withStatics } from "@opencode-ai/core/schema"
|
||||
|
||||
export const WorkspaceID = WorkspaceV2.ID.pipe(
|
||||
withStatics((schema: typeof WorkspaceV2.ID) => ({
|
||||
ascending: (id?: string) => schema.make(Identifier.ascending("workspace", id)),
|
||||
})),
|
||||
)
|
||||
export type WorkspaceID = typeof WorkspaceID.Type
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Schema, Struct } from "effect"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import type { InstanceContext } from "@/project/instance-context"
|
||||
import { WorkspaceID } from "./schema"
|
||||
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import type { DeepMutable } from "@opencode-ai/core/schema"
|
||||
|
||||
export const WorkspaceInfo = Schema.Struct({
|
||||
id: WorkspaceID,
|
||||
id: WorkspaceV2.ID,
|
||||
type: Schema.String,
|
||||
name: Schema.String,
|
||||
branch: Schema.optional(Schema.NullOr(Schema.String)),
|
||||
directory: Schema.optional(Schema.NullOr(Schema.String)),
|
||||
extra: Schema.optional(Schema.NullOr(Schema.Unknown)),
|
||||
projectID: ProjectID,
|
||||
projectID: ProjectV2.ID,
|
||||
}).annotate({ identifier: "Workspace" })
|
||||
export type WorkspaceInfo = DeepMutable<Schema.Schema.Type<typeof WorkspaceInfo>>
|
||||
|
||||
@@ -40,7 +40,7 @@ export type Target =
|
||||
|
||||
export type WorkspaceAdapterContext = {
|
||||
readonly instance?: InstanceContext
|
||||
readonly workspaceID?: WorkspaceID
|
||||
readonly workspaceID?: WorkspaceV2.ID
|
||||
}
|
||||
|
||||
export type WorkspaceAdapter = {
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { LocalContext } from "@/util/local-context"
|
||||
import type { WorkspaceID } from "../control-plane/schema"
|
||||
import type { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
|
||||
export interface WorkspaceContext {
|
||||
workspaceID: WorkspaceID | undefined
|
||||
workspaceID: WorkspaceV2.ID | undefined
|
||||
}
|
||||
|
||||
const context = LocalContext.create<WorkspaceContext>("instance")
|
||||
|
||||
export const WorkspaceContext = {
|
||||
async provide<R>(input: { workspaceID?: WorkspaceID; fn: () => R }): Promise<R> {
|
||||
async provide<R>(input: { workspaceID?: WorkspaceV2.ID; fn: () => R }): Promise<R> {
|
||||
return context.provide({ workspaceID: input.workspaceID }, () => input.fn())
|
||||
},
|
||||
|
||||
restore<R>(workspaceID: WorkspaceID, fn: () => R): R {
|
||||
restore<R>(workspaceID: WorkspaceV2.ID, fn: () => R): R {
|
||||
return context.provide({ workspaceID }, fn)
|
||||
},
|
||||
|
||||
|
||||
@@ -14,12 +14,12 @@ import { EventSequenceTable, EventTable } from "@opencode-ai/core/event/sql"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { Slug } from "@opencode-ai/core/util/slug"
|
||||
import { WorkspaceTable } from "@opencode-ai/core/control-plane/workspace.sql"
|
||||
import { getAdapter, registeredAdapters } from "./adapters"
|
||||
import { type Target, type WorkspaceInfo, WorkspaceInfo as WorkspaceInfoSchema } from "./types"
|
||||
import { WorkspaceID } from "./schema"
|
||||
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import { Session } from "@/session/session"
|
||||
import { SessionPrompt } from "@/session/prompt"
|
||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||
@@ -40,7 +40,7 @@ export const Info = Schema.Struct({
|
||||
export type Info = WorkspaceInfo & { timeUsed: number }
|
||||
|
||||
export const ConnectionStatus = Schema.Struct({
|
||||
workspaceID: WorkspaceID,
|
||||
workspaceID: WorkspaceV2.ID,
|
||||
status: Schema.Literals(["connected", "connecting", "disconnected", "error"]),
|
||||
})
|
||||
export type ConnectionStatus = Schema.Schema.Type<typeof ConnectionStatus>
|
||||
@@ -80,16 +80,16 @@ const db = <T>(fn: (d: Parameters<typeof Database.use>[0] extends (trx: infer D)
|
||||
const log = Log.create({ service: "workspace-sync" })
|
||||
|
||||
export const CreateInput = Schema.Struct({
|
||||
id: Schema.optional(WorkspaceID),
|
||||
id: Schema.optional(WorkspaceV2.ID),
|
||||
type: Info.fields.type,
|
||||
branch: Info.fields.branch,
|
||||
projectID: ProjectID,
|
||||
projectID: ProjectV2.ID,
|
||||
extra: Schema.optional(Info.fields.extra),
|
||||
})
|
||||
export type CreateInput = Schema.Schema.Type<typeof CreateInput>
|
||||
|
||||
export const SessionWarpInput = Schema.Struct({
|
||||
workspaceID: Schema.NullOr(WorkspaceID),
|
||||
workspaceID: Schema.NullOr(WorkspaceV2.ID),
|
||||
sessionID: SessionID,
|
||||
copyChanges: Schema.optional(Schema.Boolean),
|
||||
})
|
||||
@@ -105,7 +105,7 @@ export class WorkspaceNotFoundError extends Schema.TaggedErrorClass<WorkspaceNot
|
||||
"WorkspaceNotFoundError",
|
||||
{
|
||||
message: Schema.String,
|
||||
workspaceID: WorkspaceID,
|
||||
workspaceID: WorkspaceV2.ID,
|
||||
},
|
||||
) {}
|
||||
|
||||
@@ -121,7 +121,7 @@ export class SessionWarpHttpError extends Schema.TaggedErrorClass<SessionWarpHtt
|
||||
"WorkspaceSessionWarpHttpError",
|
||||
{
|
||||
message: Schema.String,
|
||||
workspaceID: WorkspaceID,
|
||||
workspaceID: WorkspaceV2.ID,
|
||||
sessionID: SessionID,
|
||||
status: Schema.Number,
|
||||
body: Schema.String,
|
||||
@@ -153,17 +153,17 @@ export interface Interface {
|
||||
readonly sessionWarp: (input: SessionWarpInput) => Effect.Effect<void, SessionWarpError>
|
||||
readonly list: (project: Project.Info) => Effect.Effect<Info[]>
|
||||
readonly syncList: (project: Project.Info) => Effect.Effect<void>
|
||||
readonly get: (id: WorkspaceID) => Effect.Effect<Info | undefined>
|
||||
readonly remove: (id: WorkspaceID) => Effect.Effect<Info | undefined>
|
||||
readonly get: (id: WorkspaceV2.ID) => Effect.Effect<Info | undefined>
|
||||
readonly remove: (id: WorkspaceV2.ID) => Effect.Effect<Info | undefined>
|
||||
readonly status: () => Effect.Effect<ConnectionStatus[]>
|
||||
readonly isSyncing: (workspaceID: WorkspaceID) => Effect.Effect<boolean>
|
||||
readonly isSyncing: (workspaceID: WorkspaceV2.ID) => Effect.Effect<boolean>
|
||||
readonly waitForSync: (
|
||||
workspaceID: WorkspaceID,
|
||||
workspaceID: WorkspaceV2.ID,
|
||||
state: Record<string, number>,
|
||||
signal?: AbortSignal,
|
||||
timeout?: number,
|
||||
) => Effect.Effect<void, WaitForSyncError>
|
||||
readonly startWorkspaceSyncing: (projectID: ProjectID) => Effect.Effect<void>
|
||||
readonly startWorkspaceSyncing: (projectID: ProjectV2.ID) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Workspace") {}
|
||||
@@ -181,10 +181,10 @@ export const layer = Layer.effect(
|
||||
const vcs = yield* Vcs.Service
|
||||
const flags = yield* RuntimeFlags.Service
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const connections = new Map<WorkspaceID, ConnectionStatus>()
|
||||
const syncFibers = yield* FiberMap.make<WorkspaceID, void, SyncLoopError>()
|
||||
const connections = new Map<WorkspaceV2.ID, ConnectionStatus>()
|
||||
const syncFibers = yield* FiberMap.make<WorkspaceV2.ID, void, SyncLoopError>()
|
||||
|
||||
const setStatus = (id: WorkspaceID, status: ConnectionStatus["status"]) => {
|
||||
const setStatus = (id: WorkspaceV2.ID, status: ConnectionStatus["status"]) => {
|
||||
const prev = connections.get(id)
|
||||
if (prev?.status === status) return
|
||||
const next = { workspaceID: id, status }
|
||||
@@ -270,7 +270,7 @@ export const layer = Layer.effect(
|
||||
})
|
||||
|
||||
const runInWorkspace = <A, E, R>(input: {
|
||||
workspaceID?: WorkspaceID
|
||||
workspaceID?: WorkspaceV2.ID
|
||||
local: () => Effect.Effect<A, E, R>
|
||||
remote: (input: {
|
||||
workspace: Info
|
||||
@@ -524,13 +524,13 @@ export const layer = Layer.effect(
|
||||
)
|
||||
})
|
||||
|
||||
const stopSync = Effect.fn("Workspace.stopSync")(function* (id: WorkspaceID) {
|
||||
const stopSync = Effect.fn("Workspace.stopSync")(function* (id: WorkspaceV2.ID) {
|
||||
yield* FiberMap.remove(syncFibers, id)
|
||||
connections.delete(id)
|
||||
})
|
||||
|
||||
const create = Effect.fn("Workspace.create")(function* (input: CreateInput) {
|
||||
const id = WorkspaceID.ascending(input.id)
|
||||
const id = WorkspaceV2.ID.ascending(input.id)
|
||||
const adapter = getAdapter(input.projectID, input.type)
|
||||
const config = yield* WorkspaceAdapterRuntime.configure(adapter, {
|
||||
...input,
|
||||
@@ -864,7 +864,7 @@ export const layer = Layer.effect(
|
||||
names.add(item.name)
|
||||
|
||||
const info: Info = {
|
||||
id: WorkspaceID.ascending(),
|
||||
id: WorkspaceV2.ID.ascending(),
|
||||
type: item.type,
|
||||
branch: item.branch,
|
||||
name: item.name,
|
||||
@@ -895,13 +895,13 @@ export const layer = Layer.effect(
|
||||
)
|
||||
})
|
||||
|
||||
const get = Effect.fn("Workspace.get")(function* (id: WorkspaceID) {
|
||||
const get = Effect.fn("Workspace.get")(function* (id: WorkspaceV2.ID) {
|
||||
const row = yield* db((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get())
|
||||
if (!row) return
|
||||
return fromRow(row)
|
||||
})
|
||||
|
||||
const remove = Effect.fn("Workspace.remove")(function* (id: WorkspaceID) {
|
||||
const remove = Effect.fn("Workspace.remove")(function* (id: WorkspaceV2.ID) {
|
||||
const sessions = yield* db((db) =>
|
||||
db
|
||||
.select({ id: SessionTable.id, parentID: SessionTable.parent_id })
|
||||
@@ -941,13 +941,13 @@ export const layer = Layer.effect(
|
||||
return [...connections.values()]
|
||||
})
|
||||
|
||||
const isSyncing = Effect.fn("Workspace.isSyncing")(function* (workspaceID: WorkspaceID) {
|
||||
const isSyncing = Effect.fn("Workspace.isSyncing")(function* (workspaceID: WorkspaceV2.ID) {
|
||||
const exists = yield* FiberMap.has(syncFibers, workspaceID)
|
||||
return exists && connections.get(workspaceID)?.status !== "error"
|
||||
})
|
||||
|
||||
const waitForSync = Effect.fn("Workspace.waitForSync")(function* (
|
||||
workspaceID: WorkspaceID,
|
||||
workspaceID: WorkspaceV2.ID,
|
||||
state: Record<string, number>,
|
||||
signal?: AbortSignal,
|
||||
timeout = TIMEOUT,
|
||||
@@ -982,7 +982,7 @@ export const layer = Layer.effect(
|
||||
)
|
||||
})
|
||||
|
||||
const startWorkspaceSyncing = Effect.fn("Workspace.startWorkspaceSyncing")(function* (projectID: ProjectID) {
|
||||
const startWorkspaceSyncing = Effect.fn("Workspace.startWorkspaceSyncing")(function* (projectID: ProjectV2.ID) {
|
||||
const rows = yield* db((db) =>
|
||||
db
|
||||
.selectDistinct({ workspace: WorkspaceTable })
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Context, Effect, Exit, Fiber } from "effect"
|
||||
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||
import type { WorkspaceID } from "@/control-plane/schema"
|
||||
import type { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import { InstanceRef, WorkspaceRef } from "./instance-ref"
|
||||
import { attachWith } from "./run-service"
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface Shape {
|
||||
readonly bind: <Args extends readonly unknown[], Result>(fn: (...args: Args) => Result) => (...args: Args) => Result
|
||||
}
|
||||
|
||||
function restoreWorkspace<R>(workspace: WorkspaceID | undefined, fn: () => R): R {
|
||||
function restoreWorkspace<R>(workspace: WorkspaceV2.ID | undefined, fn: () => R): R {
|
||||
if (workspace !== undefined) return WorkspaceContext.restore(workspace, fn)
|
||||
return fn()
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Context } from "effect"
|
||||
import type { InstanceContext } from "@/project/instance-context"
|
||||
import type { WorkspaceID } from "@/control-plane/schema"
|
||||
import type { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
|
||||
export const InstanceRef = Context.Reference<InstanceContext | undefined>("~opencode/InstanceRef", {
|
||||
defaultValue: () => undefined,
|
||||
})
|
||||
|
||||
export const WorkspaceRef = Context.Reference<WorkspaceID | undefined>("~opencode/WorkspaceRef", {
|
||||
export const WorkspaceRef = Context.Reference<WorkspaceV2.ID | undefined>("~opencode/WorkspaceRef", {
|
||||
defaultValue: () => undefined,
|
||||
})
|
||||
|
||||
@@ -15,11 +15,9 @@ export class Service extends ConfigService.Service<Service>()("@opencode/Runtime
|
||||
autoShare: bool("OPENCODE_AUTO_SHARE"),
|
||||
pure: bool("OPENCODE_PURE"),
|
||||
disableDefaultPlugins: bool("OPENCODE_DISABLE_DEFAULT_PLUGINS"),
|
||||
disableChannelDb: bool("OPENCODE_DISABLE_CHANNEL_DB"),
|
||||
disableEmbeddedWebUi: bool("OPENCODE_DISABLE_EMBEDDED_WEB_UI"),
|
||||
disableExternalSkills: bool("OPENCODE_DISABLE_EXTERNAL_SKILLS"),
|
||||
disableLspDownload: bool("OPENCODE_DISABLE_LSP_DOWNLOAD"),
|
||||
skipMigrations: bool("OPENCODE_SKIP_MIGRATIONS"),
|
||||
disableClaudeCodePrompt: Config.all({
|
||||
broad: bool("OPENCODE_DISABLE_CLAUDE_CODE"),
|
||||
direct: bool("OPENCODE_DISABLE_CLAUDE_CODE_PROMPT"),
|
||||
|
||||
@@ -116,7 +116,7 @@ const cli = yargs(args)
|
||||
run_id: processMetadata.runID,
|
||||
})
|
||||
|
||||
const marker = path.join(Global.Path.data, "opencode.db")
|
||||
const marker = Database.getPath()
|
||||
if (!(await Filesystem.exists(marker))) {
|
||||
const tty = process.stderr.isTTY
|
||||
process.stderr.write("Performing one time database migration, may take a few minutes..." + EOL)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Bus } from "@/bus"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { ConfigPermission } from "@/config/permission"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { MessageID, SessionID } from "@/session/schema"
|
||||
import { PermissionTable } from "@opencode-ai/core/session/sql"
|
||||
import { Database } from "@/storage/db"
|
||||
@@ -61,7 +61,7 @@ export const ReplyBody = Schema.Struct(reply).annotate({ identifier: "Permission
|
||||
export type ReplyBody = Schema.Schema.Type<typeof ReplyBody>
|
||||
|
||||
export const Approval = Schema.Struct({
|
||||
projectID: ProjectID,
|
||||
projectID: ProjectV2.ID,
|
||||
patterns: Schema.Array(Schema.String),
|
||||
}).annotate({ identifier: "PermissionApproval" })
|
||||
export type Approval = Schema.Schema.Type<typeof Approval>
|
||||
|
||||
@@ -9,7 +9,6 @@ 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"
|
||||
@@ -17,7 +16,7 @@ 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"
|
||||
@@ -46,7 +45,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),
|
||||
@@ -93,7 +92,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),
|
||||
@@ -108,7 +107,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,
|
||||
}) {}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -125,13 +124,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") {}
|
||||
@@ -181,11 +180,11 @@ 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(() =>
|
||||
@@ -237,8 +236,8 @@ 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 projectID = ProjectV2.ID.make(data.id)
|
||||
yield* migrateProjectId(data.previous ? ProjectV2.ID.make(data.previous) : undefined, projectID)
|
||||
const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get())
|
||||
const existing = row
|
||||
? fromRow(row)
|
||||
@@ -254,12 +253,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)
|
||||
)
|
||||
@@ -309,18 +308,18 @@ export const layer = Layer.effect(
|
||||
.run(),
|
||||
)
|
||||
|
||||
if (projectID !== ProjectID.global) {
|
||||
if (projectID !== ProjectV2.ID.global) {
|
||||
yield* db((d) =>
|
||||
d
|
||||
.update(SessionTable)
|
||||
.set({ project_id: projectID })
|
||||
.where(and(eq(SessionTable.project_id, ProjectID.global), eq(SessionTable.directory, data.directory)))
|
||||
.where(and(eq(SessionTable.project_id, ProjectV2.ID.global), eq(SessionTable.directory, data.directory)))
|
||||
.run(),
|
||||
)
|
||||
}
|
||||
|
||||
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 }
|
||||
@@ -354,7 +353,7 @@ export const layer = Layer.effect(
|
||||
return yield* db((d) => d.select().from(ProjectTable).all().map(fromRow))
|
||||
})
|
||||
|
||||
const get = Effect.fn("Project.get")(function* (id: ProjectID) {
|
||||
const get = Effect.fn("Project.get")(function* (id: ProjectV2.ID) {
|
||||
const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
|
||||
return row ? fromRow(row) : undefined
|
||||
})
|
||||
@@ -392,7 +391,7 @@ export const layer = Layer.effect(
|
||||
return project
|
||||
})
|
||||
|
||||
const setInitialized = Effect.fn("Project.setInitialized")(function* (id: ProjectID) {
|
||||
const setInitialized = Effect.fn("Project.setInitialized")(function* (id: ProjectV2.ID) {
|
||||
yield* db((d) =>
|
||||
d.update(ProjectTable).set({ time_initialized: Date.now() }).where(eq(ProjectTable.id, id)).run(),
|
||||
)
|
||||
@@ -413,7 +412,7 @@ export const layer = Layer.effect(
|
||||
yield* InstanceState.get(initState)
|
||||
})
|
||||
|
||||
const sandboxes = Effect.fn("Project.sandboxes")(function* (id: ProjectID) {
|
||||
const sandboxes = Effect.fn("Project.sandboxes")(function* (id: ProjectV2.ID) {
|
||||
const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
|
||||
if (!row) return []
|
||||
const data = fromRow(row)
|
||||
@@ -428,7 +427,7 @@ 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 addSandbox = Effect.fn("Project.addSandbox")(function* (id: ProjectV2.ID, directory: string) {
|
||||
const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
|
||||
if (!row) throw new Error(`Project not found: ${id}`)
|
||||
const sboxes = [...row.sandboxes]
|
||||
@@ -445,7 +444,7 @@ export const layer = Layer.effect(
|
||||
yield* emitUpdated(fromRow(result))
|
||||
})
|
||||
|
||||
const removeSandbox = Effect.fn("Project.removeSandbox")(function* (id: ProjectID, directory: string) {
|
||||
const removeSandbox = Effect.fn("Project.removeSandbox")(function* (id: ProjectV2.ID, directory: string) {
|
||||
const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
|
||||
if (!row) throw new Error(`Project not found: ${id}`)
|
||||
const sboxes = row.sandboxes.filter((s) => s !== directory)
|
||||
@@ -498,13 +497,13 @@ export function list() {
|
||||
)
|
||||
}
|
||||
|
||||
export function get(id: ProjectID): Info | undefined {
|
||||
export function get(id: ProjectV2.ID): 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) {
|
||||
export function setInitialized(id: ProjectV2.ID) {
|
||||
Database.use((db) =>
|
||||
db.update(ProjectTable).set({ time_initialized: Date.now() }).where(eq(ProjectTable.id, id)).run(),
|
||||
)
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { Schema } from "effect"
|
||||
|
||||
import { Project } from "@opencode-ai/core/project"
|
||||
|
||||
export const ProjectID = Project.ID
|
||||
export type ProjectID = typeof ProjectID.Type
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as PtyTicket from "./ticket"
|
||||
|
||||
import { WorkspaceID } from "@/control-plane/schema"
|
||||
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref"
|
||||
import { PtyID } from "@/pty/schema"
|
||||
import { PositiveInt } from "@opencode-ai/core/schema"
|
||||
@@ -17,7 +17,7 @@ export const ConnectToken = Schema.Struct({
|
||||
export type Scope = {
|
||||
readonly ptyID: PtyID
|
||||
readonly directory?: string
|
||||
readonly workspaceID?: WorkspaceID
|
||||
readonly workspaceID?: WorkspaceV2.ID
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Project } from "@/project/project"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { ProjectNotFoundError } from "../errors"
|
||||
@@ -50,7 +50,7 @@ export const ProjectApi = HttpApi.make("project")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.patch("update", `${root}/:projectID`, {
|
||||
params: { projectID: ProjectID },
|
||||
params: { projectID: ProjectV2.ID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: UpdatePayload,
|
||||
success: described(Project.Info, "Updated project information"),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as InstanceState from "@/effect/instance-state"
|
||||
import { Project } from "@/project/project"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { InstanceHttpApi } from "../api"
|
||||
@@ -33,7 +33,7 @@ export const projectHandlers = HttpApiBuilder.group(InstanceHttpApi, "project",
|
||||
})
|
||||
|
||||
const update = Effect.fn("ProjectHttpApi.update")(function* (ctx: {
|
||||
params: { projectID: ProjectID }
|
||||
params: { projectID: ProjectV2.ID }
|
||||
payload: Project.UpdatePayload
|
||||
}) {
|
||||
return yield* svc.update({ ...ctx.payload, projectID: ctx.params.projectID }).pipe(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { WorkspaceID } from "@/control-plane/schema"
|
||||
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { DateTime, Effect, Option, Schema } from "effect"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
@@ -20,7 +20,7 @@ const SessionCursor = Schema.Struct({
|
||||
direction: Schema.Union([Schema.Literal("previous"), Schema.Literal("next")]),
|
||||
directory: Schema.String.pipe(Schema.optional),
|
||||
path: Schema.String.pipe(Schema.optional),
|
||||
workspaceID: WorkspaceID.pipe(Schema.optional),
|
||||
workspaceID: WorkspaceV2.ID.pipe(Schema.optional),
|
||||
roots: Schema.Boolean.pipe(Schema.optional),
|
||||
start: Schema.Finite.pipe(Schema.optional),
|
||||
search: Schema.String.pipe(Schema.optional),
|
||||
@@ -78,7 +78,7 @@ const sessionCursor = {
|
||||
|
||||
function decodeWorkspaceID(input: string | undefined) {
|
||||
if (input === undefined) return Effect.succeed(undefined)
|
||||
const workspaceID = Schema.decodeUnknownOption(WorkspaceID)(input)
|
||||
const workspaceID = Schema.decodeUnknownOption(WorkspaceV2.ID)(input)
|
||||
if (Option.isSome(workspaceID)) return Effect.succeed(workspaceID.value)
|
||||
return Effect.fail(
|
||||
new InvalidRequestError({
|
||||
|
||||
+15
-15
@@ -1,4 +1,4 @@
|
||||
import { WorkspaceID } from "@/control-plane/schema"
|
||||
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import type { Target } from "@/control-plane/types"
|
||||
import { Workspace } from "@/control-plane/workspace"
|
||||
import { WorkspaceAdapterRuntime } from "@/control-plane/workspace-adapter-runtime"
|
||||
@@ -30,8 +30,8 @@ type RemoteTarget = Extract<Target, { type: "remote" }>
|
||||
|
||||
type RequestPlan = Data.TaggedEnum<{
|
||||
InvalidWorkspace: {}
|
||||
MissingWorkspace: { readonly workspaceID: WorkspaceID }
|
||||
Local: { readonly directory: string; readonly workspaceID?: WorkspaceID }
|
||||
MissingWorkspace: { readonly workspaceID: WorkspaceV2.ID }
|
||||
Local: { readonly directory: string; readonly workspaceID?: WorkspaceV2.ID }
|
||||
Remote: {
|
||||
readonly request: HttpServerRequest.HttpServerRequest
|
||||
readonly workspace: Workspace.Info
|
||||
@@ -46,7 +46,7 @@ export class WorkspaceRouteContext extends Context.Service<
|
||||
WorkspaceRouteContext,
|
||||
{
|
||||
readonly directory: string
|
||||
readonly workspaceID?: WorkspaceID
|
||||
readonly workspaceID?: WorkspaceV2.ID
|
||||
}
|
||||
>()("@opencode/ExperimentalHttpApiWorkspaceRouteContext") {}
|
||||
|
||||
@@ -62,23 +62,23 @@ function requestURL(request: HttpServerRequest.HttpServerRequest): URL {
|
||||
return new URL(request.url, "http://localhost")
|
||||
}
|
||||
|
||||
function configuredWorkspaceID(): WorkspaceID | undefined {
|
||||
return Flag.OPENCODE_WORKSPACE_ID ? WorkspaceID.make(Flag.OPENCODE_WORKSPACE_ID) : undefined
|
||||
function configuredWorkspaceID(): WorkspaceV2.ID | undefined {
|
||||
return Flag.OPENCODE_WORKSPACE_ID ? WorkspaceV2.ID.make(Flag.OPENCODE_WORKSPACE_ID) : undefined
|
||||
}
|
||||
|
||||
function selectedWorkspaceID(url: URL, sessionWorkspaceID?: WorkspaceID): WorkspaceID | undefined {
|
||||
function selectedWorkspaceID(url: URL, sessionWorkspaceID?: WorkspaceV2.ID): WorkspaceV2.ID | undefined {
|
||||
const workspaceParam = url.searchParams.get("workspace")
|
||||
return sessionWorkspaceID ?? (workspaceParam ? WorkspaceID.make(workspaceParam) : undefined)
|
||||
return sessionWorkspaceID ?? (workspaceParam ? WorkspaceV2.ID.make(workspaceParam) : undefined)
|
||||
}
|
||||
|
||||
function selectedV2WorkspaceID(
|
||||
url: URL,
|
||||
sessionWorkspaceID?: WorkspaceID,
|
||||
): WorkspaceID | typeof InvalidWorkspaceID | undefined {
|
||||
sessionWorkspaceID?: WorkspaceV2.ID,
|
||||
): WorkspaceV2.ID | typeof InvalidWorkspaceID | undefined {
|
||||
if (sessionWorkspaceID) return sessionWorkspaceID
|
||||
const workspaceParam = url.searchParams.get("workspace")
|
||||
if (!workspaceParam) return undefined
|
||||
const workspaceID = Schema.decodeUnknownOption(WorkspaceID)(workspaceParam)
|
||||
const workspaceID = Schema.decodeUnknownOption(WorkspaceV2.ID)(workspaceParam)
|
||||
if (Option.isNone(workspaceID)) return InvalidWorkspaceID
|
||||
return workspaceID.value
|
||||
}
|
||||
@@ -92,14 +92,14 @@ function shouldStayOnControlPlane(request: HttpServerRequest.HttpServerRequest,
|
||||
}
|
||||
|
||||
function resolveWorkspace(
|
||||
id: WorkspaceID | undefined,
|
||||
envWorkspaceID: WorkspaceID | undefined,
|
||||
id: WorkspaceV2.ID | undefined,
|
||||
envWorkspaceID: WorkspaceV2.ID | undefined,
|
||||
): Effect.Effect<Workspace.Info | void, never, Workspace.Service> {
|
||||
if (!id || envWorkspaceID) return Effect.void
|
||||
return Workspace.Service.use((workspace) => workspace.get(id))
|
||||
}
|
||||
|
||||
function missingWorkspaceResponse(id: WorkspaceID): HttpServerResponse.HttpServerResponse {
|
||||
function missingWorkspaceResponse(id: WorkspaceV2.ID): HttpServerResponse.HttpServerResponse {
|
||||
return HttpServerResponse.text(`Workspace not found: ${id}`, {
|
||||
status: 500,
|
||||
contentType: "text/plain; charset=utf-8",
|
||||
@@ -159,7 +159,7 @@ function planWorkspaceRequest(
|
||||
|
||||
function planRequest(
|
||||
request: HttpServerRequest.HttpServerRequest,
|
||||
sessionWorkspaceID?: WorkspaceID,
|
||||
sessionWorkspaceID?: WorkspaceV2.ID,
|
||||
): Effect.Effect<RequestPlan, never, Workspace.Service> {
|
||||
return Effect.gen(function* () {
|
||||
const url = requestURL(request)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Database } from "@/storage/db"
|
||||
import { inArray } from "drizzle-orm"
|
||||
import { EventSequenceTable } from "@opencode-ai/core/event/sql"
|
||||
import { Workspace } from "@/control-plane/workspace"
|
||||
import type { WorkspaceID } from "@/control-plane/schema"
|
||||
import type { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Effect } from "effect"
|
||||
|
||||
@@ -53,7 +53,7 @@ export function parse(headers: Headers): State | undefined {
|
||||
)
|
||||
}
|
||||
|
||||
export function wait(workspaceID: WorkspaceID, state: State, signal?: AbortSignal) {
|
||||
export function wait(workspaceID: WorkspaceV2.ID, state: State, signal?: AbortSignal) {
|
||||
return Effect.gen(function* () {
|
||||
log.info("waiting for state", {
|
||||
workspaceID,
|
||||
|
||||
@@ -29,8 +29,8 @@ import { MessageV2 } from "./message-v2"
|
||||
import type { InstanceContext } from "../project/instance-context"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { ProjectID } from "../project/schema"
|
||||
import { WorkspaceID } from "../control-plane/schema"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import { SessionID, MessageID, PartID } from "./schema"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
|
||||
@@ -208,8 +208,8 @@ const Model = Schema.Struct({
|
||||
export const Info = Schema.Struct({
|
||||
id: SessionID,
|
||||
slug: Schema.String,
|
||||
projectID: ProjectID,
|
||||
workspaceID: optionalOmitUndefined(WorkspaceID),
|
||||
projectID: ProjectV2.ID,
|
||||
workspaceID: optionalOmitUndefined(WorkspaceV2.ID),
|
||||
directory: Schema.String,
|
||||
path: optionalOmitUndefined(Schema.String),
|
||||
parentID: optionalOmitUndefined(SessionID),
|
||||
@@ -228,7 +228,7 @@ export const Info = Schema.Struct({
|
||||
export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
|
||||
|
||||
export const ProjectInfo = Schema.Struct({
|
||||
id: ProjectID,
|
||||
id: ProjectV2.ID,
|
||||
name: optionalOmitUndefined(Schema.String),
|
||||
worktree: Schema.String,
|
||||
}).annotate({ identifier: "ProjectSummary" })
|
||||
@@ -247,7 +247,7 @@ export const CreateInput = Schema.optional(
|
||||
agent: Schema.optional(Schema.String),
|
||||
model: Schema.optional(Model),
|
||||
permission: Schema.optional(Permission.Ruleset),
|
||||
workspaceID: Schema.optional(WorkspaceID),
|
||||
workspaceID: Schema.optional(WorkspaceV2.ID),
|
||||
}),
|
||||
)
|
||||
export type CreateInput = Types.DeepMutable<Schema.Schema.Type<typeof CreateInput>>
|
||||
@@ -281,7 +281,7 @@ export type ListInput = {
|
||||
directory?: string
|
||||
scope?: "project"
|
||||
path?: string
|
||||
workspaceID?: WorkspaceID
|
||||
workspaceID?: WorkspaceV2.ID
|
||||
roots?: boolean
|
||||
start?: number
|
||||
search?: string
|
||||
@@ -307,8 +307,8 @@ const UpdatedTime = Schema.Struct({
|
||||
const UpdatedInfo = Schema.Struct({
|
||||
id: Schema.optional(Schema.NullOr(SessionID)),
|
||||
slug: Schema.optional(Schema.NullOr(Schema.String)),
|
||||
projectID: Schema.optional(Schema.NullOr(ProjectID)),
|
||||
workspaceID: Schema.optional(Schema.NullOr(WorkspaceID)),
|
||||
projectID: Schema.optional(Schema.NullOr(ProjectV2.ID)),
|
||||
workspaceID: Schema.optional(Schema.NullOr(WorkspaceV2.ID)),
|
||||
directory: Schema.optional(Schema.NullOr(Schema.String)),
|
||||
path: Schema.optional(Schema.NullOr(Schema.String)),
|
||||
parentID: Schema.optional(Schema.NullOr(SessionID)),
|
||||
@@ -456,7 +456,7 @@ export interface Interface {
|
||||
agent?: string
|
||||
model?: Schema.Schema.Type<typeof Model>
|
||||
permission?: Permission.Ruleset
|
||||
workspaceID?: WorkspaceID
|
||||
workspaceID?: WorkspaceV2.ID
|
||||
}) => Effect.Effect<Info>
|
||||
readonly fork: (input: { sessionID: SessionID; messageID?: MessageID }) => Effect.Effect<Info, NotFound>
|
||||
readonly touch: (sessionID: SessionID) => Effect.Effect<void>
|
||||
@@ -526,7 +526,7 @@ export const layer: Layer.Layer<
|
||||
agent?: string
|
||||
model?: Schema.Schema.Type<typeof Model>
|
||||
parentID?: SessionID
|
||||
workspaceID?: WorkspaceID
|
||||
workspaceID?: WorkspaceV2.ID
|
||||
directory: string
|
||||
path?: string
|
||||
permission?: Permission.Ruleset
|
||||
@@ -660,7 +660,7 @@ export const layer: Layer.Layer<
|
||||
agent?: string
|
||||
model?: Schema.Schema.Type<typeof Model>
|
||||
permission?: Permission.Ruleset
|
||||
workspaceID?: WorkspaceID
|
||||
workspaceID?: WorkspaceV2.ID
|
||||
}) {
|
||||
const ctx = yield* InstanceState.context
|
||||
const workspace = yield* InstanceState.workspaceID
|
||||
@@ -890,7 +890,7 @@ const cancelBackgroundJobs = Effect.fn("Session.cancelBackgroundJobs")(function*
|
||||
|
||||
function* listByProject(
|
||||
input: ListInput & {
|
||||
projectID: ProjectID
|
||||
projectID: ProjectV2.ID
|
||||
experimentalWorkspaces: boolean
|
||||
},
|
||||
) {
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import { type SQLiteTransaction } from "drizzle-orm/sqlite-core"
|
||||
export * from "drizzle-orm"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { LocalContext } from "@/util/local-context"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import path from "path"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { InstallationChannel } from "@opencode-ai/core/installation/version"
|
||||
import { EffectBridge } from "@/effect/bridge"
|
||||
import { init } from "#db"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
|
||||
export const NotFoundError = NamedError.create("NotFoundError", {
|
||||
message: Schema.String,
|
||||
@@ -18,25 +14,7 @@ export const NotFoundError = NamedError.create("NotFoundError", {
|
||||
|
||||
const log = Log.create({ service: "db" })
|
||||
|
||||
type DatabaseFlags = Pick<RuntimeFlags.Info, "disableChannelDb" | "skipMigrations">
|
||||
|
||||
const readRuntimeFlags = () =>
|
||||
Effect.runSync(RuntimeFlags.Service.useSync((flags) => flags).pipe(Effect.provide(RuntimeFlags.defaultLayer)))
|
||||
|
||||
export function getChannelPath(flags: Pick<DatabaseFlags, "disableChannelDb"> = readRuntimeFlags()) {
|
||||
if (["latest", "beta", "prod"].includes(InstallationChannel) || flags.disableChannelDb)
|
||||
return path.join(Global.Path.data, "opencode.db")
|
||||
const safe = InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-")
|
||||
return path.join(Global.Path.data, `opencode-${safe}.db`)
|
||||
}
|
||||
|
||||
export const getPath = (flags?: Pick<DatabaseFlags, "disableChannelDb">) => {
|
||||
if (Flag.OPENCODE_DB) {
|
||||
if (Flag.OPENCODE_DB === ":memory:" || path.isAbsolute(Flag.OPENCODE_DB)) return Flag.OPENCODE_DB
|
||||
return path.join(Global.Path.data, Flag.OPENCODE_DB)
|
||||
}
|
||||
return getChannelPath(flags)
|
||||
}
|
||||
export const getPath = () => Database.path()
|
||||
|
||||
export type Transaction = SQLiteTransaction<"sync", void>
|
||||
|
||||
@@ -46,12 +24,14 @@ let client: Client | undefined
|
||||
let loaded = false
|
||||
|
||||
export const Client = Object.assign(
|
||||
(flags: DatabaseFlags = readRuntimeFlags()): Client => {
|
||||
(): Client => {
|
||||
if (loaded) return client as Client
|
||||
|
||||
const dbPath = getPath(flags)
|
||||
const dbPath = getPath()
|
||||
log.info("opening database", { path: dbPath })
|
||||
|
||||
Database.init({ path: dbPath })
|
||||
|
||||
const db = init(dbPath)
|
||||
|
||||
db.run("PRAGMA journal_mode = WAL")
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Project } from "@/project/project"
|
||||
import { Database } from "@/storage/db"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||
import type { ProjectID } from "../project/schema"
|
||||
import type { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Slug } from "@opencode-ai/core/util/slug"
|
||||
import { errorMessage } from "../util/error"
|
||||
@@ -476,7 +476,7 @@ export const layer: Layer.Layer<
|
||||
|
||||
const runStartScripts = Effect.fnUntraced(function* (
|
||||
directory: string,
|
||||
input: { projectID: ProjectID; extra?: string },
|
||||
input: { projectID: ProjectV2.ID; extra?: string },
|
||||
) {
|
||||
const row = yield* Effect.sync(() =>
|
||||
Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, input.projectID)).get()),
|
||||
|
||||
@@ -30,7 +30,7 @@ import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { pathToFileURL } from "url"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { ProjectID } from "../../src/project/schema"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { ConfigPlugin } from "@/config/plugin"
|
||||
import { AccountTest } from "../fake/account"
|
||||
@@ -274,7 +274,7 @@ async function check(map: (dir: string) => string) {
|
||||
const cfg = await load(ctx)
|
||||
expect(cfg.snapshot).toBe(true)
|
||||
expect(ctx.directory).toBe(Filesystem.resolve(tmp.path))
|
||||
expect(ctx.project.id).not.toBe(ProjectID.global)
|
||||
expect(ctx.project.id).not.toBe(ProjectV2.ID.global)
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { getAdapter, registerAdapter } from "../../src/control-plane/adapters"
|
||||
import { ProjectID } from "../../src/project/schema"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import type { WorkspaceInfo } from "../../src/control-plane/types"
|
||||
|
||||
function info(projectID: WorkspaceInfo["projectID"], type: string): WorkspaceInfo {
|
||||
@@ -36,8 +36,8 @@ function adapter(dir: string) {
|
||||
describe("control-plane/adapters", () => {
|
||||
test("isolates custom adapters by project", async () => {
|
||||
const type = `demo-${Math.random().toString(36).slice(2)}`
|
||||
const one = ProjectID.make(`project-${Math.random().toString(36).slice(2)}`)
|
||||
const two = ProjectID.make(`project-${Math.random().toString(36).slice(2)}`)
|
||||
const one = ProjectV2.ID.make(`project-${Math.random().toString(36).slice(2)}`)
|
||||
const two = ProjectV2.ID.make(`project-${Math.random().toString(36).slice(2)}`)
|
||||
registerAdapter(one, type, adapter("/one"))
|
||||
registerAdapter(two, type, adapter("/two"))
|
||||
|
||||
@@ -53,7 +53,7 @@ describe("control-plane/adapters", () => {
|
||||
|
||||
test("latest install wins within a project", async () => {
|
||||
const type = `demo-${Math.random().toString(36).slice(2)}`
|
||||
const id = ProjectID.make(`project-${Math.random().toString(36).slice(2)}`)
|
||||
const id = ProjectV2.ID.make(`project-${Math.random().toString(36).slice(2)}`)
|
||||
registerAdapter(id, type, adapter("/one"))
|
||||
|
||||
expect(await (await getAdapter(id, type)).target(info(id, type))).toEqual({
|
||||
|
||||
@@ -11,7 +11,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { GlobalBus, type GlobalEvent } from "@/bus/global"
|
||||
import { Database } from "@/storage/db"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||
import { Session as SessionNs } from "@/session/session"
|
||||
import { SessionID } from "@/session/schema"
|
||||
@@ -22,7 +22,7 @@ import { resetDatabase } from "../fixture/db"
|
||||
import { disposeAllInstances, provideTmpdirInstance, requireInstance, TestInstance } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { registerAdapter } from "../../src/control-plane/adapters"
|
||||
import { WorkspaceID } from "../../src/control-plane/schema"
|
||||
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import { WorkspaceTable } from "@opencode-ai/core/control-plane/workspace.sql"
|
||||
import type { Target, WorkspaceAdapter, WorkspaceInfo } from "../../src/control-plane/types"
|
||||
import * as Workspace from "../../src/control-plane/workspace"
|
||||
@@ -129,7 +129,7 @@ async function initGitRepo(dir: string) {
|
||||
await $`git commit -m "base"`.cwd(dir).quiet()
|
||||
}
|
||||
|
||||
const startWorkspaceSyncingWithFlag = (projectID: ProjectID, experimentalWorkspaces: boolean) =>
|
||||
const startWorkspaceSyncingWithFlag = (projectID: ProjectV2.ID, experimentalWorkspaces: boolean) =>
|
||||
Effect.runPromise(
|
||||
Workspace.use.startWorkspaceSyncing(projectID).pipe(Effect.provide(workspaceLayer(experimentalWorkspaces))),
|
||||
)
|
||||
@@ -265,9 +265,9 @@ function serverUrl() {
|
||||
})
|
||||
}
|
||||
|
||||
function workspaceInfo(projectID: ProjectID, type: string, input?: Partial<Workspace.Info>): Workspace.Info {
|
||||
function workspaceInfo(projectID: ProjectV2.ID, type: string, input?: Partial<Workspace.Info>): Workspace.Info {
|
||||
return {
|
||||
id: input?.id ?? WorkspaceID.ascending(),
|
||||
id: input?.id ?? WorkspaceV2.ID.ascending(),
|
||||
type,
|
||||
name: input?.name ?? unique("workspace"),
|
||||
branch: input?.branch ?? null,
|
||||
@@ -296,7 +296,7 @@ function insertWorkspace(info: Workspace.Info) {
|
||||
)
|
||||
}
|
||||
|
||||
function insertProject(id: ProjectID, worktree: string) {
|
||||
function insertProject(id: ProjectV2.ID, worktree: string) {
|
||||
Database.use((db) =>
|
||||
db
|
||||
.insert(ProjectTable)
|
||||
@@ -313,7 +313,7 @@ function insertProject(id: ProjectID, worktree: string) {
|
||||
)
|
||||
}
|
||||
|
||||
function attachSessionToWorkspace(sessionID: SessionID, workspaceID: WorkspaceID) {
|
||||
function attachSessionToWorkspace(sessionID: SessionID, workspaceID: WorkspaceV2.ID) {
|
||||
Database.use((db) =>
|
||||
db.update(SessionTable).set({ workspace_id: workspaceID }).where(eq(SessionTable.id, sessionID)).run(),
|
||||
)
|
||||
@@ -352,10 +352,10 @@ describe("workspace schemas and exports", () => {
|
||||
|
||||
test("validates create input with workspace id, project id, branch, type, and extra", () => {
|
||||
const input = {
|
||||
id: WorkspaceID.ascending("wrk_schema_create"),
|
||||
id: WorkspaceV2.ID.ascending("wrk_schema_create"),
|
||||
type: "worktree",
|
||||
branch: "feature/schema",
|
||||
projectID: ProjectID.make("project-schema"),
|
||||
projectID: ProjectV2.ID.make("project-schema"),
|
||||
extra: { nested: true },
|
||||
}
|
||||
|
||||
@@ -372,7 +372,7 @@ describe("workspace CRUD", () => {
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const workspace = yield* Workspace.Service
|
||||
expect(yield* workspace.get(WorkspaceID.ascending("wrk_missing_get"))).toBeUndefined()
|
||||
expect(yield* workspace.get(WorkspaceV2.ID.ascending("wrk_missing_get"))).toBeUndefined()
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
@@ -383,21 +383,21 @@ describe("workspace CRUD", () => {
|
||||
Effect.gen(function* () {
|
||||
const instance = yield* requireInstance
|
||||
const workspace = yield* Workspace.Service
|
||||
const otherProjectID = ProjectID.make("project-other")
|
||||
const otherProjectID = ProjectV2.ID.make("project-other")
|
||||
insertProject(otherProjectID, "/tmp/other")
|
||||
const a = workspaceInfo(instance.project.id, "manual", {
|
||||
id: WorkspaceID.ascending("wrk_a_list"),
|
||||
id: WorkspaceV2.ID.ascending("wrk_a_list"),
|
||||
branch: "a",
|
||||
directory: "/a",
|
||||
extra: { a: true },
|
||||
})
|
||||
const b = workspaceInfo(instance.project.id, "manual", {
|
||||
id: WorkspaceID.ascending("wrk_b_list"),
|
||||
id: WorkspaceV2.ID.ascending("wrk_b_list"),
|
||||
branch: "b",
|
||||
directory: "/b",
|
||||
extra: ["b"],
|
||||
})
|
||||
const other = workspaceInfo(otherProjectID, "manual", { id: WorkspaceID.ascending("wrk_c_list") })
|
||||
const other = workspaceInfo(otherProjectID, "manual", { id: WorkspaceV2.ID.ascending("wrk_c_list") })
|
||||
insertWorkspace(b)
|
||||
insertWorkspace(other)
|
||||
insertWorkspace(a)
|
||||
@@ -418,7 +418,7 @@ describe("workspace CRUD", () => {
|
||||
process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "https://otel.test"
|
||||
process.env.OTEL_RESOURCE_ATTRIBUTES = "service.name=opencode-test"
|
||||
|
||||
const workspaceID = WorkspaceID.ascending("wrk_create_local")
|
||||
const workspaceID = WorkspaceV2.ID.ascending("wrk_create_local")
|
||||
const type = unique("create-local")
|
||||
const targetDir = path.join(instance.directory, "created-local")
|
||||
const recorded = recordedAdapter({
|
||||
@@ -578,7 +578,7 @@ describe("workspace CRUD", () => {
|
||||
const workspace = yield* Workspace.Service
|
||||
const type = unique("list-sync")
|
||||
const existing = workspaceInfo(instance.project.id, type, {
|
||||
id: WorkspaceID.ascending("wrk_list_sync_existing"),
|
||||
id: WorkspaceV2.ID.ascending("wrk_list_sync_existing"),
|
||||
name: "existing",
|
||||
directory: path.join(instance.directory, "existing"),
|
||||
})
|
||||
@@ -748,7 +748,7 @@ describe("workspace CRUD", () => {
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const workspace = yield* Workspace.Service
|
||||
expect(yield* workspace.remove(WorkspaceID.ascending("wrk_missing_remove"))).toBeUndefined()
|
||||
expect(yield* workspace.remove(WorkspaceV2.ID.ascending("wrk_missing_remove"))).toBeUndefined()
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
@@ -793,7 +793,7 @@ describe("workspace CRUD", () => {
|
||||
const instance = yield* requireInstance
|
||||
const workspace = yield* Workspace.Service
|
||||
const type = unique("remove-throws")
|
||||
const info = workspaceInfo(instance.project.id, type, { id: WorkspaceID.ascending("wrk_remove_throws") })
|
||||
const info = workspaceInfo(instance.project.id, type, { id: WorkspaceV2.ID.ascending("wrk_remove_throws") })
|
||||
registerAdapter(
|
||||
instance.project.id,
|
||||
type,
|
||||
@@ -1555,7 +1555,7 @@ describe("workspace waitForSync", () => {
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const workspace = yield* Workspace.Service
|
||||
expect(yield* workspace.waitForSync(WorkspaceID.ascending("wrk_wait_empty"), {})).toBeUndefined()
|
||||
expect(yield* workspace.waitForSync(WorkspaceV2.ID.ascending("wrk_wait_empty"), {})).toBeUndefined()
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
@@ -1568,9 +1568,9 @@ describe("workspace waitForSync", () => {
|
||||
const sessionID = SessionID.descending("ses_wait_done")
|
||||
Database.use((db) => db.insert(EventSequenceTable).values({ aggregate_id: sessionID, seq: 4 }).run())
|
||||
|
||||
expect(yield* workspace.waitForSync(WorkspaceID.ascending("wrk_wait_done"), { [sessionID]: 4 })).toBeUndefined()
|
||||
expect(yield* workspace.waitForSync(WorkspaceV2.ID.ascending("wrk_wait_done"), { [sessionID]: 4 })).toBeUndefined()
|
||||
expect(
|
||||
yield* workspace.waitForSync(WorkspaceID.ascending("wrk_wait_done_2"), { [sessionID]: 3 }),
|
||||
yield* workspace.waitForSync(WorkspaceV2.ID.ascending("wrk_wait_done_2"), { [sessionID]: 3 }),
|
||||
).toBeUndefined()
|
||||
}),
|
||||
{ git: true },
|
||||
@@ -1581,7 +1581,7 @@ describe("workspace waitForSync", () => {
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const workspace = yield* Workspace.Service
|
||||
const workspaceID = WorkspaceID.ascending("wrk_wait_event")
|
||||
const workspaceID = WorkspaceV2.ID.ascending("wrk_wait_event")
|
||||
const sessionID = SessionID.descending("ses_wait_event")
|
||||
Database.use((db) => db.insert(EventSequenceTable).values({ aggregate_id: sessionID, seq: 1 }).run())
|
||||
|
||||
@@ -1611,7 +1611,7 @@ describe("workspace waitForSync", () => {
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const workspace = yield* Workspace.Service
|
||||
const workspaceID = WorkspaceID.ascending("wrk_wait_sync_any")
|
||||
const workspaceID = WorkspaceV2.ID.ascending("wrk_wait_sync_any")
|
||||
const sessionID = SessionID.descending("ses_wait_sync_any")
|
||||
Database.use((db) => db.insert(EventSequenceTable).values({ aggregate_id: sessionID, seq: 0 }).run())
|
||||
|
||||
@@ -1628,7 +1628,7 @@ describe("workspace waitForSync", () => {
|
||||
.run(),
|
||||
)
|
||||
GlobalBus.emit("event", {
|
||||
workspace: WorkspaceID.ascending("wrk_other_workspace"),
|
||||
workspace: WorkspaceV2.ID.ascending("wrk_other_workspace"),
|
||||
payload: { type: "sync" },
|
||||
})
|
||||
}),
|
||||
@@ -1648,7 +1648,7 @@ describe("workspace waitForSync", () => {
|
||||
const reason = new Error("caller aborted")
|
||||
const fiber = yield* Effect.forkChild(
|
||||
workspace.waitForSync(
|
||||
WorkspaceID.ascending("wrk_wait_abort"),
|
||||
WorkspaceV2.ID.ascending("wrk_wait_abort"),
|
||||
{ [SessionID.descending("ses_wait_abort")]: 1 },
|
||||
abort.signal,
|
||||
),
|
||||
@@ -1668,7 +1668,7 @@ describe("workspace waitForSync", () => {
|
||||
const sessionID = SessionID.descending("ses_wait_timeout")
|
||||
expectExitContains(
|
||||
yield* Effect.exit(
|
||||
workspace.waitForSync(WorkspaceID.ascending("wrk_wait_timeout"), { [sessionID]: 1 }, undefined, 25),
|
||||
workspace.waitForSync(WorkspaceV2.ID.ascending("wrk_wait_timeout"), { [sessionID]: 1 }, undefined, 25),
|
||||
),
|
||||
`Timed out waiting for sync fence: {"${sessionID}":1}`,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { expect } from "bun:test"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
import { InstanceRef } from "../../src/effect/instance-ref"
|
||||
import { makeRuntime } from "../../src/effect/run-service"
|
||||
import { ProjectID } from "../../src/project/schema"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { it } from "../lib/effect"
|
||||
|
||||
class Shared extends Context.Service<Shared, { readonly id: number }>()("@test/Shared") {}
|
||||
@@ -79,7 +79,7 @@ it.live("makeRuntime inherits InstanceRef from the current fiber", () =>
|
||||
directory: testDirectory,
|
||||
worktree: testDirectory,
|
||||
project: {
|
||||
id: ProjectID.global,
|
||||
id: ProjectV2.ID.global,
|
||||
worktree: testDirectory,
|
||||
time: { created: 0, updated: 0 },
|
||||
sandboxes: [],
|
||||
|
||||
@@ -24,12 +24,10 @@ describe("RuntimeFlags", () => {
|
||||
fromConfig({
|
||||
OPENCODE_PURE: "true",
|
||||
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",
|
||||
OPENCODE_DISABLE_CHANNEL_DB: "true",
|
||||
OPENCODE_AUTO_SHARE: "true",
|
||||
OPENCODE_DISABLE_EMBEDDED_WEB_UI: "true",
|
||||
OPENCODE_DISABLE_EXTERNAL_SKILLS: "true",
|
||||
OPENCODE_DISABLE_LSP_DOWNLOAD: "true",
|
||||
OPENCODE_SKIP_MIGRATIONS: "true",
|
||||
OPENCODE_EXPERIMENTAL: "true",
|
||||
OPENCODE_ENABLE_EXA: "true",
|
||||
OPENCODE_ENABLE_PARALLEL: "true",
|
||||
@@ -43,11 +41,9 @@ describe("RuntimeFlags", () => {
|
||||
expect(flags.pure).toBe(true)
|
||||
expect(flags.autoShare).toBe(true)
|
||||
expect(flags.disableDefaultPlugins).toBe(true)
|
||||
expect(flags.disableChannelDb).toBe(true)
|
||||
expect(flags.disableEmbeddedWebUi).toBe(true)
|
||||
expect(flags.disableExternalSkills).toBe(true)
|
||||
expect(flags.disableLspDownload).toBe(true)
|
||||
expect(flags.skipMigrations).toBe(true)
|
||||
expect(flags.disableClaudeCodePrompt).toBe(false)
|
||||
expect(flags.enableExa).toBe(true)
|
||||
expect(flags.enableParallel).toBe(true)
|
||||
@@ -100,11 +96,9 @@ describe("RuntimeFlags", () => {
|
||||
expect(flags.pure).toBe(false)
|
||||
expect(flags.autoShare).toBe(false)
|
||||
expect(flags.disableDefaultPlugins).toBe(true)
|
||||
expect(flags.disableChannelDb).toBe(false)
|
||||
expect(flags.disableEmbeddedWebUi).toBe(false)
|
||||
expect(flags.disableExternalSkills).toBe(false)
|
||||
expect(flags.disableLspDownload).toBe(false)
|
||||
expect(flags.skipMigrations).toBe(false)
|
||||
expect(flags.disableClaudeCodePrompt).toBe(false)
|
||||
expect(flags.disableClaudeCodeSkills).toBe(false)
|
||||
expect(flags.enableExa).toBe(false)
|
||||
@@ -157,22 +151,6 @@ describe("RuntimeFlags", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("skipMigrations defaults to false", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(Effect.provide(fromConfig({})))
|
||||
|
||||
expect(flags.skipMigrations).toBe(false)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("skipMigrations reads OPENCODE_SKIP_MIGRATIONS", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(Effect.provide(fromConfig({ OPENCODE_SKIP_MIGRATIONS: "true" })))
|
||||
|
||||
expect(flags.skipMigrations).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("disableClaudeCodePrompt defaults to false", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(Effect.provide(fromConfig({})))
|
||||
@@ -318,7 +296,6 @@ describe("RuntimeFlags", () => {
|
||||
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",
|
||||
OPENCODE_DISABLE_EXTERNAL_SKILLS: "true",
|
||||
OPENCODE_DISABLE_LSP_DOWNLOAD: "true",
|
||||
OPENCODE_SKIP_MIGRATIONS: "true",
|
||||
OPENCODE_EXPERIMENTAL: "true",
|
||||
OPENCODE_ENABLE_EXA: "true",
|
||||
OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS: "1234",
|
||||
@@ -330,11 +307,9 @@ describe("RuntimeFlags", () => {
|
||||
|
||||
expect(flags.pure).toBe(false)
|
||||
expect(flags.disableDefaultPlugins).toBe(false)
|
||||
expect(flags.disableChannelDb).toBe(false)
|
||||
expect(flags.disableEmbeddedWebUi).toBe(false)
|
||||
expect(flags.disableExternalSkills).toBe(false)
|
||||
expect(flags.disableLspDownload).toBe(false)
|
||||
expect(flags.skipMigrations).toBe(false)
|
||||
expect(flags.disableClaudeCodePrompt).toBe(false)
|
||||
expect(flags.disableClaudeCodeSkills).toBe(false)
|
||||
expect(flags.enableExa).toBe(false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { WorkspaceID } from "@/control-plane/schema"
|
||||
import type { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Effect, Scope } from "effect"
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Effect, Scope } from "effect"
|
||||
* on entry and restores it via finalizer when the surrounding scope closes —
|
||||
* preserves the original try/finally semantics regardless of test outcome.
|
||||
*/
|
||||
export function withFixedWorkspaceID(id: WorkspaceID): Effect.Effect<void, never, Scope.Scope> {
|
||||
export function withFixedWorkspaceID(id: WorkspaceV2.ID): Effect.Effect<void, never, Scope.Scope> {
|
||||
return Effect.gen(function* () {
|
||||
const previous = Flag.OPENCODE_WORKSPACE_ID
|
||||
Flag.OPENCODE_WORKSPACE_ID = id
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Database } from "@/storage/db"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||
import { ProjectID } from "../../src/project/schema"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { SessionID } from "../../src/session/schema"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { $ } from "bun"
|
||||
@@ -22,7 +22,7 @@ function legacySessionID() {
|
||||
return crypto.randomUUID() as SessionID
|
||||
}
|
||||
|
||||
function seed(opts: { id: SessionID; dir: string; project: ProjectID }) {
|
||||
function seed(opts: { id: SessionID; dir: string; project: ProjectV2.ID }) {
|
||||
const now = Date.now()
|
||||
Database.use((db) =>
|
||||
db
|
||||
@@ -46,7 +46,7 @@ function ensureGlobal() {
|
||||
db
|
||||
.insert(ProjectTable)
|
||||
.values({
|
||||
id: ProjectID.global,
|
||||
id: ProjectV2.ID.global,
|
||||
worktree: "/",
|
||||
time_created: Date.now(),
|
||||
time_updated: Date.now(),
|
||||
@@ -68,17 +68,17 @@ describe("migrateFromGlobal", () => {
|
||||
yield* Effect.promise(() => $`git config commit.gpgsign false`.cwd(tmp).quiet())
|
||||
const projects = yield* Project.Service
|
||||
const { project: pre } = yield* projects.fromDirectory(tmp)
|
||||
expect(pre.id).toBe(ProjectID.global)
|
||||
expect(pre.id).toBe(ProjectV2.ID.global)
|
||||
|
||||
// 2. Seed a session under "global" with matching directory
|
||||
const id = legacySessionID()
|
||||
yield* Effect.sync(() => seed({ id, dir: tmp, project: ProjectID.global }))
|
||||
yield* Effect.sync(() => seed({ id, dir: tmp, project: ProjectV2.ID.global }))
|
||||
|
||||
// 3. Make a commit so the project gets a real ID
|
||||
yield* Effect.promise(() => $`git commit --allow-empty -m "root"`.cwd(tmp).quiet())
|
||||
|
||||
const { project: real } = yield* projects.fromDirectory(tmp)
|
||||
expect(real.id).not.toBe(ProjectID.global)
|
||||
expect(real.id).not.toBe(ProjectV2.ID.global)
|
||||
|
||||
// 4. The session should have been migrated to the real project ID
|
||||
const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get())
|
||||
@@ -93,7 +93,7 @@ describe("migrateFromGlobal", () => {
|
||||
const tmp = yield* tmpdirScoped({ git: true })
|
||||
const projects = yield* Project.Service
|
||||
const { project } = yield* projects.fromDirectory(tmp)
|
||||
expect(project.id).not.toBe(ProjectID.global)
|
||||
expect(project.id).not.toBe(ProjectV2.ID.global)
|
||||
|
||||
// 2. Ensure "global" project row exists (as it would from a prior no-git session)
|
||||
yield* Effect.sync(() => ensureGlobal())
|
||||
@@ -102,7 +102,7 @@ describe("migrateFromGlobal", () => {
|
||||
// This simulates a session created before git init that wasn't
|
||||
// present when the real project row was first created.
|
||||
const id = legacySessionID()
|
||||
yield* Effect.sync(() => seed({ id, dir: tmp, project: ProjectID.global }))
|
||||
yield* Effect.sync(() => seed({ id, dir: tmp, project: ProjectV2.ID.global }))
|
||||
|
||||
// 4. Call fromDirectory again — project row already exists,
|
||||
// so the current code skips migration entirely. This is the bug.
|
||||
@@ -119,20 +119,20 @@ describe("migrateFromGlobal", () => {
|
||||
const tmp = yield* tmpdirScoped({ git: true })
|
||||
const projects = yield* Project.Service
|
||||
const { project } = yield* projects.fromDirectory(tmp)
|
||||
expect(project.id).not.toBe(ProjectID.global)
|
||||
expect(project.id).not.toBe(ProjectV2.ID.global)
|
||||
|
||||
yield* Effect.sync(() => ensureGlobal())
|
||||
|
||||
// Legacy sessions may lack a directory value.
|
||||
// Without a matching origin directory, they should remain global.
|
||||
const id = legacySessionID()
|
||||
yield* Effect.sync(() => seed({ id, dir: "", project: ProjectID.global }))
|
||||
yield* Effect.sync(() => seed({ id, dir: "", project: ProjectV2.ID.global }))
|
||||
|
||||
yield* projects.fromDirectory(tmp)
|
||||
|
||||
const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get())
|
||||
expect(row).toBeDefined()
|
||||
expect(row!.project_id).toBe(ProjectID.global)
|
||||
expect(row!.project_id).toBe(ProjectV2.ID.global)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -141,19 +141,19 @@ describe("migrateFromGlobal", () => {
|
||||
const tmp = yield* tmpdirScoped({ git: true })
|
||||
const projects = yield* Project.Service
|
||||
const { project } = yield* projects.fromDirectory(tmp)
|
||||
expect(project.id).not.toBe(ProjectID.global)
|
||||
expect(project.id).not.toBe(ProjectV2.ID.global)
|
||||
|
||||
yield* Effect.sync(() => ensureGlobal())
|
||||
|
||||
// Seed a session under "global" but for a DIFFERENT directory
|
||||
const id = legacySessionID()
|
||||
yield* Effect.sync(() => seed({ id, dir: "/some/other/dir", project: ProjectID.global }))
|
||||
yield* Effect.sync(() => seed({ id, dir: "/some/other/dir", project: ProjectV2.ID.global }))
|
||||
|
||||
yield* projects.fromDirectory(tmp)
|
||||
const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get())
|
||||
expect(row).toBeDefined()
|
||||
// Should remain under "global" — not stolen
|
||||
expect(row!.project_id).toBe(ProjectID.global)
|
||||
expect(row!.project_id).toBe(ProjectV2.ID.global)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -6,7 +6,6 @@ import { $ } from "bun"
|
||||
import path from "path"
|
||||
import { tmpdirScoped } from "../fixture/fixture"
|
||||
import { GlobalBus } from "../../src/bus/global"
|
||||
import { ProjectID } from "../../src/project/schema"
|
||||
import { Database } from "@/storage/db"
|
||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||
import { PermissionTable, SessionTable } from "@opencode-ai/core/session/sql"
|
||||
@@ -14,13 +13,13 @@ import { WorkspaceTable } from "@opencode-ai/core/control-plane/workspace.sql"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Hash } from "@opencode-ai/core/util/hash"
|
||||
import { SessionID } from "@/session/schema"
|
||||
import { WorkspaceID } from "@/control-plane/schema"
|
||||
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import { Cause, Effect, Exit, Layer, Stream } from "effect"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
import { NodePath } from "@effect/platform-node"
|
||||
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 { testEffect } from "../lib/effect"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
@@ -40,7 +39,7 @@ function run<A, E>(fn: (svc: Project.Interface) => Effect.Effect<A, E>) {
|
||||
}
|
||||
|
||||
function remoteProjectID(remote: string) {
|
||||
return ProjectID.make(Hash.fast(`git-remote:${remote}`))
|
||||
return ProjectV2.ID.make(Hash.fast(`git-remote:${remote}`))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,7 +107,7 @@ const iconDiscoveryIt = testEffect(
|
||||
Layer.provideMerge(projectLayerWithRuntimeFlags({ experimentalIconDiscovery: true }), CrossSpawnSpawner.defaultLayer),
|
||||
)
|
||||
|
||||
function waitForProjectIcon(id: ProjectID, attempts = 50): Effect.Effect<Project.Info> {
|
||||
function waitForProjectIcon(id: ProjectV2.ID, attempts = 50): Effect.Effect<Project.Info> {
|
||||
return Effect.gen(function* () {
|
||||
const project = Project.get(id)
|
||||
if (project?.icon?.url) return project
|
||||
@@ -127,7 +126,7 @@ describe("Project.fromDirectory", () => {
|
||||
const { project } = yield* run((svc) => svc.fromDirectory(tmp))
|
||||
|
||||
expect(project).toBeDefined()
|
||||
expect(project.id).toBe(ProjectID.global)
|
||||
expect(project.id).toBe(ProjectV2.ID.global)
|
||||
expect(project.vcs).toBe("git")
|
||||
expect(project.worktree).toBe(tmp)
|
||||
|
||||
@@ -143,7 +142,7 @@ describe("Project.fromDirectory", () => {
|
||||
const { project } = yield* run((svc) => svc.fromDirectory(tmp))
|
||||
|
||||
expect(project).toBeDefined()
|
||||
expect(project.id).not.toBe(ProjectID.global)
|
||||
expect(project.id).not.toBe(ProjectV2.ID.global)
|
||||
expect(project.vcs).toBe("git")
|
||||
expect(project.worktree).toBe(tmp)
|
||||
}),
|
||||
@@ -153,7 +152,7 @@ describe("Project.fromDirectory", () => {
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* tmpdirScoped()
|
||||
const { project } = yield* run((svc) => svc.fromDirectory(tmp))
|
||||
expect(project.id).toBe(ProjectID.global)
|
||||
expect(project.id).toBe(ProjectV2.ID.global)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -199,7 +198,7 @@ describe("Project.fromDirectory", () => {
|
||||
const { project: rootProject } = yield* projects.fromDirectory(tmp)
|
||||
const remoteID = remoteProjectID("github.com/acme/app")
|
||||
const sessionID = crypto.randomUUID() as SessionID
|
||||
const workspaceID = WorkspaceID.ascending()
|
||||
const workspaceID = WorkspaceV2.ID.ascending()
|
||||
|
||||
yield* Effect.sync(() => {
|
||||
Database.use((db) => {
|
||||
@@ -264,7 +263,7 @@ describe("Project.fromDirectory git failure paths", () => {
|
||||
// rev-list fails because HEAD doesn't exist yet: this is the natural scenario.
|
||||
const { project } = yield* run((svc) => svc.fromDirectory(tmp))
|
||||
expect(project.vcs).toBe("git")
|
||||
expect(project.id).toBe(ProjectID.global)
|
||||
expect(project.id).toBe(ProjectV2.ID.global)
|
||||
expect(project.worktree).toBe(tmp)
|
||||
}),
|
||||
)
|
||||
@@ -586,7 +585,7 @@ describe("Project.update", () => {
|
||||
Effect.gen(function* () {
|
||||
const exit = yield* run((svc) =>
|
||||
svc.update({
|
||||
projectID: ProjectID.make("nonexistent-project-id"),
|
||||
projectID: ProjectV2.ID.make("nonexistent-project-id"),
|
||||
name: "Should Fail",
|
||||
}),
|
||||
).pipe(Effect.exit)
|
||||
@@ -665,7 +664,7 @@ describe("Project.list and Project.get", () => {
|
||||
)
|
||||
|
||||
test("get returns undefined for unknown id", () => {
|
||||
const found = Project.get(ProjectID.make("nonexistent"))
|
||||
const found = Project.get(ProjectV2.ID.make("nonexistent"))
|
||||
expect(found).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -740,7 +739,7 @@ describe("Project.fromDirectory with bare repos", () => {
|
||||
|
||||
const { project } = yield* run((svc) => svc.fromDirectory(worktreePath))
|
||||
|
||||
expect(project.id).not.toBe(ProjectID.global)
|
||||
expect(project.id).not.toBe(ProjectV2.ID.global)
|
||||
expect(project.worktree).toBe(worktreePath)
|
||||
|
||||
const correctCache = path.join(barePath, "opencode")
|
||||
@@ -803,7 +802,7 @@ describe("Project.fromDirectory with bare repos", () => {
|
||||
|
||||
const { project } = yield* run((svc) => svc.fromDirectory(worktreePath))
|
||||
|
||||
expect(project.id).not.toBe(ProjectID.global)
|
||||
expect(project.id).not.toBe(ProjectV2.ID.global)
|
||||
expect(project.worktree).toBe(worktreePath)
|
||||
|
||||
const correctCache = path.join(barePath, "opencode")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { WorkspaceID } from "../../src/control-plane/schema"
|
||||
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import { PtyID } from "../../src/pty/schema"
|
||||
import { PtyTicket } from "../../src/pty/ticket"
|
||||
import { testEffect } from "../lib/effect"
|
||||
@@ -47,10 +47,10 @@ describe("PTY websocket tickets", () => {
|
||||
Effect.gen(function* () {
|
||||
const tickets = yield* PtyTicket.Service
|
||||
const ptyID = PtyID.ascending()
|
||||
const workspaceID = WorkspaceID.ascending()
|
||||
const workspaceID = WorkspaceV2.ID.ascending()
|
||||
const issued = yield* tickets.issue({ ptyID, workspaceID })
|
||||
|
||||
expect(yield* tickets.consume({ ptyID, workspaceID: WorkspaceID.ascending(), ticket: issued.ticket })).toBe(false)
|
||||
expect(yield* tickets.consume({ ptyID, workspaceID: WorkspaceV2.ID.ascending(), ticket: issued.ticket })).toBe(false)
|
||||
expect(yield* tickets.consume({ ptyID, workspaceID, ticket: issued.ticket })).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as Socket from "effect/unstable/socket/Socket"
|
||||
import { mkdir } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { registerAdapter } from "../../src/control-plane/adapters"
|
||||
import { WorkspaceID } from "../../src/control-plane/schema"
|
||||
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import type { WorkspaceAdapter } from "../../src/control-plane/types"
|
||||
import { Workspace } from "../../src/control-plane/workspace"
|
||||
import { InstanceRef, WorkspaceRef } from "../../src/effect/instance-ref"
|
||||
@@ -198,7 +198,7 @@ describe("HttpApi instance context middleware", () => {
|
||||
|
||||
it.live("uses configured workspace id instead of routing to the requested workspace", () =>
|
||||
Effect.gen(function* () {
|
||||
const fixedWorkspaceID = WorkspaceID.ascending()
|
||||
const fixedWorkspaceID = WorkspaceV2.ID.ascending()
|
||||
yield* withFixedWorkspaceID(fixedWorkspaceID)
|
||||
|
||||
const dir = yield* tmpdirScoped({ git: true })
|
||||
@@ -226,7 +226,7 @@ describe("HttpApi instance context middleware", () => {
|
||||
|
||||
it.live("falls through to local instead of MissingWorkspace when configured workspace id is set", () =>
|
||||
Effect.gen(function* () {
|
||||
const fixedWorkspaceID = WorkspaceID.ascending()
|
||||
const fixedWorkspaceID = WorkspaceV2.ID.ascending()
|
||||
yield* withFixedWorkspaceID(fixedWorkspaceID)
|
||||
|
||||
const dir = yield* tmpdirScoped({ git: true })
|
||||
@@ -238,7 +238,7 @@ describe("HttpApi instance context middleware", () => {
|
||||
// MissingWorkspace response. With the env set, planRequest must skip the
|
||||
// MissingWorkspace branch and fall through to Local with the configured
|
||||
// workspace id.
|
||||
const unknownWorkspaceID = WorkspaceID.ascending()
|
||||
const unknownWorkspaceID = WorkspaceV2.ID.ascending()
|
||||
const response = yield* HttpClientRequest.get(`/probe?workspace=${unknownWorkspaceID}`).pipe(
|
||||
HttpClientRequest.setHeader("x-opencode-directory", dir),
|
||||
HttpClient.execute,
|
||||
@@ -254,7 +254,7 @@ describe("HttpApi instance context middleware", () => {
|
||||
|
||||
it.live("keeps configured workspace id on control-plane routes without remote routing", () =>
|
||||
Effect.gen(function* () {
|
||||
const fixedWorkspaceID = WorkspaceID.ascending()
|
||||
const fixedWorkspaceID = WorkspaceV2.ID.ascending()
|
||||
yield* withFixedWorkspaceID(fixedWorkspaceID)
|
||||
|
||||
const dir = yield* tmpdirScoped({ git: true })
|
||||
|
||||
@@ -4,12 +4,12 @@ import { describe, expect } from "bun:test"
|
||||
import { Config, Context, Effect, FileSystem, Layer, Path } from "effect"
|
||||
import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http"
|
||||
import * as Socket from "effect/unstable/socket/Socket"
|
||||
import { WorkspaceID } from "../../src/control-plane/schema"
|
||||
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import { ControlPaths } from "../../src/server/routes/instance/httpapi/groups/control"
|
||||
import { InstancePaths } from "../../src/server/routes/instance/httpapi/groups/instance"
|
||||
import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session"
|
||||
import { PermissionID } from "../../src/permission/schema"
|
||||
import { ProjectID } from "../../src/project/schema"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { QuestionID } from "../../src/question/schema"
|
||||
import { HttpApiApp } from "../../src/server/routes/instance/httpapi/server"
|
||||
import { HEADER as FenceHeader } from "../../src/server/shared/fence"
|
||||
@@ -76,7 +76,7 @@ describe("instance HttpApi", () => {
|
||||
it.live("emits a sync fence header for fixed-workspace mutations", () =>
|
||||
Effect.gen(function* () {
|
||||
const originalWorkspaceID = Flag.OPENCODE_WORKSPACE_ID
|
||||
Flag.OPENCODE_WORKSPACE_ID = WorkspaceID.ascending()
|
||||
Flag.OPENCODE_WORKSPACE_ID = WorkspaceV2.ID.ascending()
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.sync(() => {
|
||||
Flag.OPENCODE_WORKSPACE_ID = originalWorkspaceID
|
||||
@@ -98,7 +98,7 @@ describe("instance HttpApi", () => {
|
||||
it.live("does not emit sync fence headers for fixed-workspace reads or no-op mutations", () =>
|
||||
Effect.gen(function* () {
|
||||
const originalWorkspaceID = Flag.OPENCODE_WORKSPACE_ID
|
||||
Flag.OPENCODE_WORKSPACE_ID = WorkspaceID.ascending()
|
||||
Flag.OPENCODE_WORKSPACE_ID = WorkspaceV2.ID.ascending()
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.sync(() => {
|
||||
Flag.OPENCODE_WORKSPACE_ID = originalWorkspaceID
|
||||
@@ -209,7 +209,7 @@ describe("instance HttpApi", () => {
|
||||
it.live("returns typed not found bodies for missing projects", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped({ git: true })
|
||||
const projectID = ProjectID.make("project_missing")
|
||||
const projectID = ProjectV2.ID.make("project_missing")
|
||||
const response = yield* Effect.promise(() =>
|
||||
HttpApiApp.webHandler().handler(
|
||||
new Request(`http://localhost/project/${projectID}`, {
|
||||
|
||||
@@ -15,7 +15,7 @@ import Http from "node:http"
|
||||
import { mkdir } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { registerAdapter } from "../../src/control-plane/adapters"
|
||||
import { WorkspaceID } from "../../src/control-plane/schema"
|
||||
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import type { WorkspaceAdapter } from "../../src/control-plane/types"
|
||||
import { Workspace } from "../../src/control-plane/workspace"
|
||||
import { WorkspaceTable } from "@opencode-ai/core/control-plane/workspace.sql"
|
||||
@@ -161,7 +161,7 @@ const insertRemoteWorkspaceWithoutSync = (input: {
|
||||
url: string
|
||||
}) =>
|
||||
Effect.sync(() => {
|
||||
const id = WorkspaceID.ascending()
|
||||
const id = WorkspaceV2.ID.ascending()
|
||||
registerAdapter(input.projectID, input.type, remoteAdapter(path.join(input.dir, `.${input.type}`), input.url))
|
||||
Database.use((db) => db.insert(WorkspaceTable).values({ id, type: input.type, project_id: input.projectID }).run())
|
||||
return id
|
||||
@@ -286,9 +286,9 @@ describe("HttpApi workspace routing middleware", () => {
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped({ git: true })
|
||||
const project = yield* Project.use.fromDirectory(dir)
|
||||
const workspaceID = WorkspaceID.ascending()
|
||||
const workspaceID = WorkspaceV2.ID.ascending()
|
||||
const type = "remote-http-fence-target"
|
||||
const waited = yield* Ref.make<{ workspaceID: WorkspaceID; state: Record<string, number> } | undefined>(undefined)
|
||||
const waited = yield* Ref.make<{ workspaceID: WorkspaceV2.ID; state: Record<string, number> } | undefined>(undefined)
|
||||
|
||||
const remoteUrl = yield* startRemoteWorkspaceHttpServer(() =>
|
||||
HttpServerResponse.json(
|
||||
@@ -403,7 +403,7 @@ describe("HttpApi workspace routing middleware", () => {
|
||||
|
||||
it.live("returns a missing workspace response for unknown workspace ids", () =>
|
||||
Effect.gen(function* () {
|
||||
const workspaceID = WorkspaceID.ascending("wrk_missing")
|
||||
const workspaceID = WorkspaceV2.ID.ascending("wrk_missing")
|
||||
// If the middleware resolves the workspace first, this handler is never
|
||||
// reached and the response should be the middleware error response.
|
||||
yield* HttpRouter.add("GET", "/probe", HttpServerResponse.text("route called")).pipe(
|
||||
|
||||
@@ -5,7 +5,7 @@ import path from "node:path"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { registerAdapter } from "../../src/control-plane/adapters"
|
||||
import { WorkspaceID } from "../../src/control-plane/schema"
|
||||
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import type { WorkspaceAdapter } from "../../src/control-plane/types"
|
||||
import { Workspace } from "../../src/control-plane/workspace"
|
||||
import { WorkspacePaths } from "../../src/server/routes/instance/httpapi/groups/workspace"
|
||||
@@ -255,7 +255,7 @@ describe("workspace HttpApi", () => {
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped({ git: true })
|
||||
const session = yield* Session.use.create({}).pipe(provideInstance(dir))
|
||||
const workspaceID = WorkspaceID.ascending("wrk_missing_warp")
|
||||
const workspaceID = WorkspaceV2.ID.ascending("wrk_missing_warp")
|
||||
|
||||
const response = yield* request(WorkspacePaths.warp, dir, {
|
||||
method: "POST",
|
||||
|
||||
@@ -8,8 +8,8 @@ import { SessionStatus } from "../../src/session/status"
|
||||
import { SessionSummary } from "../../src/session/summary"
|
||||
import { Todo } from "../../src/session/todo"
|
||||
import { SessionID, MessageID, PartID } from "../../src/session/schema"
|
||||
import { ProjectID } from "../../src/project/schema"
|
||||
import { WorkspaceID } from "../../src/control-plane/schema"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
|
||||
// Covers the session-domain Effect Schema migration. For each migrated
|
||||
// schema we assert:
|
||||
@@ -22,8 +22,8 @@ const sessionID = Schema.decodeUnknownSync(SessionID)("ses_01J5Y5H0AH4Q4NXJ6P4C3
|
||||
const sessionIDChild = Schema.decodeUnknownSync(SessionID)("ses_01J5Y5H0AH4Q4NXJ6P4C3P5V2L")
|
||||
const messageID = Schema.decodeUnknownSync(MessageID)("msg_01J5Y5H0AH4Q4NXJ6P4C3P5V2M")
|
||||
const partID = Schema.decodeUnknownSync(PartID)("prt_01J5Y5H0AH4Q4NXJ6P4C3P5V2N")
|
||||
const projectID = ProjectID.make("proj-alpha")
|
||||
const workspaceID = Schema.decodeUnknownSync(WorkspaceID)("wrk-primary")
|
||||
const projectID = ProjectV2.ID.make("proj-alpha")
|
||||
const workspaceID = Schema.decodeUnknownSync(WorkspaceV2.ID)("wrk-primary")
|
||||
|
||||
function decodeUnknown<S extends Schema.Top>(schema: S) {
|
||||
const decode = Schema.decodeUnknownSync(schema as any)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Schema } from "effect"
|
||||
import { ProjectID } from "../../src/project/schema"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { MessageID, SessionID } from "../../src/session/schema"
|
||||
import { Session } from "../../src/session/session"
|
||||
|
||||
const info = {
|
||||
id: SessionID.descending(),
|
||||
slug: "test-session",
|
||||
projectID: ProjectID.global,
|
||||
projectID: ProjectV2.ID.global,
|
||||
workspaceID: undefined,
|
||||
directory: "/tmp/opencode",
|
||||
parentID: undefined,
|
||||
@@ -43,7 +43,7 @@ describe("Session schema", () => {
|
||||
const encoded = Schema.encodeUnknownSync(Session.GlobalInfo)({
|
||||
...info,
|
||||
project: {
|
||||
id: ProjectID.global,
|
||||
id: ProjectV2.ID.global,
|
||||
name: undefined,
|
||||
worktree: "/tmp/opencode",
|
||||
},
|
||||
|
||||
@@ -1,38 +1,9 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import path from "path"
|
||||
import { Effect } from "effect"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { InstallationChannel } from "@opencode-ai/core/installation/version"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { Database as CoreDatabase } from "@opencode-ai/core/database/database"
|
||||
import { Database } from "@/storage/db"
|
||||
import { it } from "../lib/effect"
|
||||
|
||||
describe("Database.getChannelPath", () => {
|
||||
it.effect("returns database path for the current channel", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* RuntimeFlags.Service
|
||||
const expected = ["latest", "beta", "prod"].includes(InstallationChannel)
|
||||
? path.join(Global.Path.data, "opencode.db")
|
||||
: path.join(Global.Path.data, `opencode-${InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-")}.db`)
|
||||
|
||||
expect(Database.getChannelPath(flags)).toBe(expected)
|
||||
}).pipe(Effect.provide(RuntimeFlags.layer())),
|
||||
)
|
||||
|
||||
it.effect("uses the shared database path when channel databases are disabled", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* RuntimeFlags.Service
|
||||
|
||||
expect(Database.getChannelPath(flags)).toBe(path.join(Global.Path.data, "opencode.db"))
|
||||
}).pipe(Effect.provide(RuntimeFlags.layer({ disableChannelDb: true }))),
|
||||
)
|
||||
|
||||
it.effect("accepts RuntimeFlags with skipMigrations for database callers", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* RuntimeFlags.Service
|
||||
|
||||
expect(flags.skipMigrations).toBe(true)
|
||||
expect(Database.getChannelPath(flags)).toBe(Database.getChannelPath({ disableChannelDb: flags.disableChannelDb }))
|
||||
}).pipe(Effect.provide(RuntimeFlags.layer({ skipMigrations: true }))),
|
||||
)
|
||||
describe("Database.getPath", () => {
|
||||
it("delegates to the core database path", () => {
|
||||
expect(Database.getPath()).toBe(CoreDatabase.path())
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@ import { readFileSync, readdirSync } from "fs"
|
||||
import { JsonMigration } from "@/storage/json-migration"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||
import { ProjectID } from "../../src/project/schema"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "@opencode-ai/core/session/sql"
|
||||
import { SessionShareTable } from "@opencode-ai/core/share/sql"
|
||||
import { SessionID, MessageID, PartID } from "../../src/session/schema"
|
||||
@@ -127,7 +127,7 @@ describe("JSON to SQLite migration", () => {
|
||||
|
||||
const projects = db.select().from(ProjectTable).all()
|
||||
expect(projects.length).toBe(1)
|
||||
expect(projects[0].id).toBe(ProjectID.make("proj_test123abc"))
|
||||
expect(projects[0].id).toBe(ProjectV2.ID.make("proj_test123abc"))
|
||||
expect(projects[0].worktree).toBe("/test/path")
|
||||
expect(projects[0].name).toBe("Test Project")
|
||||
expect(projects[0].sandboxes).toEqual(["/test/sandbox"])
|
||||
@@ -151,7 +151,7 @@ describe("JSON to SQLite migration", () => {
|
||||
|
||||
const projects = db.select().from(ProjectTable).all()
|
||||
expect(projects.length).toBe(1)
|
||||
expect(projects[0].id).toBe(ProjectID.make("proj_filename")) // Uses filename, not JSON id
|
||||
expect(projects[0].id).toBe(ProjectV2.ID.make("proj_filename")) // Uses filename, not JSON id
|
||||
})
|
||||
|
||||
test("migrates project with commands", async () => {
|
||||
@@ -171,7 +171,7 @@ describe("JSON to SQLite migration", () => {
|
||||
|
||||
const projects = db.select().from(ProjectTable).all()
|
||||
expect(projects.length).toBe(1)
|
||||
expect(projects[0].id).toBe(ProjectID.make("proj_with_commands"))
|
||||
expect(projects[0].id).toBe(ProjectV2.ID.make("proj_with_commands"))
|
||||
expect(projects[0].commands).toEqual({ start: "npm run dev" })
|
||||
})
|
||||
|
||||
@@ -191,7 +191,7 @@ describe("JSON to SQLite migration", () => {
|
||||
|
||||
const projects = db.select().from(ProjectTable).all()
|
||||
expect(projects.length).toBe(1)
|
||||
expect(projects[0].id).toBe(ProjectID.make("proj_no_commands"))
|
||||
expect(projects[0].id).toBe(ProjectV2.ID.make("proj_no_commands"))
|
||||
expect(projects[0].commands).toBeNull()
|
||||
})
|
||||
|
||||
@@ -220,7 +220,7 @@ describe("JSON to SQLite migration", () => {
|
||||
const sessions = db.select().from(SessionTable).all()
|
||||
expect(sessions.length).toBe(1)
|
||||
expect(sessions[0].id).toBe(SessionID.make("ses_test456def"))
|
||||
expect(sessions[0].project_id).toBe(ProjectID.make("proj_test123abc"))
|
||||
expect(sessions[0].project_id).toBe(ProjectV2.ID.make("proj_test123abc"))
|
||||
expect(sessions[0].slug).toBe("test-session")
|
||||
expect(sessions[0].title).toBe("Test Session Title")
|
||||
expect(sessions[0].summary_additions).toBe(10)
|
||||
@@ -421,7 +421,7 @@ describe("JSON to SQLite migration", () => {
|
||||
const sessions = db.select().from(SessionTable).all()
|
||||
expect(sessions.length).toBe(1)
|
||||
expect(sessions[0].id).toBe(SessionID.make("ses_migrated"))
|
||||
expect(sessions[0].project_id).toBe(ProjectID.make(gitBasedProjectID)) // Uses directory, not stale JSON
|
||||
expect(sessions[0].project_id).toBe(ProjectV2.ID.make(gitBasedProjectID)) // Uses directory, not stale JSON
|
||||
})
|
||||
|
||||
test("uses filename for session id when JSON has different value", async () => {
|
||||
@@ -452,7 +452,7 @@ describe("JSON to SQLite migration", () => {
|
||||
const sessions = db.select().from(SessionTable).all()
|
||||
expect(sessions.length).toBe(1)
|
||||
expect(sessions[0].id).toBe(SessionID.make("ses_from_filename")) // Uses filename, not JSON id
|
||||
expect(sessions[0].project_id).toBe(ProjectID.make("proj_test123abc"))
|
||||
expect(sessions[0].project_id).toBe(ProjectV2.ID.make("proj_test123abc"))
|
||||
})
|
||||
|
||||
test("is idempotent (running twice doesn't duplicate)", async () => {
|
||||
@@ -631,7 +631,7 @@ describe("JSON to SQLite migration", () => {
|
||||
|
||||
const projects = db.select().from(ProjectTable).all()
|
||||
expect(projects.length).toBe(1)
|
||||
expect(projects[0].id).toBe(ProjectID.make("proj_test123abc"))
|
||||
expect(projects[0].id).toBe(ProjectV2.ID.make("proj_test123abc"))
|
||||
})
|
||||
|
||||
test("skips invalid todo entries while preserving source positions", async () => {
|
||||
|
||||
Reference in New Issue
Block a user