| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670 |
- import { afterEach, describe, expect } from "bun:test"
- import { ConfigProvider, Effect, Layer } from "effect"
- import type * as Scope from "effect/Scope"
- import { HttpRouter } from "effect/unstable/http"
- import { Flag } from "@opencode-ai/core/flag/flag"
- import { createOpencodeClient } from "@opencode-ai/sdk/v2"
- import { Instance } from "../../src/project/instance"
- import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
- import { Server } from "../../src/server/server"
- import { MessageID, PartID, SessionID } from "../../src/session/schema"
- import { MessageV2 } from "../../src/session/message-v2"
- import { ModelID, ProviderID } from "../../src/provider/schema"
- import type { Config } from "@/config/config"
- import { Session as SessionNs } from "@/session/session"
- import { TestLLMServer } from "../lib/llm-server"
- import path from "path"
- import { resetDatabase } from "../fixture/db"
- import { tmpdir } from "../fixture/fixture"
- import { it } from "../lib/effect"
- const original = {
- OPENCODE_EXPERIMENTAL_HTTPAPI: Flag.OPENCODE_EXPERIMENTAL_HTTPAPI,
- OPENCODE_SERVER_PASSWORD: Flag.OPENCODE_SERVER_PASSWORD,
- OPENCODE_SERVER_USERNAME: Flag.OPENCODE_SERVER_USERNAME,
- }
- type Backend = "legacy" | "httpapi"
- type Sdk = ReturnType<typeof createOpencodeClient>
- type SdkResult = { response: Response; data?: unknown; error?: unknown }
- type Captured = { status: number; data?: unknown; error?: unknown }
- type ProjectFixture = { sdk: Sdk; directory: string }
- type LlmProjectFixture = ProjectFixture & { llm: TestLLMServer["Service"] }
- function app(backend: Backend, input?: { password?: string; username?: string }) {
- Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = backend === "httpapi"
- Flag.OPENCODE_SERVER_PASSWORD = input?.password
- Flag.OPENCODE_SERVER_USERNAME = input?.username
- if (backend === "legacy") return Server.Legacy().app
- const handler = HttpRouter.toWebHandler(
- ExperimentalHttpApiServer.routes.pipe(
- Layer.provide(
- ConfigProvider.layer(
- ConfigProvider.fromUnknown({
- OPENCODE_SERVER_PASSWORD: input?.password,
- OPENCODE_SERVER_USERNAME: input?.username,
- }),
- ),
- ),
- ),
- { disableLogger: true },
- ).handler
- return {
- fetch: (request: Request) => handler(request, ExperimentalHttpApiServer.context),
- request(input: string | URL | Request, init?: RequestInit) {
- return this.fetch(input instanceof Request ? input : new Request(new URL(input, "http://localhost"), init))
- },
- }
- }
- function client(
- backend: Backend,
- directory?: string,
- input?: { password?: string; username?: string; headers?: Record<string, string> },
- ) {
- const serverApp = app(backend, input)
- const fetch = Object.assign(
- async (request: RequestInfo | URL, init?: RequestInit) =>
- await serverApp.fetch(request instanceof Request ? request : new Request(request, init)),
- { preconnect: globalThis.fetch.preconnect },
- ) satisfies typeof globalThis.fetch
- return createOpencodeClient({
- baseUrl: "http://localhost",
- directory,
- headers: input?.headers,
- fetch,
- })
- }
- function authorization(username: string, password: string) {
- return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
- }
- function providerConfig(url: string) {
- return {
- formatter: false,
- lsp: false,
- provider: {
- test: {
- name: "Test",
- id: "test",
- env: [],
- npm: "@ai-sdk/openai-compatible",
- models: {
- "test-model": {
- id: "test-model",
- name: "Test Model",
- attachment: false,
- reasoning: false,
- temperature: false,
- tool_call: true,
- release_date: "2025-01-01",
- limit: { context: 100000, output: 10000 },
- cost: { input: 0, output: 0 },
- options: {},
- },
- },
- options: {
- apiKey: "test-key",
- baseURL: url,
- },
- },
- },
- }
- }
- function call<T>(request: () => Promise<T>) {
- return Effect.promise(request)
- }
- function capture(request: () => Promise<SdkResult>) {
- return call(request).pipe(
- Effect.map((result) => ({
- status: result.response.status,
- data: result.data,
- error: result.error,
- })),
- )
- }
- function expectStatus(request: () => Promise<{ response: Response }>, status: number) {
- return call(request).pipe(
- Effect.tap((result) => Effect.sync(() => expect(result.response.status).toBe(status))),
- Effect.asVoid,
- )
- }
- function firstEvent(open: () => Promise<{ stream: AsyncIterator<unknown> }>) {
- return Effect.acquireRelease(call(open), (events) =>
- call(async () => void (await events.stream.return?.(undefined))).pipe(Effect.ignore),
- ).pipe(
- Effect.flatMap((events) => call(() => events.stream.next())),
- Effect.map((result) => result.value),
- )
- }
- function record(value: unknown) {
- return value && typeof value === "object" && !Array.isArray(value) ? Object.fromEntries(Object.entries(value)) : {}
- }
- function array(value: unknown) {
- return Array.isArray(value) ? value : []
- }
- function statuses(input: Record<string, Captured>) {
- return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, value.status]))
- }
- function firstPartText(value: unknown) {
- return record(array(record(value).parts)[0]).text
- }
- function sessionTitles(value: unknown) {
- return array(value)
- .map((item) => record(item).title)
- .filter((title): title is string => typeof title === "string")
- .sort()
- }
- function resetState() {
- return Effect.promise(async () => {
- await Instance.disposeAll()
- await resetDatabase()
- })
- }
- function httpapi<A, E>(name: string, effect: Effect.Effect<A, E, Scope.Scope>) {
- it.live(name, effect)
- }
- function parity<A, E>(name: string, scenario: (backend: Backend) => Effect.Effect<A, E, Scope.Scope>) {
- it.live(
- name,
- Effect.gen(function* () {
- const legacy = yield* scenario("legacy")
- yield* resetState()
- const httpapi = yield* scenario("httpapi")
- expect(httpapi).toEqual(legacy)
- }),
- )
- }
- function withProject<A, E, R>(
- backend: Backend,
- options: { git?: boolean; config?: Partial<Config.Info>; setup?: (dir: string) => Effect.Effect<void> },
- run: (input: ProjectFixture) => Effect.Effect<A, E, R>,
- ) {
- return Effect.acquireRelease(
- call(() => tmpdir({ git: options.git ?? true, config: { formatter: false, lsp: false, ...options.config } })),
- (tmp) => call(() => tmp[Symbol.asyncDispose]()).pipe(Effect.ignore),
- ).pipe(
- Effect.tap((tmp) => options.setup?.(tmp.path) ?? Effect.void),
- Effect.flatMap((tmp) => run({ sdk: client(backend, tmp.path), directory: tmp.path })),
- )
- }
- function withStandardProject<A, E, R>(backend: Backend, run: (input: ProjectFixture) => Effect.Effect<A, E, R>) {
- return withProject(backend, { setup: writeStandardFiles }, run)
- }
- function withFakeLlm<A, E, R>(backend: Backend, run: (input: LlmProjectFixture) => Effect.Effect<A, E, R>) {
- return Effect.gen(function* () {
- const llm = yield* TestLLMServer
- return yield* withProject(backend, { config: providerConfig(llm.url) }, (input) => run({ ...input, llm }))
- }).pipe(Effect.provide(TestLLMServer.layer))
- }
- function writeStandardFiles(dir: string) {
- return Effect.all([
- call(() => Bun.write(path.join(dir, "hello.txt"), "hello")),
- call(() => Bun.write(path.join(dir, "needle.ts"), "export const needle = 'sdk-parity'\n")),
- ]).pipe(Effect.asVoid)
- }
- function seedMessage(directory: string, sessionID: string) {
- const id = SessionID.make(sessionID)
- return call(
- async () =>
- await Instance.provide({
- directory,
- fn: () =>
- Effect.runPromise(
- SessionNs.Service.use((svc) =>
- Effect.gen(function* () {
- const message = yield* svc.updateMessage({
- id: MessageID.ascending(),
- sessionID: id,
- role: "user",
- time: { created: Date.now() },
- agent: "test",
- model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") },
- tools: {},
- } satisfies MessageV2.User)
- const part = yield* svc.updatePart({
- id: PartID.ascending(),
- sessionID: id,
- messageID: message.id,
- type: "text",
- text: "seeded message",
- })
- return { message, part }
- }),
- ).pipe(Effect.provide(SessionNs.defaultLayer)),
- ),
- }),
- )
- }
- afterEach(async () => {
- Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original.OPENCODE_EXPERIMENTAL_HTTPAPI
- Flag.OPENCODE_SERVER_PASSWORD = original.OPENCODE_SERVER_PASSWORD
- Flag.OPENCODE_SERVER_USERNAME = original.OPENCODE_SERVER_USERNAME
- await Instance.disposeAll()
- await resetDatabase()
- })
- describe("HttpApi SDK", () => {
- httpapi(
- "uses the generated SDK for global and control routes",
- Effect.gen(function* () {
- const sdk = client("httpapi")
- const health = yield* call(() => sdk.global.health())
- const log = yield* call(() => sdk.app.log({ service: "httpapi-sdk-test", level: "info", message: "hello" }))
- expect(health.response.status).toBe(200)
- expect(health.data).toMatchObject({ healthy: true })
- expect(yield* firstEvent(() => sdk.global.event({ signal: AbortSignal.timeout(1_000) }))).toMatchObject({
- payload: { type: "server.connected" },
- })
- expect(log.response.status).toBe(200)
- expect(log.data).toBe(true)
- yield* expectStatus(() => sdk.auth.set({ providerID: "test" }), 400)
- }),
- )
- httpapi(
- "uses the generated SDK for safe instance routes",
- withProject("httpapi", { git: false, setup: writeStandardFiles }, ({ sdk }) =>
- Effect.gen(function* () {
- const file = yield* call(() => sdk.file.read({ path: "hello.txt" }))
- const session = yield* call(() => sdk.session.create({ title: "sdk" }))
- const listed = yield* call(() => sdk.session.list({ roots: true, limit: 10 }))
- expect(file.response.status).toBe(200)
- expect(file.data).toMatchObject({ content: "hello" })
- expect(session.response.status).toBe(200)
- expect(session.data).toMatchObject({ title: "sdk" })
- expect(listed.response.status).toBe(200)
- expect(listed.data?.map((item) => item.id)).toContain(session.data?.id)
- yield* Effect.all([
- expectStatus(() => sdk.project.current(), 200),
- expectStatus(() => sdk.config.get(), 200),
- expectStatus(() => sdk.config.providers(), 200),
- expectStatus(() => sdk.find.files({ query: "hello", limit: 10 }), 200),
- ])
- }),
- ),
- )
- parity("matches generated SDK global and control behavior across backends", (backend) =>
- Effect.gen(function* () {
- const sdk = client(backend)
- const health = yield* capture(() => sdk.global.health())
- const log = yield* capture(() => sdk.app.log({ service: "sdk-parity", level: "info", message: "hello" }))
- const invalidAuth = yield* capture(() => sdk.auth.set({ providerID: "test" }))
- return {
- statuses: statuses({ health, log, invalidAuth }),
- health: record(health.data).healthy,
- log: log.data,
- }
- }),
- )
- parity("matches generated SDK global event stream across backends", (backend) =>
- firstEvent(() => client(backend).global.event({ signal: AbortSignal.timeout(1_000) })).pipe(
- Effect.map((event) => ({ type: record(record(event).payload).type })),
- ),
- )
- parity("matches generated SDK instance event stream across backends", (backend) =>
- withStandardProject(backend, ({ sdk }) =>
- firstEvent(() => sdk.event.subscribe(undefined, { signal: AbortSignal.timeout(1_000) })).pipe(
- Effect.map((event) => ({ type: record(record(event).payload).type })),
- ),
- ),
- )
- parity("matches generated SDK basic auth behavior across backends", (backend) =>
- withStandardProject(backend, ({ directory }) =>
- Effect.gen(function* () {
- const missing = yield* capture(() =>
- client(backend, directory, { password: "secret" }).file.read({ path: "hello.txt" }),
- )
- const bad = yield* capture(() =>
- client(backend, directory, {
- password: "secret",
- headers: { authorization: authorization("opencode", "wrong") },
- }).file.read({ path: "hello.txt" }),
- )
- const good = yield* capture(() =>
- client(backend, directory, {
- password: "secret",
- headers: { authorization: authorization("opencode", "secret") },
- }).file.read({ path: "hello.txt" }),
- )
- return {
- statuses: statuses({ missing, bad, good }),
- content: record(good.data).content,
- }
- }),
- ),
- )
- parity("matches generated SDK instance read routes across backends", (backend) =>
- withStandardProject(backend, ({ sdk, directory }) =>
- Effect.gen(function* () {
- const project = yield* capture(() => sdk.project.current())
- const projects = yield* capture(() => sdk.project.list())
- const paths = yield* capture(() => sdk.path.get())
- const config = yield* capture(() => sdk.config.get())
- const providers = yield* capture(() => sdk.config.providers())
- const file = yield* capture(() => sdk.file.read({ path: "hello.txt" }))
- const files = yield* capture(() => sdk.file.list({ path: "." }))
- const fileStatus = yield* capture(() => sdk.file.status())
- const findFiles = yield* capture(() => sdk.find.files({ query: "hello", limit: 10 }))
- const findText = yield* capture(() => sdk.find.text({ pattern: "sdk-parity" }))
- const agents = yield* capture(() => sdk.app.agents())
- const skills = yield* capture(() => sdk.app.skills())
- const tools = yield* capture(() => sdk.tool.ids())
- const vcs = yield* capture(() => sdk.vcs.get())
- const formatter = yield* capture(() => sdk.formatter.status())
- const lsp = yield* capture(() => sdk.lsp.status())
- return {
- statuses: statuses({
- project,
- projects,
- paths,
- config,
- providers,
- file,
- files,
- fileStatus,
- findFiles,
- findText,
- agents,
- skills,
- tools,
- vcs,
- formatter,
- lsp,
- }),
- project: { worktreeSelected: record(project.data).worktree === directory },
- paths: { cwdSelected: record(paths.data).cwd === directory },
- file: record(file.data).content,
- hasProject: array(projects.data).length > 0,
- foundFile: JSON.stringify(findFiles.data).includes("hello.txt"),
- foundText: JSON.stringify(findText.data ?? null).includes("sdk-parity"),
- listedFile: JSON.stringify(files.data).includes("hello.txt"),
- }
- }),
- ),
- )
- parity("matches generated SDK session lifecycle routes across backends", (backend) =>
- withStandardProject(backend, ({ sdk }) =>
- Effect.gen(function* () {
- const parent = yield* capture(() => sdk.session.create({ title: "parent" }))
- const parentID = String(record(parent.data).id)
- const child = yield* capture(() => sdk.session.create({ title: "child", parentID }))
- const childID = String(record(child.data).id)
- const get = yield* capture(() => sdk.session.get({ sessionID: parentID }))
- const update = yield* capture(() => sdk.session.update({ sessionID: parentID, title: "renamed" }))
- const roots = yield* capture(() => sdk.session.list({ roots: true, limit: 10 }))
- const all = yield* capture(() => sdk.session.list({ roots: false, limit: 10 }))
- const children = yield* capture(() => sdk.session.children({ sessionID: parentID }))
- const todo = yield* capture(() => sdk.session.todo({ sessionID: parentID }))
- const status = yield* capture(() => sdk.session.status())
- const messages = yield* capture(() => sdk.session.messages({ sessionID: parentID }))
- const missingGet = yield* capture(() => sdk.session.get({ sessionID: "ses_missing" }))
- const missingMessages = yield* capture(() => sdk.session.messages({ sessionID: "ses_missing", limit: 2 }))
- const invalidCursor = yield* capture(() =>
- sdk.session.messages({ sessionID: parentID, limit: 2, before: "bad" }),
- )
- const deleted = yield* capture(() => sdk.session.delete({ sessionID: childID }))
- const getDeleted = yield* capture(() => sdk.session.get({ sessionID: childID }))
- return {
- statuses: statuses({
- parent,
- child,
- get,
- update,
- roots,
- all,
- children,
- todo,
- status,
- messages,
- missingGet,
- missingMessages,
- invalidCursor,
- deleted,
- getDeleted,
- }),
- getTitle: record(get.data).title,
- updatedTitle: record(update.data).title,
- rootTitles: sessionTitles(roots.data),
- allTitles: sessionTitles(all.data),
- childCount: array(children.data).length,
- todoCount: array(todo.data).length,
- messageCount: array(messages.data).length,
- }
- }),
- ),
- )
- parity("matches generated SDK session message and part routes across backends", (backend) =>
- withStandardProject(backend, ({ sdk, directory }) =>
- Effect.gen(function* () {
- const session = yield* capture(() => sdk.session.create({ title: "messages" }))
- const sessionID = String(record(session.data).id)
- const seeded = yield* seedMessage(directory, sessionID)
- const list = yield* capture(() => sdk.session.messages({ sessionID }))
- const page = yield* capture(() => sdk.session.messages({ sessionID, limit: 1 }))
- const message = yield* capture(() => sdk.session.message({ sessionID, messageID: seeded.message.id }))
- const partUpdate = yield* capture(() =>
- sdk.part.update({
- sessionID,
- messageID: seeded.message.id,
- partID: seeded.part.id,
- part: { ...seeded.part, text: "updated message" } as NonNullable<
- Parameters<Sdk["part"]["update"]>[0]["part"]
- >,
- }),
- )
- const updated = yield* capture(() => sdk.session.message({ sessionID, messageID: seeded.message.id }))
- const partDelete = yield* capture(() =>
- sdk.part.delete({ sessionID, messageID: seeded.message.id, partID: seeded.part.id }),
- )
- const withoutPart = yield* capture(() => sdk.session.message({ sessionID, messageID: seeded.message.id }))
- const deleteMessage = yield* capture(() =>
- sdk.session.deleteMessage({ sessionID, messageID: seeded.message.id }),
- )
- const missingMessage = yield* capture(() => sdk.session.message({ sessionID, messageID: seeded.message.id }))
- return {
- statuses: statuses({
- session,
- list,
- page,
- message,
- partUpdate,
- updated,
- partDelete,
- withoutPart,
- deleteMessage,
- missingMessage,
- }),
- listCount: array(list.data).length,
- pageCount: array(page.data).length,
- initialText: firstPartText(message.data),
- updatedText: firstPartText(updated.data),
- partCountAfterDelete: array(record(withoutPart.data).parts).length,
- }
- }),
- ),
- )
- parity("matches generated SDK prompt no-reply routes across backends", (backend) =>
- withStandardProject(backend, ({ sdk }) =>
- Effect.gen(function* () {
- const session = yield* capture(() => sdk.session.create({ title: "prompt" }))
- const sessionID = String(record(session.data).id)
- const prompt = yield* capture(() =>
- sdk.session.prompt({
- sessionID,
- agent: "build",
- noReply: true,
- parts: [{ type: "text", text: "hello" }],
- }),
- )
- const asyncPrompt = yield* capture(() =>
- sdk.session.promptAsync({
- sessionID,
- agent: "build",
- noReply: true,
- parts: [{ type: "text", text: "async hello" }],
- }),
- )
- const messages = yield* capture(() => sdk.session.messages({ sessionID }))
- return {
- statuses: statuses({ session, prompt, asyncPrompt, messages }),
- promptRole: record(record(prompt.data).info).role,
- messageCount: array(messages.data).length,
- messageTexts: array(messages.data)
- .flatMap((item) => array(record(item).parts))
- .map((part) => record(part).text)
- .filter((text): text is string => typeof text === "string")
- .sort(),
- }
- }),
- ),
- )
- parity("matches generated SDK prompt streaming through fake LLM across backends", (backend) =>
- withFakeLlm(backend, ({ sdk, llm }) =>
- Effect.gen(function* () {
- yield* llm.text("fake world", { usage: { input: 11, output: 7 } })
- const session = yield* capture(() =>
- sdk.session.create({
- title: "llm prompt",
- permission: [{ permission: "*", pattern: "*", action: "allow" }],
- }),
- )
- const sessionID = String(record(session.data).id)
- const prompt = yield* capture(() =>
- sdk.session.prompt({
- sessionID,
- agent: "build",
- model: { providerID: "test", modelID: "test-model" },
- parts: [{ type: "text", text: "hello llm" }],
- }),
- )
- const messages = yield* capture(() => sdk.session.messages({ sessionID }))
- const inputs = yield* llm.inputs
- return {
- statuses: statuses({ session, prompt, messages }),
- calls: inputs.length,
- requestedModel: inputs[0]?.model,
- responseText: JSON.stringify(prompt.data).includes("fake world"),
- persistedText: JSON.stringify(messages.data).includes("fake world"),
- userText: JSON.stringify(messages.data).includes("hello llm"),
- }
- }),
- ),
- )
- parity("matches generated SDK TUI validation and command routes across backends", (backend) =>
- withStandardProject(backend, ({ sdk }) =>
- Effect.gen(function* () {
- const session = yield* capture(() => sdk.session.create({ title: "tui" }))
- const sessionID = String(record(session.data).id)
- const appendPrompt = yield* capture(() => sdk.tui.appendPrompt({ text: "hello" }))
- const openHelp = yield* capture(() => sdk.tui.openHelp())
- const openSessions = yield* capture(() => sdk.tui.openSessions())
- const openThemes = yield* capture(() => sdk.tui.openThemes())
- const openModels = yield* capture(() => sdk.tui.openModels())
- const submitPrompt = yield* capture(() => sdk.tui.submitPrompt())
- const clearPrompt = yield* capture(() => sdk.tui.clearPrompt())
- const executeCommand = yield* capture(() => sdk.tui.executeCommand({ command: "session_new" }))
- const showToast = yield* capture(() => sdk.tui.showToast({ title: "SDK", message: "hello", variant: "info" }))
- const selectSession = yield* capture(() => sdk.tui.selectSession({ sessionID }))
- const missingSession = yield* capture(() => sdk.tui.selectSession({ sessionID: "ses_missing" }))
- const invalidSession = yield* capture(() => sdk.tui.selectSession({ sessionID: "invalid_session_id" }))
- return {
- statuses: statuses({
- session,
- appendPrompt,
- openHelp,
- openSessions,
- openThemes,
- openModels,
- submitPrompt,
- clearPrompt,
- executeCommand,
- showToast,
- selectSession,
- missingSession,
- invalidSession,
- }),
- data: {
- appendPrompt: appendPrompt.data,
- openHelp: openHelp.data,
- openSessions: openSessions.data,
- openThemes: openThemes.data,
- openModels: openModels.data,
- submitPrompt: submitPrompt.data,
- clearPrompt: clearPrompt.data,
- executeCommand: executeCommand.data,
- showToast: showToast.data,
- selectSession: selectSession.data,
- },
- }
- }),
- ),
- )
- parity("matches generated SDK project git initialization across backends", (backend) =>
- withProject(backend, { git: false }, ({ sdk, directory }) =>
- Effect.gen(function* () {
- const before = yield* capture(() => sdk.project.current())
- const init = yield* capture(() => sdk.project.initGit())
- const after = yield* capture(() => sdk.project.current())
- return {
- statuses: statuses({ before, init, after }),
- before: {
- vcs: record(before.data).vcs ?? null,
- worktree: record(before.data).worktree,
- },
- init: {
- vcs: record(init.data).vcs,
- worktreeSelected: record(init.data).worktree === directory,
- },
- after: {
- vcs: record(after.data).vcs,
- worktreeSelected: record(after.data).worktree === directory,
- },
- }
- }),
- ),
- )
- })
|