Compare commits

...

1 Commits

Author SHA1 Message Date
Kit Langton 1170776f8c feat: unwrap project namespaces to flat exports + barrel 2026-04-15 23:14:54 -04:00
18 changed files with 655 additions and 657 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
import { EOL } from "os" import { EOL } from "os"
import { Project } from "../../../project/project" import { Project } from "../../../project"
import { Log } from "../../../util/log" import { Log } from "../../../util/log"
import { cmd } from "../cmd" import { cmd } from "../cmd"
+1 -1
View File
@@ -4,7 +4,7 @@ import { Session } from "../../session"
import { bootstrap } from "../bootstrap" import { bootstrap } from "../bootstrap"
import { Database } from "../../storage/db" import { Database } from "../../storage/db"
import { SessionTable } from "../../session/session.sql" import { SessionTable } from "../../session/session.sql"
import { Project } from "../../project/project" import { Project } from "../../project"
import { Instance } from "../../project/instance" import { Instance } from "../../project/instance"
import { AppRuntime } from "@/effect/app-runtime" import { AppRuntime } from "@/effect/app-runtime"
@@ -2,7 +2,7 @@ import z from "zod"
import { setTimeout as sleep } from "node:timers/promises" import { setTimeout as sleep } from "node:timers/promises"
import { fn } from "@/util/fn" import { fn } from "@/util/fn"
import { Database, asc, eq, inArray } from "@/storage/db" import { Database, asc, eq, inArray } from "@/storage/db"
import { Project } from "@/project/project" import { Project } from "@/project"
import { BusEvent } from "@/bus/bus-event" import { BusEvent } from "@/bus/bus-event"
import { GlobalBus } from "@/bus/global" import { GlobalBus } from "@/bus/global"
import { SyncEvent } from "@/sync" import { SyncEvent } from "@/sync"
+2 -2
View File
@@ -40,8 +40,8 @@ import { Command } from "@/command"
import { Truncate } from "@/tool/truncate" import { Truncate } from "@/tool/truncate"
import { ToolRegistry } from "@/tool/registry" import { ToolRegistry } from "@/tool/registry"
import { Format } from "@/format" import { Format } from "@/format"
import { Project } from "@/project/project" import { Project } from "@/project"
import { Vcs } from "@/project/vcs" import { Vcs } from "@/project"
import { Worktree } from "@/worktree" import { Worktree } from "@/worktree"
import { Pty } from "@/pty" import { Pty } from "@/pty"
import { Installation } from "@/installation" import { Installation } from "@/installation"
@@ -7,7 +7,7 @@ import { FileWatcher } from "@/file/watcher"
import { Format } from "@/format" import { Format } from "@/format"
import { ShareNext } from "@/share/share-next" import { ShareNext } from "@/share/share-next"
import { File } from "@/file" import { File } from "@/file"
import { Vcs } from "@/project/vcs" import { Vcs } from "@/project"
import { Snapshot } from "@/snapshot" import { Snapshot } from "@/snapshot"
import { Bus } from "@/bus" import { Bus } from "@/bus"
import { Observability } from "./observability" import { Observability } from "./observability"
+2 -2
View File
@@ -3,8 +3,8 @@ import { Format } from "../format"
import { LSP } from "../lsp" import { LSP } from "../lsp"
import { File } from "../file" import { File } from "../file"
import { Snapshot } from "../snapshot" import { Snapshot } from "../snapshot"
import { Project } from "./project" import { Project } from "."
import { Vcs } from "./vcs" import { Vcs } from "."
import { Bus } from "../bus" import { Bus } from "../bus"
import { Command } from "../command" import { Command } from "../command"
import { Instance } from "./instance" import { Instance } from "./instance"
+2
View File
@@ -0,0 +1,2 @@
export * as Vcs from "./vcs"
export * as Project from "./project"
+1 -1
View File
@@ -5,7 +5,7 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { iife } from "@/util/iife" import { iife } from "@/util/iife"
import { Log } from "@/util/log" import { Log } from "@/util/log"
import { LocalContext } from "../util/local-context" import { LocalContext } from "../util/local-context"
import { Project } from "./project" import { Project } from "."
import { WorkspaceContext } from "@/control-plane/workspace-context" import { WorkspaceContext } from "@/control-plane/workspace-context"
export interface InstanceContext { export interface InstanceContext {
+28 -30
View File
@@ -14,10 +14,9 @@ import { NodePath } from "@effect/platform-node"
import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
export namespace Project { const log = Log.create({ service: "project" })
const log = Log.create({ service: "project" })
export const Info = z export const Info = z
.object({ .object({
id: ProjectID.zod, id: ProjectID.zod,
worktree: z.string(), worktree: z.string(),
@@ -45,15 +44,15 @@ export namespace Project {
.meta({ .meta({
ref: "Project", ref: "Project",
}) })
export type Info = z.infer<typeof Info> export type Info = z.infer<typeof Info>
export const Event = { export const Event = {
Updated: BusEvent.define("project.updated", Info), Updated: BusEvent.define("project.updated", Info),
} }
type Row = typeof ProjectTable.$inferSelect type Row = typeof ProjectTable.$inferSelect
export function fromRow(row: Row): Info { export function fromRow(row: Row): Info {
const icon = const icon =
row.icon_url || row.icon_color row.icon_url || row.icon_color
? { url: row.icon_url ?? undefined, color: row.icon_color ?? undefined } ? { url: row.icon_url ?? undefined, color: row.icon_color ?? undefined }
@@ -72,21 +71,21 @@ export namespace Project {
sandboxes: row.sandboxes, sandboxes: row.sandboxes,
commands: row.commands ?? undefined, commands: row.commands ?? undefined,
} }
} }
export const UpdateInput = z.object({ export const UpdateInput = z.object({
projectID: ProjectID.zod, projectID: ProjectID.zod,
name: z.string().optional(), name: z.string().optional(),
icon: Info.shape.icon.optional(), icon: Info.shape.icon.optional(),
commands: Info.shape.commands.optional(), commands: Info.shape.commands.optional(),
}) })
export type UpdateInput = z.infer<typeof UpdateInput> export type UpdateInput = z.infer<typeof UpdateInput>
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Effect service // Effect service
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export interface Interface { export interface Interface {
readonly fromDirectory: (directory: string) => Effect.Effect<{ project: Info; sandbox: string }> readonly fromDirectory: (directory: string) => Effect.Effect<{ project: Info; sandbox: string }>
readonly discover: (input: Info) => Effect.Effect<void> readonly discover: (input: Info) => Effect.Effect<void>
readonly list: () => Effect.Effect<Info[]> readonly list: () => Effect.Effect<Info[]>
@@ -97,17 +96,17 @@ export namespace Project {
readonly sandboxes: (id: ProjectID) => Effect.Effect<string[]> readonly sandboxes: (id: ProjectID) => Effect.Effect<string[]>
readonly addSandbox: (id: ProjectID, directory: string) => Effect.Effect<void> readonly addSandbox: (id: ProjectID, directory: string) => Effect.Effect<void>
readonly removeSandbox: (id: ProjectID, directory: string) => Effect.Effect<void> readonly removeSandbox: (id: ProjectID, directory: string) => Effect.Effect<void>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Project") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Project") {}
type GitResult = { code: number; text: string; stderr: string } type GitResult = { code: number; text: string; stderr: string }
export const layer: Layer.Layer< export const layer: Layer.Layer<
Service, Service,
never, never,
AppFileSystem.Service | Path.Path | ChildProcessSpawner.ChildProcessSpawner AppFileSystem.Service | Path.Path | ChildProcessSpawner.ChildProcessSpawner
> = Layer.effect( > = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const fs = yield* AppFileSystem.Service const fs = yield* AppFileSystem.Service
@@ -455,15 +454,15 @@ export namespace Project {
removeSandbox, removeSandbox,
}) })
}), }),
) )
export const defaultLayer = layer.pipe( export const defaultLayer = layer.pipe(
Layer.provide(CrossSpawnSpawner.defaultLayer), Layer.provide(CrossSpawnSpawner.defaultLayer),
Layer.provide(AppFileSystem.defaultLayer), Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(NodePath.layer), Layer.provide(NodePath.layer),
) )
export function list() { export function list() {
return Database.use((db) => return Database.use((db) =>
db db
.select() .select()
@@ -471,17 +470,16 @@ export namespace Project {
.all() .all()
.map((row) => fromRow(row)), .map((row) => fromRow(row)),
) )
} }
export function get(id: ProjectID): Info | undefined { export function get(id: ProjectID): Info | undefined {
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
if (!row) return undefined if (!row) return undefined
return fromRow(row) return fromRow(row)
} }
export function setInitialized(id: ProjectID) { export function setInitialized(id: ProjectID) {
Database.use((db) => Database.use((db) =>
db.update(ProjectTable).set({ time_initialized: Date.now() }).where(eq(ProjectTable.id, id)).run(), db.update(ProjectTable).set({ time_initialized: Date.now() }).where(eq(ProjectTable.id, id)).run(),
) )
}
} }
+34 -36
View File
@@ -11,42 +11,41 @@ import { Log } from "@/util/log"
import { Instance } from "./instance" import { Instance } from "./instance"
import z from "zod" import z from "zod"
export namespace Vcs { const log = Log.create({ service: "vcs" })
const log = Log.create({ service: "vcs" })
const count = (text: string) => { const count = (text: string) => {
if (!text) return 0 if (!text) return 0
if (!text.endsWith("\n")) return text.split("\n").length if (!text.endsWith("\n")) return text.split("\n").length
return text.slice(0, -1).split("\n").length return text.slice(0, -1).split("\n").length
} }
const work = Effect.fnUntraced(function* (fs: AppFileSystem.Interface, cwd: string, file: string) { const work = Effect.fnUntraced(function* (fs: AppFileSystem.Interface, cwd: string, file: string) {
const full = path.join(cwd, file) const full = path.join(cwd, file)
if (!(yield* fs.exists(full).pipe(Effect.orDie))) return "" if (!(yield* fs.exists(full).pipe(Effect.orDie))) return ""
const buf = yield* fs.readFile(full).pipe(Effect.catch(() => Effect.succeed(new Uint8Array()))) const buf = yield* fs.readFile(full).pipe(Effect.catch(() => Effect.succeed(new Uint8Array())))
if (Buffer.from(buf).includes(0)) return "" if (Buffer.from(buf).includes(0)) return ""
return Buffer.from(buf).toString("utf8") return Buffer.from(buf).toString("utf8")
}) })
const nums = (list: Git.Stat[]) => const nums = (list: Git.Stat[]) =>
new Map(list.map((item) => [item.file, { additions: item.additions, deletions: item.deletions }] as const)) new Map(list.map((item) => [item.file, { additions: item.additions, deletions: item.deletions }] as const))
const merge = (...lists: Git.Item[][]) => { const merge = (...lists: Git.Item[][]) => {
const out = new Map<string, Git.Item>() const out = new Map<string, Git.Item>()
lists.flat().forEach((item) => { lists.flat().forEach((item) => {
if (!out.has(item.file)) out.set(item.file, item) if (!out.has(item.file)) out.set(item.file, item)
}) })
return [...out.values()] return [...out.values()]
} }
const files = Effect.fnUntraced(function* ( const files = Effect.fnUntraced(function* (
fs: AppFileSystem.Interface, fs: AppFileSystem.Interface,
git: Git.Interface, git: Git.Interface,
cwd: string, cwd: string,
ref: string | undefined, ref: string | undefined,
list: Git.Item[], list: Git.Item[],
map: Map<string, { additions: number; deletions: number }>, map: Map<string, { additions: number; deletions: number }>,
) { ) {
const base = ref ? yield* git.prefix(cwd) : "" const base = ref ? yield* git.prefix(cwd) : ""
const patch = (file: string, before: string, after: string) => const patch = (file: string, before: string, after: string) =>
formatPatch(structuredPatch(file, file, before, after, "", "", { context: Number.MAX_SAFE_INTEGER })) formatPatch(structuredPatch(file, file, before, after, "", "", { context: Number.MAX_SAFE_INTEGER }))
@@ -68,25 +67,25 @@ export namespace Vcs {
{ concurrency: 8 }, { concurrency: 8 },
) )
return next.toSorted((a, b) => a.file.localeCompare(b.file)) return next.toSorted((a, b) => a.file.localeCompare(b.file))
}) })
const track = Effect.fnUntraced(function* ( const track = Effect.fnUntraced(function* (
fs: AppFileSystem.Interface, fs: AppFileSystem.Interface,
git: Git.Interface, git: Git.Interface,
cwd: string, cwd: string,
ref: string | undefined, ref: string | undefined,
) { ) {
if (!ref) return yield* files(fs, git, cwd, ref, yield* git.status(cwd), new Map()) if (!ref) return yield* files(fs, git, cwd, ref, yield* git.status(cwd), new Map())
const [list, stats] = yield* Effect.all([git.status(cwd), git.stats(cwd, ref)], { concurrency: 2 }) const [list, stats] = yield* Effect.all([git.status(cwd), git.stats(cwd, ref)], { concurrency: 2 })
return yield* files(fs, git, cwd, ref, list, nums(stats)) return yield* files(fs, git, cwd, ref, list, nums(stats))
}) })
const compare = Effect.fnUntraced(function* ( const compare = Effect.fnUntraced(function* (
fs: AppFileSystem.Interface, fs: AppFileSystem.Interface,
git: Git.Interface, git: Git.Interface,
cwd: string, cwd: string,
ref: string, ref: string,
) { ) {
const [list, stats, extra] = yield* Effect.all([git.diff(cwd, ref), git.stats(cwd, ref), git.status(cwd)], { const [list, stats, extra] = yield* Effect.all([git.diff(cwd, ref), git.stats(cwd, ref), git.status(cwd)], {
concurrency: 3, concurrency: 3,
}) })
@@ -101,21 +100,21 @@ export namespace Vcs {
), ),
nums(stats), nums(stats),
) )
}) })
export const Mode = z.enum(["git", "branch"]) export const Mode = z.enum(["git", "branch"])
export type Mode = z.infer<typeof Mode> export type Mode = z.infer<typeof Mode>
export const Event = { export const Event = {
BranchUpdated: BusEvent.define( BranchUpdated: BusEvent.define(
"vcs.branch.updated", "vcs.branch.updated",
z.object({ z.object({
branch: z.string().optional(), branch: z.string().optional(),
}), }),
), ),
} }
export const Info = z export const Info = z
.object({ .object({
branch: z.string().optional(), branch: z.string().optional(),
default_branch: z.string().optional(), default_branch: z.string().optional(),
@@ -123,9 +122,9 @@ export namespace Vcs {
.meta({ .meta({
ref: "VcsInfo", ref: "VcsInfo",
}) })
export type Info = z.infer<typeof Info> export type Info = z.infer<typeof Info>
export const FileDiff = z export const FileDiff = z
.object({ .object({
file: z.string(), file: z.string(),
patch: z.string(), patch: z.string(),
@@ -136,23 +135,23 @@ export namespace Vcs {
.meta({ .meta({
ref: "VcsFileDiff", ref: "VcsFileDiff",
}) })
export type FileDiff = z.infer<typeof FileDiff> export type FileDiff = z.infer<typeof FileDiff>
export interface Interface { export interface Interface {
readonly init: () => Effect.Effect<void> readonly init: () => Effect.Effect<void>
readonly branch: () => Effect.Effect<string | undefined> readonly branch: () => Effect.Effect<string | undefined>
readonly defaultBranch: () => Effect.Effect<string | undefined> readonly defaultBranch: () => Effect.Effect<string | undefined>
readonly diff: (mode: Mode) => Effect.Effect<FileDiff[]> readonly diff: (mode: Mode) => Effect.Effect<FileDiff[]>
} }
interface State { interface State {
current: string | undefined current: string | undefined
root: Git.Base | undefined root: Git.Base | undefined
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Vcs") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Vcs") {}
export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Git.Service | Bus.Service> = Layer.effect( export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Git.Service | Bus.Service> = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const fs = yield* AppFileSystem.Service const fs = yield* AppFileSystem.Service
@@ -223,11 +222,10 @@ export namespace Vcs {
}), }),
}) })
}), }),
) )
export const defaultLayer = layer.pipe( export const defaultLayer = layer.pipe(
Layer.provide(Git.defaultLayer), Layer.provide(Git.defaultLayer),
Layer.provide(AppFileSystem.defaultLayer), Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(Bus.layer), Layer.provide(Bus.layer),
) )
}
@@ -5,7 +5,7 @@ import { ProviderID, ModelID } from "../../provider/schema"
import { ToolRegistry } from "../../tool/registry" import { ToolRegistry } from "../../tool/registry"
import { Worktree } from "../../worktree" import { Worktree } from "../../worktree"
import { Instance } from "../../project/instance" import { Instance } from "../../project/instance"
import { Project } from "../../project/project" import { Project } from "../../project"
import { MCP } from "../../mcp" import { MCP } from "../../mcp"
import { Session } from "../../session" import { Session } from "../../session"
import { Config } from "../../config" import { Config } from "../../config"
@@ -6,7 +6,7 @@ import z from "zod"
import { Format } from "../../format" import { Format } from "../../format"
import { TuiRoutes } from "./tui" import { TuiRoutes } from "./tui"
import { Instance } from "../../project/instance" import { Instance } from "../../project/instance"
import { Vcs } from "../../project/vcs" import { Vcs } from "../../project"
import { Agent } from "../../agent/agent" import { Agent } from "../../agent/agent"
import { Skill } from "../../skill" import { Skill } from "../../skill"
import { Global } from "../../global" import { Global } from "../../global"
@@ -2,7 +2,7 @@ import { Hono } from "hono"
import { describeRoute, validator } from "hono-openapi" import { describeRoute, validator } from "hono-openapi"
import { resolver } from "hono-openapi" import { resolver } from "hono-openapi"
import { Instance } from "../../project/instance" import { Instance } from "../../project/instance"
import { Project } from "../../project/project" import { Project } from "../../project"
import z from "zod" import z from "zod"
import { ProjectID } from "../../project/schema" import { ProjectID } from "../../project/schema"
import { errors } from "../error" import { errors } from "../error"
+1 -1
View File
@@ -3,7 +3,7 @@ import { NamedError } from "@opencode-ai/shared/util/error"
import { Global } from "../global" import { Global } from "../global"
import { Instance } from "../project/instance" import { Instance } from "../project/instance"
import { InstanceBootstrap } from "../project/bootstrap" import { InstanceBootstrap } from "../project/bootstrap"
import { Project } from "../project/project" import { Project } from "../project"
import { Database, eq } from "../storage/db" import { Database, eq } from "../storage/db"
import { ProjectTable } from "../project/project.sql" import { ProjectTable } from "../project/project.sql"
import type { ProjectID } from "../project/schema" import type { ProjectID } from "../project/schema"
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import { Project } from "../../src/project/project" import { Project } from "../../src/project"
import { Database, eq } from "../../src/storage/db" import { Database, eq } from "../../src/storage/db"
import { SessionTable } from "../../src/session/session.sql" import { SessionTable } from "../../src/session/session.sql"
import { ProjectTable } from "../../src/project/project.sql" import { ProjectTable } from "../../src/project/project.sql"
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import { Project } from "../../src/project/project" import { Project } from "../../src/project"
import { Log } from "../../src/util/log" import { Log } from "../../src/util/log"
import { $ } from "bun" import { $ } from "bun"
import path from "path" import path from "path"
+1 -1
View File
@@ -8,7 +8,7 @@ import { AppRuntime } from "../../src/effect/app-runtime"
import { FileWatcher } from "../../src/file/watcher" import { FileWatcher } from "../../src/file/watcher"
import { Instance } from "../../src/project/instance" import { Instance } from "../../src/project/instance"
import { GlobalBus } from "../../src/bus/global" import { GlobalBus } from "../../src/bus/global"
import { Vcs } from "../../src/project/vcs" import { Vcs } from "../../src/project"
// Skip in CI — native @parcel/watcher binding needed // Skip in CI — native @parcel/watcher binding needed
const describeVcs = FileWatcher.hasNativeBinding() && !process.env.CI ? describe : describe.skip const describeVcs = FileWatcher.hasNativeBinding() && !process.env.CI ? describe : describe.skip
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
import { Effect } from "effect" import { Effect } from "effect"
import z from "zod" import z from "zod"
import { Instance } from "../../src/project/instance" import { Instance } from "../../src/project/instance"
import { Project } from "../../src/project/project" import { Project } from "../../src/project"
import { Session as SessionNs } from "../../src/session" import { Session as SessionNs } from "../../src/session"
import { Log } from "../../src/util/log" import { Log } from "../../src/util/log"
import { tmpdir } from "../fixture/fixture" import { tmpdir } from "../fixture/fixture"