diff --git a/packages/core/src/control-plane/workspace.sql.ts b/packages/core/src/control-plane/workspace.sql.ts index b7c7e1d6c..ef5195216 100644 --- a/packages/core/src/control-plane/workspace.sql.ts +++ b/packages/core/src/control-plane/workspace.sql.ts @@ -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() + .$type() .notNull() .references(() => ProjectTable.id, { onDelete: "cascade" }), time_used: integer() diff --git a/packages/core/src/database/database.ts b/packages/core/src/database/database.ts index 2840c256c..830814619 100644 --- a/packages/core/src/database/database.ts +++ b/packages/core/src/database/database.ts @@ -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 @@ -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)))) +} diff --git a/packages/core/src/database/migration.ts b/packages/core/src/database/migration.ts index 4d018bc40..42aebf02d 100644 --- a/packages/core/src/database/migration.ts +++ b/packages/core/src/database/migration.ts @@ -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()})`, + ) }), ) } diff --git a/packages/core/src/project.ts b/packages/core/src/project.ts index 9c265d75b..2cd65687d 100644 --- a/packages/core/src/project.ts +++ b/packages/core/src/project.ts @@ -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" diff --git a/packages/core/src/project/sql.ts b/packages/core/src/project/sql.ts index e70cba7f5..1588446cf 100644 --- a/packages/core/src/project/sql.ts +++ b/packages/core/src/project/sql.ts @@ -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().primaryKey(), + id: text().$type().primaryKey(), worktree: text().notNull(), vcs: text(), name: text(), diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index f70fafaf7..74f3a80c7 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -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()("@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, diff --git a/packages/core/src/session/schema.ts b/packages/core/src/session/schema.ts index fe4626d4a..8562a097e 100644 --- a/packages/core/src/session/schema.ts +++ b/packages/core/src/session/schema.ts @@ -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("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), diff --git a/packages/core/src/session/sql.ts b/packages/core/src/session/sql.ts index 0cea7b913..f99187e9f 100644 --- a/packages/core/src/session/sql.ts +++ b/packages/core/src/session/sql.ts @@ -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().primaryKey(), project_id: text() - .$type() + .$type() .notNull() .references(() => ProjectTable.id, { onDelete: "cascade" }), workspace_id: text().$type(), diff --git a/packages/core/src/workspace.ts b/packages/core/src/workspace.ts index da0cdf973..a532ba525 100644 --- a/packages/core/src/workspace.ts +++ b/packages/core/src/workspace.ts @@ -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 diff --git a/packages/core/test/project.test.ts b/packages/core/test/project.test.ts index c5b96b638..f6b3a884f 100644 --- a/packages/core/test/project.test.ts +++ b/packages/core/test/project.test.ts @@ -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") }), diff --git a/packages/opencode/src/control-plane/adapters/index.ts b/packages/opencode/src/control-plane/adapters/index.ts index e5fa13714..0b052f5c9 100644 --- a/packages/opencode/src/control-plane/adapters/index.ts +++ b/packages/opencode/src/control-plane/adapters/index.ts @@ -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 = { worktree: WorktreeAdapter, } -const state = new Map>() +const state = new Map>() -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() adapters.set(type, adapter) state.set(projectID, adapters) diff --git a/packages/opencode/src/control-plane/schema.ts b/packages/opencode/src/control-plane/schema.ts deleted file mode 100644 index 7af1e59c8..000000000 --- a/packages/opencode/src/control-plane/schema.ts +++ /dev/null @@ -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 diff --git a/packages/opencode/src/control-plane/types.ts b/packages/opencode/src/control-plane/types.ts index daa837453..f54a878db 100644 --- a/packages/opencode/src/control-plane/types.ts +++ b/packages/opencode/src/control-plane/types.ts @@ -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> @@ -40,7 +40,7 @@ export type Target = export type WorkspaceAdapterContext = { readonly instance?: InstanceContext - readonly workspaceID?: WorkspaceID + readonly workspaceID?: WorkspaceV2.ID } export type WorkspaceAdapter = { diff --git a/packages/opencode/src/control-plane/workspace-context.ts b/packages/opencode/src/control-plane/workspace-context.ts index 2e6aff1be..52229e563 100644 --- a/packages/opencode/src/control-plane/workspace-context.ts +++ b/packages/opencode/src/control-plane/workspace-context.ts @@ -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("instance") export const WorkspaceContext = { - async provide(input: { workspaceID?: WorkspaceID; fn: () => R }): Promise { + async provide(input: { workspaceID?: WorkspaceV2.ID; fn: () => R }): Promise { return context.provide({ workspaceID: input.workspaceID }, () => input.fn()) }, - restore(workspaceID: WorkspaceID, fn: () => R): R { + restore(workspaceID: WorkspaceV2.ID, fn: () => R): R { return context.provide({ workspaceID }, fn) }, diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index 361859bc3..cc68ae6ff 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -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 @@ -80,16 +80,16 @@ const db = (fn: (d: Parameters[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 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 Effect.Effect readonly list: (project: Project.Info) => Effect.Effect readonly syncList: (project: Project.Info) => Effect.Effect - readonly get: (id: WorkspaceID) => Effect.Effect - readonly remove: (id: WorkspaceID) => Effect.Effect + readonly get: (id: WorkspaceV2.ID) => Effect.Effect + readonly remove: (id: WorkspaceV2.ID) => Effect.Effect readonly status: () => Effect.Effect - readonly isSyncing: (workspaceID: WorkspaceID) => Effect.Effect + readonly isSyncing: (workspaceID: WorkspaceV2.ID) => Effect.Effect readonly waitForSync: ( - workspaceID: WorkspaceID, + workspaceID: WorkspaceV2.ID, state: Record, signal?: AbortSignal, timeout?: number, ) => Effect.Effect - readonly startWorkspaceSyncing: (projectID: ProjectID) => Effect.Effect + readonly startWorkspaceSyncing: (projectID: ProjectV2.ID) => Effect.Effect } export class Service extends Context.Service()("@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() - const syncFibers = yield* FiberMap.make() + const connections = new Map() + const syncFibers = yield* FiberMap.make() - 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 = (input: { - workspaceID?: WorkspaceID + workspaceID?: WorkspaceV2.ID local: () => Effect.Effect 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, 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 }) diff --git a/packages/opencode/src/effect/bridge.ts b/packages/opencode/src/effect/bridge.ts index 99f16f437..a51c2938d 100644 --- a/packages/opencode/src/effect/bridge.ts +++ b/packages/opencode/src/effect/bridge.ts @@ -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: (fn: (...args: Args) => Result) => (...args: Args) => Result } -function restoreWorkspace(workspace: WorkspaceID | undefined, fn: () => R): R { +function restoreWorkspace(workspace: WorkspaceV2.ID | undefined, fn: () => R): R { if (workspace !== undefined) return WorkspaceContext.restore(workspace, fn) return fn() } diff --git a/packages/opencode/src/effect/instance-ref.ts b/packages/opencode/src/effect/instance-ref.ts index d95932c2d..49636c1f4 100644 --- a/packages/opencode/src/effect/instance-ref.ts +++ b/packages/opencode/src/effect/instance-ref.ts @@ -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("~opencode/InstanceRef", { defaultValue: () => undefined, }) -export const WorkspaceRef = Context.Reference("~opencode/WorkspaceRef", { +export const WorkspaceRef = Context.Reference("~opencode/WorkspaceRef", { defaultValue: () => undefined, }) diff --git a/packages/opencode/src/effect/runtime-flags.ts b/packages/opencode/src/effect/runtime-flags.ts index 8d9d1fd97..88d8b342b 100644 --- a/packages/opencode/src/effect/runtime-flags.ts +++ b/packages/opencode/src/effect/runtime-flags.ts @@ -15,11 +15,9 @@ export class Service extends ConfigService.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"), diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index d20f29dd4..3498d0ef7 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -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) diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index 44b6b0975..7cc211f2f 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -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 export const Approval = Schema.Struct({ - projectID: ProjectID, + projectID: ProjectV2.ID, patterns: Schema.Array(Schema.String), }).annotate({ identifier: "PermissionApproval" }) export type Approval = Schema.Schema.Type diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 6628bacda..e88657082 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -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(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> export class NotFoundError extends Schema.TaggedErrorClass()("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 readonly list: () => Effect.Effect - readonly get: (id: ProjectID) => Effect.Effect + readonly get: (id: ProjectV2.ID) => Effect.Effect readonly update: (input: UpdateInput) => Effect.Effect readonly initGit: (input: { directory: string; project: Info }) => Effect.Effect - readonly setInitialized: (id: ProjectID) => Effect.Effect - readonly sandboxes: (id: ProjectID) => Effect.Effect - readonly addSandbox: (id: ProjectID, directory: string) => Effect.Effect - readonly removeSandbox: (id: ProjectID, directory: string) => Effect.Effect + readonly setInitialized: (id: ProjectV2.ID) => Effect.Effect + readonly sandboxes: (id: ProjectV2.ID) => Effect.Effect + readonly addSandbox: (id: ProjectV2.ID, directory: string) => Effect.Effect + readonly removeSandbox: (id: ProjectV2.ID, directory: string) => Effect.Effect } export class Service extends Context.Service()("@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(), ) diff --git a/packages/opencode/src/project/schema.ts b/packages/opencode/src/project/schema.ts deleted file mode 100644 index df4cbe48d..000000000 --- a/packages/opencode/src/project/schema.ts +++ /dev/null @@ -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 diff --git a/packages/opencode/src/pty/ticket.ts b/packages/opencode/src/pty/ticket.ts index 0978e5208..cf6751fb1 100644 --- a/packages/opencode/src/pty/ticket.ts +++ b/packages/opencode/src/pty/ticket.ts @@ -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 { diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/project.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/project.ts index b7be4044f..c6b2fab40 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/project.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/project.ts @@ -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"), diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/project.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/project.ts index 1b61204c4..8b4fc608f 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/project.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/project.ts @@ -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( diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts index 4033aa831..0de8a1224 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts @@ -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({ diff --git a/packages/opencode/src/server/routes/instance/httpapi/middleware/workspace-routing.ts b/packages/opencode/src/server/routes/instance/httpapi/middleware/workspace-routing.ts index 8bffe5964..267d7d1a9 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/middleware/workspace-routing.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/middleware/workspace-routing.ts @@ -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 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 { 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 { return Effect.gen(function* () { const url = requestURL(request) diff --git a/packages/opencode/src/server/shared/fence.ts b/packages/opencode/src/server/shared/fence.ts index 346fb39a9..8cf27ed50 100644 --- a/packages/opencode/src/server/shared/fence.ts +++ b/packages/opencode/src/server/shared/fence.ts @@ -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, diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index 453e5acb9..54b0e98e5 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -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> 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> @@ -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 permission?: Permission.Ruleset - workspaceID?: WorkspaceID + workspaceID?: WorkspaceV2.ID }) => Effect.Effect readonly fork: (input: { sessionID: SessionID; messageID?: MessageID }) => Effect.Effect readonly touch: (sessionID: SessionID) => Effect.Effect @@ -526,7 +526,7 @@ export const layer: Layer.Layer< agent?: string model?: Schema.Schema.Type 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 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 }, ) { diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 3d8c172e8..f7e1feaba 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -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 - -const readRuntimeFlags = () => - Effect.runSync(RuntimeFlags.Service.useSync((flags) => flags).pipe(Effect.provide(RuntimeFlags.defaultLayer))) - -export function getChannelPath(flags: Pick = 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) => { - 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") diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts index c78598d6c..534bba2fc 100644 --- a/packages/opencode/src/worktree/index.ts +++ b/packages/opencode/src/worktree/index.ts @@ -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()), diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 6ce0acdb2..d60ce0fe2 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -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 { diff --git a/packages/opencode/test/control-plane/adapters.test.ts b/packages/opencode/test/control-plane/adapters.test.ts index 762bb5d57..fbeb7eeb2 100644 --- a/packages/opencode/test/control-plane/adapters.test.ts +++ b/packages/opencode/test/control-plane/adapters.test.ts @@ -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({ diff --git a/packages/opencode/test/control-plane/workspace.test.ts b/packages/opencode/test/control-plane/workspace.test.ts index f038734b6..6b8f302f2 100644 --- a/packages/opencode/test/control-plane/workspace.test.ts +++ b/packages/opencode/test/control-plane/workspace.test.ts @@ -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 { +function workspaceInfo(projectID: ProjectV2.ID, type: string, input?: Partial): 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}`, ) diff --git a/packages/opencode/test/effect/run-service.test.ts b/packages/opencode/test/effect/run-service.test.ts index 16538bb8a..08c8fef43 100644 --- a/packages/opencode/test/effect/run-service.test.ts +++ b/packages/opencode/test/effect/run-service.test.ts @@ -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()("@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: [], diff --git a/packages/opencode/test/effect/runtime-flags.test.ts b/packages/opencode/test/effect/runtime-flags.test.ts index 27b29b1ac..bf7c7ae9c 100644 --- a/packages/opencode/test/effect/runtime-flags.test.ts +++ b/packages/opencode/test/effect/runtime-flags.test.ts @@ -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) diff --git a/packages/opencode/test/fixture/flag.ts b/packages/opencode/test/fixture/flag.ts index 224c5ef1f..cf00d9e7b 100644 --- a/packages/opencode/test/fixture/flag.ts +++ b/packages/opencode/test/fixture/flag.ts @@ -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 { +export function withFixedWorkspaceID(id: WorkspaceV2.ID): Effect.Effect { return Effect.gen(function* () { const previous = Flag.OPENCODE_WORKSPACE_ID Flag.OPENCODE_WORKSPACE_ID = id diff --git a/packages/opencode/test/project/migrate-global.test.ts b/packages/opencode/test/project/migrate-global.test.ts index d1ee73c42..a9a511f18 100644 --- a/packages/opencode/test/project/migrate-global.test.ts +++ b/packages/opencode/test/project/migrate-global.test.ts @@ -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) }), ) }) diff --git a/packages/opencode/test/project/project.test.ts b/packages/opencode/test/project/project.test.ts index 1d55dd50e..96e3a7ce0 100644 --- a/packages/opencode/test/project/project.test.ts +++ b/packages/opencode/test/project/project.test.ts @@ -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(fn: (svc: Project.Interface) => Effect.Effect) { } 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 { +function waitForProjectIcon(id: ProjectV2.ID, attempts = 50): Effect.Effect { 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") diff --git a/packages/opencode/test/pty/ticket.test.ts b/packages/opencode/test/pty/ticket.test.ts index 4886f250f..2a6124f5d 100644 --- a/packages/opencode/test/pty/ticket.test.ts +++ b/packages/opencode/test/pty/ticket.test.ts @@ -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) }), ) diff --git a/packages/opencode/test/server/httpapi-instance-context.test.ts b/packages/opencode/test/server/httpapi-instance-context.test.ts index 35dbf97ba..32e33e835 100644 --- a/packages/opencode/test/server/httpapi-instance-context.test.ts +++ b/packages/opencode/test/server/httpapi-instance-context.test.ts @@ -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 }) diff --git a/packages/opencode/test/server/httpapi-instance.test.ts b/packages/opencode/test/server/httpapi-instance.test.ts index 2087ad830..456f2ca1a 100644 --- a/packages/opencode/test/server/httpapi-instance.test.ts +++ b/packages/opencode/test/server/httpapi-instance.test.ts @@ -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}`, { diff --git a/packages/opencode/test/server/httpapi-workspace-routing.test.ts b/packages/opencode/test/server/httpapi-workspace-routing.test.ts index 14d29807e..5265b9604 100644 --- a/packages/opencode/test/server/httpapi-workspace-routing.test.ts +++ b/packages/opencode/test/server/httpapi-workspace-routing.test.ts @@ -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 } | undefined>(undefined) + const waited = yield* Ref.make<{ workspaceID: WorkspaceV2.ID; state: Record } | 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( diff --git a/packages/opencode/test/server/httpapi-workspace.test.ts b/packages/opencode/test/server/httpapi-workspace.test.ts index 2e10d325f..43bcb1a87 100644 --- a/packages/opencode/test/server/httpapi-workspace.test.ts +++ b/packages/opencode/test/server/httpapi-workspace.test.ts @@ -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", diff --git a/packages/opencode/test/session/schema-decoding.test.ts b/packages/opencode/test/session/schema-decoding.test.ts index 3a367fa6c..2298a275d 100644 --- a/packages/opencode/test/session/schema-decoding.test.ts +++ b/packages/opencode/test/session/schema-decoding.test.ts @@ -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(schema: S) { const decode = Schema.decodeUnknownSync(schema as any) diff --git a/packages/opencode/test/session/session-schema.test.ts b/packages/opencode/test/session/session-schema.test.ts index 906414fdb..92249c4a0 100644 --- a/packages/opencode/test/session/session-schema.test.ts +++ b/packages/opencode/test/session/session-schema.test.ts @@ -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", }, diff --git a/packages/opencode/test/storage/db.test.ts b/packages/opencode/test/storage/db.test.ts index ba7f0912a..c25a29a92 100644 --- a/packages/opencode/test/storage/db.test.ts +++ b/packages/opencode/test/storage/db.test.ts @@ -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()) + }) }) diff --git a/packages/opencode/test/storage/json-migration.test.ts b/packages/opencode/test/storage/json-migration.test.ts index 85bcded17..b5dec2fa7 100644 --- a/packages/opencode/test/storage/json-migration.test.ts +++ b/packages/opencode/test/storage/json-migration.test.ts @@ -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 () => {