Compare commits

...
Sign in to create a new pull request.

1 commit

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 636 additions and 638 deletions

View file

@ -1,5 +1,5 @@
import { EOL } from "os"
import { Project } from "../../../project/project"
import { Project } from "../../../project"
import { Log } from "../../../util/log"
import { cmd } from "../cmd"

View file

@ -4,7 +4,7 @@ import { Session } from "../../session"
import { bootstrap } from "../bootstrap"
import { Database } from "../../storage/db"
import { SessionTable } from "../../session/session.sql"
import { Project } from "../../project/project"
import { Project } from "../../project"
import { Instance } from "../../project/instance"
import { AppRuntime } from "@/effect/app-runtime"

View file

@ -2,7 +2,7 @@ import z from "zod"
import { setTimeout as sleep } from "node:timers/promises"
import { fn } from "@/util/fn"
import { Database, asc, eq, inArray } from "@/storage/db"
import { Project } from "@/project/project"
import { Project } from "@/project"
import { BusEvent } from "@/bus/bus-event"
import { GlobalBus } from "@/bus/global"
import { SyncEvent } from "@/sync"

View file

@ -40,8 +40,8 @@ import { Command } from "@/command"
import { Truncate } from "@/tool/truncate"
import { ToolRegistry } from "@/tool/registry"
import { Format } from "@/format"
import { Project } from "@/project/project"
import { Vcs } from "@/project/vcs"
import { Project } from "@/project"
import { Vcs } from "@/project"
import { Worktree } from "@/worktree"
import { Pty } from "@/pty"
import { Installation } from "@/installation"

View file

@ -7,7 +7,7 @@ import { FileWatcher } from "@/file/watcher"
import { Format } from "@/format"
import { ShareNext } from "@/share/share-next"
import { File } from "@/file"
import { Vcs } from "@/project/vcs"
import { Vcs } from "@/project"
import { Snapshot } from "@/snapshot"
import { Bus } from "@/bus"
import { Observability } from "./observability"

View file

@ -3,8 +3,8 @@ import { Format } from "../format"
import { LSP } from "../lsp"
import { File } from "../file"
import { Snapshot } from "../snapshot"
import { Project } from "./project"
import { Vcs } from "./vcs"
import { Project } from "."
import { Vcs } from "."
import { Bus } from "../bus"
import { Command } from "../command"
import { Instance } from "./instance"

View file

@ -0,0 +1,2 @@
export * as Vcs from "./vcs"
export * as Project from "./project"

View file

