Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 41 additions & 35 deletions packages/opencode/src/project/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,46 +8,52 @@ import { BusEvent } from "@/bus/bus-event"
import { GlobalBus } from "@/bus/global"
import { which } from "../util/which"
import { ProjectID } from "./schema"
import { Effect, Layer, Path, Scope, Context, Stream } from "effect"
import { Effect, Layer, Path, Scope, Context, Stream, Types, Schema } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { NodePath } from "@effect/platform-node"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
import { zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"

const log = Log.create({ service: "project" })

export const Info = z
.object({
id: ProjectID.zod,
worktree: z.string(),
vcs: z.literal("git").optional(),
name: z.string().optional(),
icon: z
.object({
url: z.string().optional(),
override: z.string().optional(),
color: z.string().optional(),
})
.optional(),
commands: z
.object({
start: z.string().optional().describe("Startup script to run when creating a new workspace (worktree)"),
})
.optional(),
time: z.object({
created: z.number(),
updated: z.number(),
initialized: z.number().optional(),
}),
sandboxes: z.array(z.string()),
})
.meta({
ref: "Project",
})
export type Info = z.infer<typeof Info>
const ProjectVcs = Schema.Literal("git")

const ProjectIcon = Schema.Struct({
url: Schema.optional(Schema.String),
override: Schema.optional(Schema.String),
color: Schema.optional(Schema.String),
})

const ProjectCommands = Schema.Struct({
start: Schema.optional(
Schema.String.annotate({ description: "Startup script to run when creating a new workspace (worktree)" }),
),
})

const ProjectTime = Schema.Struct({
created: Schema.Number,
updated: Schema.Number,
initialized: Schema.optional(Schema.Number),
})

export const Info = Schema.Struct({
id: ProjectID,
worktree: Schema.String,
vcs: Schema.optional(ProjectVcs),
name: Schema.optional(Schema.String),
icon: Schema.optional(ProjectIcon),
commands: Schema.optional(ProjectCommands),
time: ProjectTime,
sandboxes: Schema.Array(Schema.String),
})
.annotate({ identifier: "Project" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>

export const Event = {
Updated: BusEvent.define("project.updated", Info),
Updated: BusEvent.define("project.updated", Info.zod),
}

type Row = typeof ProjectTable.$inferSelect
Expand All @@ -58,7 +64,7 @@ export function fromRow(row: Row): Info {
return {
id: row.id,
worktree: row.worktree,
vcs: row.vcs ? Info.shape.vcs.parse(row.vcs) : undefined,
vcs: row.vcs ? Schema.decodeUnknownSync(ProjectVcs)(row.vcs) : undefined,
name: row.name ?? undefined,
icon,
time: {
Expand All @@ -74,8 +80,8 @@ export function fromRow(row: Row): Info {
export const UpdateInput = z.object({
projectID: ProjectID.zod,
name: z.string().optional(),
icon: Info.shape.icon.optional(),
commands: Info.shape.commands.optional(),
icon: zod(ProjectIcon).optional(),
commands: zod(ProjectCommands).optional(),
})
export type UpdateInput = z.infer<typeof UpdateInput>

Expand Down Expand Up @@ -139,7 +145,7 @@ export const layer: Layer.Layer<
}),
)

const fakeVcs = Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS)
const fakeVcs = Schema.decodeUnknownSync(Schema.optional(ProjectVcs))(Flag.OPENCODE_FAKE_VCS)

const resolveGitPath = (cwd: string, name: string) => {
if (!name) return cwd
Expand Down
62 changes: 62 additions & 0 deletions packages/opencode/src/server/instance/httpapi/project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Instance } from "@/project/instance"
import { Project } from "@/project"
import { Effect, Layer, Schema } from "effect"
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"

const root = "/project"

export const ProjectApi = HttpApi.make("project")
.add(
HttpApiGroup.make("project")
.add(
HttpApiEndpoint.get("list", root, {
success: Schema.Array(Project.Info),
}).annotateMerge(
OpenApi.annotations({
identifier: "project.list",
summary: "List all projects",
description: "Get a list of projects that have been opened with OpenCode.",
}),
),
HttpApiEndpoint.get("current", `${root}/current`, {
success: Project.Info,
}).annotateMerge(
OpenApi.annotations({
identifier: "project.current",
summary: "Get current project",
description: "Retrieve the currently active project that OpenCode is working with.",
}),
),
)
.annotateMerge(
OpenApi.annotations({
title: "project",
description: "Experimental HttpApi project routes.",
}),
),
)
.annotateMerge(
OpenApi.annotations({
title: "opencode experimental HttpApi",
version: "0.0.1",
description: "Experimental HttpApi surface for selected instance routes.",
}),
)

export const projectHandlers = Layer.unwrap(
Effect.gen(function* () {
const svc = yield* Project.Service

const list = Effect.fn("ProjectHttpApi.list")(function* () {
return yield* svc.list()
})

const current = Effect.fn("ProjectHttpApi.current")(function* () {
return Instance.project
})

return HttpApiBuilder.group(ProjectApi, "project", (handlers) =>
handlers.handle("list", list).handle("current", current),
)
}),
).pipe(Layer.provide(Project.defaultLayer))
3 changes: 3 additions & 0 deletions packages/opencode/src/server/instance/httpapi/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { lazy } from "@/util/lazy"
import { Filesystem } from "@/util"
import { ConfigApi, configHandlers } from "./config"
import { PermissionApi, permissionHandlers } from "./permission"
import { ProjectApi, projectHandlers } from "./project"
import { ProviderApi, providerHandlers } from "./provider"
import { QuestionApi, questionHandlers } from "./question"

Expand Down Expand Up @@ -108,11 +109,13 @@ const instance = HttpRouter.middleware()(

const QuestionSecured = QuestionApi.middleware(Authorization)
const PermissionSecured = PermissionApi.middleware(Authorization)
const ProjectSecured = ProjectApi.middleware(Authorization)
const ProviderSecured = ProviderApi.middleware(Authorization)
const ConfigSecured = ConfigApi.middleware(Authorization)

export const routes = Layer.mergeAll(
HttpApiBuilder.layer(ConfigSecured).pipe(Layer.provide(configHandlers)),
HttpApiBuilder.layer(ProjectSecured).pipe(Layer.provide(projectHandlers)),
HttpApiBuilder.layer(QuestionSecured).pipe(Layer.provide(questionHandlers)),
HttpApiBuilder.layer(PermissionSecured).pipe(Layer.provide(permissionHandlers)),
HttpApiBuilder.layer(ProviderSecured).pipe(Layer.provide(providerHandlers)),
Expand Down
17 changes: 9 additions & 8 deletions packages/opencode/src/server/instance/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,7 @@ import { WorkspaceRouterMiddleware } from "./middleware"
import { AppRuntime } from "@/effect/app-runtime"

export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => {
const app = new Hono()
.use(WorkspaceRouterMiddleware(upgrade))
.route("/project", ProjectRoutes())
.route("/pty", PtyRoutes(upgrade))
.route("/config", ConfigRoutes())
.route("/experimental", ExperimentalRoutes())
.route("/session", SessionRoutes())
.route("/permission", PermissionRoutes())
const app = new Hono().use(WorkspaceRouterMiddleware(upgrade))

if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) {
const handler = ExperimentalHttpApiServer.webHandler().handler
Expand All @@ -52,9 +45,17 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => {
app.get("/provider/auth", (c) => handler(c.req.raw, context))
app.post("/provider/:providerID/oauth/authorize", (c) => handler(c.req.raw, context))
app.post("/provider/:providerID/oauth/callback", (c) => handler(c.req.raw, context))
app.get("/project", (c) => handler(c.req.raw, context))
app.get("/project/current", (c) => handler(c.req.raw, context))
}

return app
.route("/project", ProjectRoutes())
.route("/pty", PtyRoutes(upgrade))
.route("/config", ConfigRoutes())
.route("/experimental", ExperimentalRoutes())
.route("/session", SessionRoutes())
.route("/permission", PermissionRoutes())
.route("/question", QuestionRoutes())
.route("/provider", ProviderRoutes())
.route("/sync", SyncRoutes())
Expand Down
8 changes: 4 additions & 4 deletions packages/opencode/src/server/instance/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const ProjectRoutes = lazy(() =>
description: "List of projects",
content: {
"application/json": {
schema: resolver(Project.Info.array()),
schema: resolver(Project.Info.zod.array()),
},
},
},
Expand All @@ -45,7 +45,7 @@ export const ProjectRoutes = lazy(() =>
description: "Current project information",
content: {
"application/json": {
schema: resolver(Project.Info),
schema: resolver(Project.Info.zod),
},
},
},
Expand All @@ -66,7 +66,7 @@ export const ProjectRoutes = lazy(() =>
description: "Project information after git initialization",
content: {
"application/json": {
schema: resolver(Project.Info),
schema: resolver(Project.Info.zod),
},
},
},
Expand Down Expand Up @@ -99,7 +99,7 @@ export const ProjectRoutes = lazy(() =>
description: "Updated project information",
content: {
"application/json": {
schema: resolver(Project.Info),
schema: resolver(Project.Info.zod),
},
},
},
Expand Down
Loading