@ -5,7 +5,7 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { iife } from "@/util/iife"
import { Log } from "@/util/log"
import { LocalContext } from "../util/local-context"
import { Project } from "./project"
import { Project } from "."
import { WorkspaceContext } from "@/control-plane/workspace-context"
export interface InstanceContext {

View file

@ -14,10 +14,9 @@ import { NodePath } from "@effect/platform-node"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
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({
id: ProjectID.zod,
worktree: z.string(),
@ -45,15 +44,15 @@ export namespace Project {
.meta({
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),
}
}
type Row = typeof ProjectTable.$inferSelect
type Row = typeof ProjectTable.$inferSelect
export function fromRow(row: Row): Info {
export function fromRow(row: Row): Info {
const icon =
row.icon_url || row.icon_color
? { url: row.icon_url ?? undefined, color: row.icon_color ?? undefined }
@ -72,21 +71,21 @@ export namespace Project {
sandboxes: row.sandboxes,
commands: row.commands ?? undefined,
}
}
}
export const UpdateInput = z.object({
export const UpdateInput = z.object({
projectID: ProjectID.zod,
name: z.string().optional(),
icon: Info.shape.icon.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 discover: (input: Info) => Effect.Effect<void>
readonly list: () => Effect.Effect<Info[]>
@ -97,17 +96,17 @@ export namespace Project {
readonly sandboxes: (id: ProjectID) => Effect.Effect<string[]>
readonly addSandbox: (id: ProjectID, directory: string) => Effect.Effect<void>
readonly removeSandbox: (id: ProjectID, directory: string) => Effect.Effect<void>
}
}
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,
never,
AppFileSystem.Service | Path.Path | ChildProcessSpawner.ChildProcessSpawner
> = Layer.effect(
> = Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
@ -455,15 +454,15 @@ export namespace Project {
removeSandbox,
})
}),
)
)
export const defaultLayer = layer.pipe(
export const defaultLayer = layer.pipe(
Layer.provide(CrossSpawnSpawner.defaultLayer),
Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(NodePath.layer),
)
)
export function list() {
export function list() {
return Database.use((db) =>
db
.select()
@ -471,17 +470,16 @@ export namespace Project {
.all()
.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())
if (!row) return undefined
return fromRow(row)
}
}
export function setInitialized(id: ProjectID) {
export function setInitialized(id: ProjectID) {
Database.use((db) =>
db.update(ProjectTable).set({ time_initialized: Date.now() }).where(eq(ProjectTable.id, id)).run(),
)
}
}

View file

@ -11,42 +11,41 @@ import { Log } from "@/util/log"
import { Instance } from "./instance"
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.endsWith("\n")) return text.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)
if (!(yield* fs.exists(full).pipe(Effect.orDie))) return ""
const buf = yield* fs.readFile(full).pipe(Effect.catch(() => Effect.succeed(new Uint8Array())))
if (Buffer.from(buf).includes(0)) return ""
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))
const merge = (...lists: Git.Item[][]) => {
const merge = (...lists: Git.Item[][]) => {
const out = new Map<string, Git.Item>()
lists.flat().forEach((item) => {
if (!out.has(item.file)) out.set(item.file, item)
})
return [...out.values()]
}
}
const files = Effect.fnUntraced(function* (
const files = Effect.fnUntraced(function* (
fs: AppFileSystem.Interface,
git: Git.Interface,
cwd: string,
ref: string | undefined,
list: Git.Item[],
map: Map<string, { additions: number; deletions: number }>,
) {
) {
const base = ref ? yield* git.prefix(cwd) : ""
const patch = (file: string, before: string, after: string) =>
formatPatch(structuredPatch(file, file, before, after, "", "", { context: Number.MAX_SAFE_INTEGER }))
@ -68,25 +67,25 @@ export namespace Vcs {
{ concurrency: 8 },
)
return next.toSorted((a, b) => a.file.localeCompare(b.file))
})
})
const track = Effect.fnUntraced(function* (
const track = Effect.fnUntraced(function* (
fs: AppFileSystem.Interface,
git: Git.Interface,
cwd: string,
ref: string | undefined,
) {
) {
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 })
return yield* files(fs, git, cwd, ref, list, nums(stats))
})
})
const compare = Effect.fnUntraced(function* (
const compare = Effect.fnUntraced(function* (
fs: AppFileSystem.Interface,
git: Git.Interface,
cwd: string,
ref: string,
) {
) {
const [list, stats, extra] = yield* Effect.all([git.diff(cwd, ref), git.stats(cwd, ref), git.status(cwd)], {
concurrency: 3,
})
@ -101,21 +100,21 @@ export namespace Vcs {
),
nums(stats),
)
})
})
export const Mode = z.enum(["git", "branch"])
export type Mode = z.infer<typeof Mode>
export const Mode = z.enum(["git", "branch"])
export type Mode = z.infer<typeof Mode>
export const Event = {
export const Event = {
BranchUpdated: BusEvent.define(
"vcs.branch.updated",
z.object({
branch: z.string().optional(),
}),
),
}
}
export const Info = z
export const Info = z
.object({
branch: z.string().optional(),
default_branch: z.string().optional(),
@ -123,9 +122,9 @@ export namespace Vcs {
.meta({
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({
file: z.string(),
patch: z.string(),
@ -136,23 +135,23 @@ export namespace Vcs {
.meta({
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 branch: () => Effect.Effect<string | undefined>
readonly defaultBranch: () => Effect.Effect<string | undefined>
readonly diff: (mode: Mode) => Effect.Effect<FileDiff[]>
}
}
interface State {
interface State {
current: string | 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,
Effect.gen(function* () {
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(AppFileSystem.defaultLayer),
Layer.provide(Bus.layer),
)
}
)

View file

@ -5,7 +5,7 @@ import { ProviderID, ModelID } from "../../provider/schema"
import { ToolRegistry } from "../../tool/registry"
import { Worktree } from "../../worktree"
import { Instance } from "../../project/instance"
import { Project } from "../../project/project"
import { Project } from "../../project"
import { MCP } from "../../mcp"
import { Session } from "../../session"
import { Config } from "../../config"

View file

@ -6,7 +6,7 @@ import z from "zod"
import { Format } from "../../format"
import { TuiRoutes } from "./tui"
import { Instance } from "../../project/instance"
import { Vcs } from "../../project/vcs"
import { Vcs } from "../../project"
import { Agent } from "../../agent/agent"
import { Skill } from "../../skill"
import { Global } from "../../global"

View file

@ -2,7 +2,7 @@ import { Hono } from "hono"
import { describeRoute, validator } from "hono-openapi"
import { resolver } from "hono-openapi"
import { Instance } from "../../project/instance"
import { Project } from "../../project/project"
import { Project } from "../../project"
import z from "zod"
import { ProjectID } from "../../project/schema"
import { errors } from "../error"

View file

@ -3,7 +3,7 @@ import { NamedError } from "@opencode-ai/shared/util/error"
import { Global } from "../global"
import { Instance } from "../project/instance"
import { InstanceBootstrap } from "../project/bootstrap"
import { Project } from "../project/project"
import { Project } from "../project"
import { Database, eq } from "../storage/db"
import { ProjectTable } from "../project/project.sql"
import type { ProjectID } from "../project/schema"

View file

@ -1,5 +1,5 @@
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 { SessionTable } from "../../src/session/session.sql"
import { ProjectTable } from "../../src/project/project.sql"

View file

@ -1,5 +1,5 @@
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 { $ } from "bun"
import path from "path"

View file

@ -8,7 +8,7 @@ import { AppRuntime } from "../../src/effect/app-runtime"
import { FileWatcher } from "../../src/file/watcher"
import { Instance } from "../../src/project/instance"
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
const describeVcs = FileWatcher.hasNativeBinding() && !process.env.CI ? describe : describe.skip

View file

@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
import z from "zod"
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 { Log } from "../../src/util/log"
import { tmpdir } from "../fixture/fixture"