mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-10 11:39:45 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 20f6635ca0 | |||
| a7f335aec0 |
@@ -2,14 +2,14 @@ import { run } from "@opencode-ai/tui"
|
|||||||
import { TuiConfig } from "@opencode-ai/tui/config"
|
import { TuiConfig } from "@opencode-ai/tui/config"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
||||||
|
|
||||||
export function runTui(transport: { url: string; headers: RequestInit["headers"] }) {
|
export function runTui(transport: { url: string; headers: RequestInit["headers"] }) {
|
||||||
const config = TuiConfig.resolve({}, { terminalSuspend: false })
|
const config = TuiConfig.resolve({}, { terminalSuspend: false })
|
||||||
return run({
|
return run({
|
||||||
...transport,
|
sdk: createOpencodeClient({ baseUrl: transport.url, headers: transport.headers, fetch: gracefulFetch }),
|
||||||
args: {},
|
args: {},
|
||||||
config,
|
config,
|
||||||
fetch: gracefulFetch,
|
|
||||||
pluginHost: {
|
pluginHost: {
|
||||||
async start() {},
|
async start() {},
|
||||||
async dispose() {},
|
async dispose() {},
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
import { UI } from "@/cli/ui"
|
import { UI } from "@/cli/ui"
|
||||||
import { errorMessage } from "@opencode-ai/tui/util/error"
|
import { errorMessage } from "@opencode-ai/tui/util/error"
|
||||||
import { validateSession } from "../tui/validate-session"
|
|
||||||
import { ServerAuth } from "@/server/auth"
|
import { ServerAuth } from "@/server/auth"
|
||||||
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
||||||
|
import { SessionID } from "@/session/schema"
|
||||||
|
import { Schema } from "effect"
|
||||||
|
|
||||||
export const AttachCommand = cmd({
|
export const AttachCommand = cmd({
|
||||||
command: "attach <url>",
|
command: "attach <url>",
|
||||||
@@ -61,15 +63,22 @@ export const AttachCommand = cmd({
|
|||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
const headers = ServerAuth.headers({ password: args.password, username: args.username })
|
const headers = ServerAuth.headers({ password: args.password, username: args.username })
|
||||||
|
const sdk = createOpencodeClient({ baseUrl: args.url, directory, headers })
|
||||||
const config = await TuiConfig.get()
|
const config = await TuiConfig.get()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await validateSession({
|
if (args.session) {
|
||||||
url: args.url,
|
const sessionID = (() => {
|
||||||
sessionID: args.session,
|
try {
|
||||||
directory,
|
return Schema.decodeUnknownSync(SessionID)(args.session)
|
||||||
headers,
|
} catch (error) {
|
||||||
})
|
throw new Error(`Invalid session ID: ${error instanceof Error ? error.message : "unknown error"}`, {
|
||||||
|
cause: error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
await sdk.session.get({ sessionID }, { throwOnError: true })
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
UI.error(errorMessage(error))
|
UI.error(errorMessage(error))
|
||||||
process.exitCode = 1
|
process.exitCode = 1
|
||||||
@@ -77,11 +86,12 @@ export const AttachCommand = cmd({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { Effect } = await import("effect")
|
const { Effect } = await import("effect")
|
||||||
const { run } = await import("../tui/layer")
|
const { Global } = await import("@opencode-ai/core/global")
|
||||||
|
const { run } = await import("@opencode-ai/tui")
|
||||||
const { createLegacyTuiPluginHost } = await import("@/plugin/tui/runtime")
|
const { createLegacyTuiPluginHost } = await import("@/plugin/tui/runtime")
|
||||||
await Effect.runPromise(
|
await Effect.runPromise(
|
||||||
run({
|
run({
|
||||||
url: args.url,
|
sdk,
|
||||||
config,
|
config,
|
||||||
pluginHost: createLegacyTuiPluginHost(),
|
pluginHost: createLegacyTuiPluginHost(),
|
||||||
args: {
|
args: {
|
||||||
@@ -90,8 +100,7 @@ export const AttachCommand = cmd({
|
|||||||
fork: args.fork,
|
fork: args.fork,
|
||||||
},
|
},
|
||||||
directory,
|
directory,
|
||||||
headers,
|
}).pipe(Effect.provide(Global.defaultLayer)),
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { cmd } from "@/cli/cmd/cmd"
|
import { cmd } from "@/cli/cmd/cmd"
|
||||||
import { Rpc } from "@/util/rpc"
|
import { Rpc } from "@/util/rpc"
|
||||||
import { type rpc } from "../tui/worker"
|
import { type rpc } from "../tui/worker"
|
||||||
|
import { ServerAuth } from "@/server/auth"
|
||||||
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
||||||
|
import { SessionID } from "@/session/schema"
|
||||||
|
import { Schema } from "effect"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
import { randomBytes } from "node:crypto"
|
||||||
import { fileURLToPath } from "url"
|
import { fileURLToPath } from "url"
|
||||||
import { UI } from "@/cli/ui"
|
import { UI } from "@/cli/ui"
|
||||||
import * as Log from "@opencode-ai/core/util/log"
|
import * as Log from "@opencode-ai/core/util/log"
|
||||||
@@ -9,8 +14,6 @@ import { errorMessage } from "@opencode-ai/tui/util/error"
|
|||||||
import { withTimeout } from "@/util/timeout"
|
import { withTimeout } from "@/util/timeout"
|
||||||
import { withNetworkOptions, resolveNetworkOptionsNoConfig } from "@/cli/network"
|
import { withNetworkOptions, resolveNetworkOptionsNoConfig } from "@/cli/network"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
import type { GlobalEvent } from "@opencode-ai/sdk/v2"
|
|
||||||
import type { EventSource } from "@opencode-ai/tui/context/sdk"
|
|
||||||
import { writeHeapSnapshot } from "v8"
|
import { writeHeapSnapshot } from "v8"
|
||||||
import {
|
import {
|
||||||
OPENCODE_PROCESS_ROLE,
|
OPENCODE_PROCESS_ROLE,
|
||||||
@@ -18,43 +21,12 @@ import {
|
|||||||
ensureRunID,
|
ensureRunID,
|
||||||
sanitizedProcessEnv,
|
sanitizedProcessEnv,
|
||||||
} from "@opencode-ai/core/util/opencode-process"
|
} from "@opencode-ai/core/util/opencode-process"
|
||||||
import { validateSession } from "../tui/validate-session"
|
|
||||||
import { win32InstallCtrlCGuard } from "@opencode-ai/tui/terminal-win32"
|
import { win32InstallCtrlCGuard } from "@opencode-ai/tui/terminal-win32"
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
const OPENCODE_WORKER_PATH: string
|
const OPENCODE_WORKER_PATH: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type RpcClient = ReturnType<typeof Rpc.client<typeof rpc>>
|
|
||||||
|
|
||||||
function createWorkerFetch(client: RpcClient): typeof fetch {
|
|
||||||
const fn = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
||||||
const request = new Request(input, init)
|
|
||||||
const body = request.body ? await request.text() : undefined
|
|
||||||
const result = await client.call("fetch", {
|
|
||||||
url: request.url,
|
|
||||||
method: request.method,
|
|
||||||
headers: Object.fromEntries(request.headers.entries()),
|
|
||||||
body,
|
|
||||||
})
|
|
||||||
return new Response(result.body, {
|
|
||||||
status: result.status,
|
|
||||||
headers: result.headers,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return fn as typeof fetch
|
|
||||||
}
|
|
||||||
|
|
||||||
function createEventSource(client: RpcClient): EventSource {
|
|
||||||
return {
|
|
||||||
subscribe: async (handler) => {
|
|
||||||
return client.on<GlobalEvent>("global.event", (e) => {
|
|
||||||
handler(e)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function target() {
|
async function target() {
|
||||||
if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH
|
if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH
|
||||||
const dist = new URL("./cli/tui/worker.js", import.meta.url)
|
const dist = new URL("./cli/tui/worker.js", import.meta.url)
|
||||||
@@ -132,6 +104,15 @@ export const TuiThreadCommand = cmd({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const cwd = Filesystem.resolve(process.cwd())
|
const cwd = Filesystem.resolve(process.cwd())
|
||||||
|
const network = resolveNetworkOptionsNoConfig(args)
|
||||||
|
const external =
|
||||||
|
process.argv.includes("--port") ||
|
||||||
|
process.argv.includes("--hostname") ||
|
||||||
|
process.argv.includes("--mdns") ||
|
||||||
|
network.mdns ||
|
||||||
|
network.port !== 0 ||
|
||||||
|
network.hostname !== "127.0.0.1"
|
||||||
|
const password = external ? undefined : randomBytes(32).toString("base64url")
|
||||||
const env = sanitizedProcessEnv({
|
const env = sanitizedProcessEnv({
|
||||||
[OPENCODE_PROCESS_ROLE]: "worker",
|
[OPENCODE_PROCESS_ROLE]: "worker",
|
||||||
[OPENCODE_RUN_ID]: ensureRunID(),
|
[OPENCODE_RUN_ID]: ensureRunID(),
|
||||||
@@ -183,34 +164,41 @@ export const TuiThreadCommand = cmd({
|
|||||||
const prompt = await input(args.prompt)
|
const prompt = await input(args.prompt)
|
||||||
const config = await TuiConfig.get()
|
const config = await TuiConfig.get()
|
||||||
|
|
||||||
const network = resolveNetworkOptionsNoConfig(args)
|
|
||||||
const external =
|
|
||||||
process.argv.includes("--port") ||
|
|
||||||
process.argv.includes("--hostname") ||
|
|
||||||
process.argv.includes("--mdns") ||
|
|
||||||
network.mdns ||
|
|
||||||
network.port !== 0 ||
|
|
||||||
network.hostname !== "127.0.0.1"
|
|
||||||
|
|
||||||
const transport = external
|
const transport = external
|
||||||
? {
|
? {
|
||||||
url: (await client.call("server", network)).url,
|
url: (await client.call("server", network)).url,
|
||||||
fetch: undefined,
|
headers: ServerAuth.headers(),
|
||||||
events: undefined,
|
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
url: "http://opencode.internal",
|
url: (
|
||||||
fetch: createWorkerFetch(client),
|
await client.call("server", {
|
||||||
events: createEventSource(client),
|
hostname: "127.0.0.1",
|
||||||
|
port: 0,
|
||||||
|
preferredPort: 0,
|
||||||
|
auth: { password },
|
||||||
|
})
|
||||||
|
).url,
|
||||||
|
headers: ServerAuth.headers({ password }),
|
||||||
}
|
}
|
||||||
|
const sdk = createOpencodeClient({
|
||||||
|
baseUrl: transport.url,
|
||||||
|
directory: cwd,
|
||||||
|
headers: transport.headers,
|
||||||
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await validateSession({
|
if (args.session) {
|
||||||
url: transport.url,
|
const sessionID = (() => {
|
||||||
sessionID: args.session,
|
try {
|
||||||
directory: cwd,
|
return Schema.decodeUnknownSync(SessionID)(args.session)
|
||||||
fetch: transport.fetch,
|
} catch (error) {
|
||||||
})
|
throw new Error(`Invalid session ID: ${error instanceof Error ? error.message : "unknown error"}`, {
|
||||||
|
cause: error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
await sdk.session.get({ sessionID }, { throwOnError: true })
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
UI.error(errorMessage(error))
|
UI.error(errorMessage(error))
|
||||||
process.exitCode = 1
|
process.exitCode = 1
|
||||||
@@ -223,11 +211,12 @@ export const TuiThreadCommand = cmd({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const { Effect } = await import("effect")
|
const { Effect } = await import("effect")
|
||||||
const { run } = await import("../tui/layer")
|
const { Global } = await import("@opencode-ai/core/global")
|
||||||
|
const { run } = await import("@opencode-ai/tui")
|
||||||
const { createLegacyTuiPluginHost } = await import("@/plugin/tui/runtime")
|
const { createLegacyTuiPluginHost } = await import("@/plugin/tui/runtime")
|
||||||
await Effect.runPromise(
|
await Effect.runPromise(
|
||||||
run({
|
run({
|
||||||
url: transport.url,
|
sdk,
|
||||||
async onSnapshot() {
|
async onSnapshot() {
|
||||||
const tui = writeHeapSnapshot("tui.heapsnapshot")
|
const tui = writeHeapSnapshot("tui.heapsnapshot")
|
||||||
const server = await client.call("snapshot", undefined)
|
const server = await client.call("snapshot", undefined)
|
||||||
@@ -236,8 +225,6 @@ export const TuiThreadCommand = cmd({
|
|||||||
config,
|
config,
|
||||||
pluginHost: createLegacyTuiPluginHost(),
|
pluginHost: createLegacyTuiPluginHost(),
|
||||||
directory: cwd,
|
directory: cwd,
|
||||||
fetch: transport.fetch,
|
|
||||||
events: transport.events,
|
|
||||||
args: {
|
args: {
|
||||||
continue: args.continue,
|
continue: args.continue,
|
||||||
sessionID: args.session,
|
sessionID: args.session,
|
||||||
@@ -246,12 +233,11 @@ export const TuiThreadCommand = cmd({
|
|||||||
prompt,
|
prompt,
|
||||||
fork: args.fork,
|
fork: args.fork,
|
||||||
},
|
},
|
||||||
}),
|
}).pipe(Effect.provide(Global.defaultLayer)),
|
||||||
)
|
)
|
||||||
} finally {
|
} finally {
|
||||||
await stop()
|
await stop()
|
||||||
}
|
}
|
||||||
process.exit(0)
|
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
unguard?.()
|
unguard?.()
|
||||||
@@ -259,6 +245,6 @@ export const TuiThreadCommand = cmd({
|
|||||||
Log.Default.warn("failed to restore terminal guard", { error: errorMessage(error) })
|
Log.Default.warn("failed to restore terminal guard", { error: errorMessage(error) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
process.exit(0)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
// scratch
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { run as runTui, type TuiInput } from "@opencode-ai/tui"
|
|
||||||
import { Global } from "@opencode-ai/core/global"
|
|
||||||
import { Effect } from "effect"
|
|
||||||
|
|
||||||
export function run(input: TuiInput) {
|
|
||||||
return runTui(input).pipe(Effect.provide(Global.defaultLayer))
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
|
||||||
import { SessionID } from "@/session/schema"
|
|
||||||
import { Schema } from "effect"
|
|
||||||
|
|
||||||
const decodeSessionID = Schema.decodeUnknownSync(SessionID)
|
|
||||||
|
|
||||||
export async function validateSession(input: {
|
|
||||||
url: string
|
|
||||||
sessionID?: string
|
|
||||||
directory?: string
|
|
||||||
fetch?: typeof fetch
|
|
||||||
headers?: RequestInit["headers"]
|
|
||||||
}) {
|
|
||||||
if (!input.sessionID) return
|
|
||||||
|
|
||||||
let sessionID: SessionID
|
|
||||||
try {
|
|
||||||
sessionID = decodeSessionID(input.sessionID)
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Invalid session ID: ${error instanceof Error ? error.message : "unknown error"}`, { cause: error })
|
|
||||||
}
|
|
||||||
|
|
||||||
await createOpencodeClient({
|
|
||||||
baseUrl: input.url,
|
|
||||||
directory: input.directory,
|
|
||||||
fetch: input.fetch,
|
|
||||||
headers: input.headers,
|
|
||||||
}).session.get({ sessionID }, { throwOnError: true })
|
|
||||||
}
|
|
||||||
@@ -5,8 +5,6 @@ import { InstanceRuntime } from "@/project/instance-runtime"
|
|||||||
import { Rpc } from "@/util/rpc"
|
import { Rpc } from "@/util/rpc"
|
||||||
import { upgrade } from "@/cli/upgrade"
|
import { upgrade } from "@/cli/upgrade"
|
||||||
import { Config } from "@/config/config"
|
import { Config } from "@/config/config"
|
||||||
import { GlobalBus } from "@/bus/global"
|
|
||||||
import { ServerAuth } from "@/server/auth"
|
|
||||||
import { writeHeapSnapshot } from "node:v8"
|
import { writeHeapSnapshot } from "node:v8"
|
||||||
import { Heap } from "@/cli/heap"
|
import { Heap } from "@/cli/heap"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
@@ -39,38 +37,22 @@ process.on("uncaughtException", (e) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Subscribe to global events and forward them via RPC
|
|
||||||
GlobalBus.on("event", (event) => {
|
|
||||||
Rpc.emit("global.event", event)
|
|
||||||
})
|
|
||||||
|
|
||||||
let server: Awaited<ReturnType<typeof Server.listen>> | undefined
|
let server: Awaited<ReturnType<typeof Server.listen>> | undefined
|
||||||
|
|
||||||
export const rpc = {
|
export const rpc = {
|
||||||
async fetch(input: { url: string; method: string; headers: Record<string, string>; body?: string }) {
|
|
||||||
const headers = { ...input.headers }
|
|
||||||
const auth = ServerAuth.header()
|
|
||||||
if (auth && !headers["authorization"] && !headers["Authorization"]) {
|
|
||||||
headers["Authorization"] = auth
|
|
||||||
}
|
|
||||||
const request = new Request(input.url, {
|
|
||||||
method: input.method,
|
|
||||||
headers,
|
|
||||||
body: input.body,
|
|
||||||
})
|
|
||||||
const response = await Server.Default().app.fetch(request)
|
|
||||||
const body = await response.text()
|
|
||||||
return {
|
|
||||||
status: response.status,
|
|
||||||
headers: Object.fromEntries(response.headers.entries()),
|
|
||||||
body,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
snapshot() {
|
snapshot() {
|
||||||
const result = writeHeapSnapshot("server.heapsnapshot")
|
const result = writeHeapSnapshot("server.heapsnapshot")
|
||||||
return result
|
return result
|
||||||
},
|
},
|
||||||
async server(input: { port: number; hostname: string; mdns?: boolean; cors?: string[] }) {
|
async server(input: {
|
||||||
|
port: number
|
||||||
|
hostname: string
|
||||||
|
mdns?: boolean
|
||||||
|
mdnsDomain?: string
|
||||||
|
cors?: string[]
|
||||||
|
preferredPort?: number
|
||||||
|
auth?: { username?: string; password?: string }
|
||||||
|
}) {
|
||||||
if (server) await server.stop(true)
|
if (server) await server.stop(true)
|
||||||
server = await Server.listen(input)
|
server = await Server.listen(input)
|
||||||
return { url: server.url.toString() }
|
return { url: server.url.toString() }
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { WebSocketTracker } from "./routes/instance/httpapi/websocket-tracker"
|
|||||||
import { PublicApi } from "./routes/instance/httpapi/public"
|
import { PublicApi } from "./routes/instance/httpapi/public"
|
||||||
import type { CorsOptions } from "./cors"
|
import type { CorsOptions } from "./cors"
|
||||||
import { lazy } from "@/util/lazy"
|
import { lazy } from "@/util/lazy"
|
||||||
|
import { ServerAuth } from "./auth"
|
||||||
|
|
||||||
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
|
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
|
||||||
globalThis.AI_SDK_LOG_WARNINGS = false
|
globalThis.AI_SDK_LOG_WARNINGS = false
|
||||||
@@ -36,6 +37,8 @@ type ListenOptions = CorsOptions & {
|
|||||||
hostname: string
|
hostname: string
|
||||||
mdns?: boolean
|
mdns?: boolean
|
||||||
mdnsDomain?: string
|
mdnsDomain?: string
|
||||||
|
preferredPort?: number
|
||||||
|
auth?: ServerAuth.Credentials
|
||||||
}
|
}
|
||||||
type ListenerState = {
|
type ListenerState = {
|
||||||
scope: Scope.Scope
|
scope: Scope.Scope
|
||||||
@@ -109,19 +112,30 @@ function listenerLayer(opts: ListenOptions, port: number) {
|
|||||||
Layer.provideMerge(WebSocketTracker.layer),
|
Layer.provideMerge(WebSocketTracker.layer),
|
||||||
Layer.provideMerge(serverLayer({ port, hostname: opts.hostname })),
|
Layer.provideMerge(serverLayer({ port, hostname: opts.hostname })),
|
||||||
// Install a fresh `ConfigProvider` per listener so `Config.string(...)`
|
// Install a fresh `ConfigProvider` per listener so `Config.string(...)`
|
||||||
// reads reflect the current `process.env`. Effect's default
|
// reads reflect the current `process.env` and listener-local credentials. Effect's default
|
||||||
// `ConfigProvider` snapshots `process.env` on first read and caches the
|
// `ConfigProvider` snapshots `process.env` on first read and caches the
|
||||||
// result on a module-singleton Reference; without overriding it here,
|
// result on a module-singleton Reference; without overriding it here,
|
||||||
// every later `Server.listen()` keeps observing that initial snapshot.
|
// every later `Server.listen()` keeps observing that initial snapshot.
|
||||||
Layer.provide(ConfigProvider.layer(ConfigProvider.fromEnv())),
|
Layer.provide(
|
||||||
|
ConfigProvider.layer(
|
||||||
|
opts.auth
|
||||||
|
? ConfigProvider.fromUnknown({
|
||||||
|
...process.env,
|
||||||
|
OPENCODE_SERVER_PASSWORD: opts.auth.password,
|
||||||
|
OPENCODE_SERVER_USERNAME: opts.auth.username ?? "opencode",
|
||||||
|
})
|
||||||
|
: ConfigProvider.fromEnv(),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function startWithPortFallback(opts: ListenOptions) {
|
function startWithPortFallback(opts: ListenOptions) {
|
||||||
if (opts.port !== 0) return startListener(opts, opts.port)
|
if (opts.port !== 0) return startListener(opts, opts.port)
|
||||||
|
if (opts.preferredPort === 0) return startListener(opts, 0)
|
||||||
// Match the legacy listener port-resolution behavior: explicit `0` prefers
|
// Match the legacy listener port-resolution behavior: explicit `0` prefers
|
||||||
// 4096 first, then any free port.
|
// 4096 first, then any free port.
|
||||||
return startListener(opts, 4096).pipe(Effect.catch(() => startListener(opts, 0)))
|
return startListener(opts, opts.preferredPort ?? 4096).pipe(Effect.catch(() => startListener(opts, 0)))
|
||||||
}
|
}
|
||||||
|
|
||||||
function startListener(opts: ListenOptions, port: number) {
|
function startListener(opts: ListenOptions, port: number) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ describe("tui attach", () => {
|
|||||||
test("loads the TUI integration lazily", async () => {
|
test("loads the TUI integration lazily", async () => {
|
||||||
const source = await Bun.file(new URL("../../../src/cli/cmd/attach.ts", import.meta.url)).text()
|
const source = await Bun.file(new URL("../../../src/cli/cmd/attach.ts", import.meta.url)).text()
|
||||||
|
|
||||||
expect(source).toContain('await import("../tui/layer")')
|
expect(source).toContain('await import("@opencode-ai/tui")')
|
||||||
expect(source).toMatch(/await import\(["']@\/plugin\/tui\/runtime["']\)/)
|
expect(source).toMatch(/await import\(["']@\/plugin\/tui\/runtime["']\)/)
|
||||||
expect(source).not.toContain('import("./app")')
|
expect(source).not.toContain('import("./app")')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ describe("tui thread", () => {
|
|||||||
test("loads the TUI integration lazily", async () => {
|
test("loads the TUI integration lazily", async () => {
|
||||||
const source = await Bun.file(new URL("../../../src/cli/cmd/tui.ts", import.meta.url)).text()
|
const source = await Bun.file(new URL("../../../src/cli/cmd/tui.ts", import.meta.url)).text()
|
||||||
|
|
||||||
expect(source).toContain('await import("../tui/layer")')
|
expect(source).toContain('await import("@opencode-ai/tui")')
|
||||||
expect(source).toMatch(/await import\(["']@\/plugin\/tui\/runtime["']\)/)
|
expect(source).toMatch(/await import\(["']@\/plugin\/tui\/runtime["']\)/)
|
||||||
expect(source).not.toContain('import("./app")')
|
expect(source).not.toContain('import("./app")')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import net from "node:net"
|
|||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
import * as Log from "@opencode-ai/core/util/log"
|
import * as Log from "@opencode-ai/core/util/log"
|
||||||
import { Server } from "../../src/server/server"
|
import { Server } from "../../src/server/server"
|
||||||
|
import { ServerAuth } from "../../src/server/auth"
|
||||||
import { PtyPaths } from "../../src/server/routes/instance/httpapi/groups/pty"
|
import { PtyPaths } from "../../src/server/routes/instance/httpapi/groups/pty"
|
||||||
import { withTimeout } from "../../src/util/timeout"
|
import { withTimeout } from "../../src/util/timeout"
|
||||||
import { resetDatabase } from "../fixture/db"
|
import { resetDatabase } from "../fixture/db"
|
||||||
@@ -313,6 +314,38 @@ describe("HttpApi Server.listen", () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("preferred port 0 uses an OS-assigned port", async () => {
|
||||||
|
const listener = await Server.listen({ hostname: "127.0.0.1", port: 0, preferredPort: 0 })
|
||||||
|
try {
|
||||||
|
expect(listener.port).toBeGreaterThan(0)
|
||||||
|
expect(listener.port).not.toBe(4096)
|
||||||
|
} finally {
|
||||||
|
await stop(listener, "timed out cleaning up OS-assigned listener")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("accepts listener-local credentials without changing process auth", async () => {
|
||||||
|
const listener = await Server.listen({
|
||||||
|
hostname: "127.0.0.1",
|
||||||
|
port: 0,
|
||||||
|
preferredPort: 0,
|
||||||
|
auth: { password: "local-secret" },
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
expect((await fetch(new URL("/doc", listener.url))).status).toBe(401)
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
await fetch(new URL("/doc", listener.url), {
|
||||||
|
headers: { authorization: ServerAuth.header({ password: "local-secret" }) ?? "" },
|
||||||
|
})
|
||||||
|
).status,
|
||||||
|
).toBe(200)
|
||||||
|
expect(process.env.OPENCODE_SERVER_PASSWORD).toBe(original.envPassword)
|
||||||
|
} finally {
|
||||||
|
await stop(listener, "timed out cleaning up listener-local auth listener")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test("port 0 falls back when 4096 is taken", async () => {
|
test("port 0 falls back when 4096 is taken", async () => {
|
||||||
const blocker = await occupyPort(4096)
|
const blocker = await occupyPort(4096)
|
||||||
if (!blocker) return
|
if (!blocker) return
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
|
|||||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
||||||
import { validateSession } from "../../src/cli/tui/validate-session"
|
|
||||||
import { InstanceBootstrap } from "../../src/project/bootstrap-service"
|
import { InstanceBootstrap } from "../../src/project/bootstrap-service"
|
||||||
import { InstanceStore } from "../../src/project/instance-store"
|
import { InstanceStore } from "../../src/project/instance-store"
|
||||||
import { MessageID, PartID, SessionID } from "../../src/session/schema"
|
import { MessageID, PartID, SessionID } from "../../src/session/schema"
|
||||||
@@ -470,14 +469,8 @@ describe("HttpApi SDK", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const sessionID = "ses_206f84f18ffeZ6hhD7pFYAiW5T"
|
const sessionID = "ses_206f84f18ffeZ6hhD7pFYAiW5T"
|
||||||
const fetch = yield* serverFetch(serverPath)
|
const fetch = yield* serverFetch(serverPath)
|
||||||
const thrown = yield* captureThrown(() =>
|
const sdk = createOpencodeClient({ baseUrl: "http://localhost", directory, fetch })
|
||||||
validateSession({
|
const thrown = yield* captureThrown(() => sdk.session.get({ sessionID }, { throwOnError: true }))
|
||||||
url: "http://localhost",
|
|
||||||
directory,
|
|
||||||
sessionID,
|
|
||||||
fetch,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
expect(errorMessage(thrown)).toBe(`Session not found: ${sessionID}`)
|
expect(errorMessage(thrown)).toBe(`Session not found: ${sessionID}`)
|
||||||
return errorMessage(thrown)
|
return errorMessage(thrown)
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { ProjectProvider, useProject } from "./context/project"
|
|||||||
import { EditorContextProvider } from "./context/editor"
|
import { EditorContextProvider } from "./context/editor"
|
||||||
import { useEvent } from "./context/event"
|
import { useEvent } from "./context/event"
|
||||||
import { SDKProvider, useSDK } from "./context/sdk"
|
import { SDKProvider, useSDK } from "./context/sdk"
|
||||||
|
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
|
||||||
import { StartupLoading } from "./component/startup-loading"
|
import { StartupLoading } from "./component/startup-loading"
|
||||||
import { SyncProvider, useSync } from "./context/sync"
|
import { SyncProvider, useSync } from "./context/sync"
|
||||||
import { SyncProviderV2 } from "./context/sync-v2"
|
import { SyncProviderV2 } from "./context/sync-v2"
|
||||||
@@ -130,13 +131,11 @@ const appBindingCommands = [
|
|||||||
] as const
|
] as const
|
||||||
|
|
||||||
export type TuiInput = {
|
export type TuiInput = {
|
||||||
url: string
|
sdk: OpencodeClient
|
||||||
args: Args
|
args: Args
|
||||||
config: TuiConfig.Resolved
|
config: TuiConfig.Resolved
|
||||||
onSnapshot?: () => Promise<string[]>
|
onSnapshot?: () => Promise<string[]>
|
||||||
directory?: string
|
directory?: string
|
||||||
fetch?: typeof fetch
|
|
||||||
headers?: RequestInit["headers"]
|
|
||||||
events?: EventSource
|
events?: EventSource
|
||||||
pluginHost: TuiPluginHost
|
pluginHost: TuiPluginHost
|
||||||
}
|
}
|
||||||
@@ -270,10 +269,8 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
|||||||
<TuiConfigProvider config={input.config}>
|
<TuiConfigProvider config={input.config}>
|
||||||
<PluginRuntimeProvider value={pluginRuntime}>
|
<PluginRuntimeProvider value={pluginRuntime}>
|
||||||
<SDKProvider
|
<SDKProvider
|
||||||
url={input.url}
|
sdk={input.sdk}
|
||||||
directory={input.directory}
|
directory={input.directory}
|
||||||
fetch={input.fetch}
|
|
||||||
headers={input.headers}
|
|
||||||
events={input.events}
|
events={input.events}
|
||||||
>
|
>
|
||||||
<ProjectProvider>
|
<ProjectProvider>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
import type { GlobalEvent, OpencodeClient } from "@opencode-ai/sdk/v2"
|
||||||
import type { GlobalEvent } from "@opencode-ai/sdk/v2"
|
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
import { createSimpleContext } from "./helper"
|
import { createSimpleContext } from "./helper"
|
||||||
import { batch, onCleanup, onMount } from "solid-js"
|
import { batch, onCleanup, onMount } from "solid-js"
|
||||||
@@ -11,26 +10,14 @@ export type EventSource = {
|
|||||||
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||||
name: "SDK",
|
name: "SDK",
|
||||||
init: (props: {
|
init: (props: {
|
||||||
url: string
|
sdk: OpencodeClient
|
||||||
directory?: string
|
directory?: string
|
||||||
fetch?: typeof fetch
|
|
||||||
headers?: RequestInit["headers"]
|
|
||||||
events?: EventSource
|
events?: EventSource
|
||||||
}) => {
|
}) => {
|
||||||
const abort = new AbortController()
|
const abort = new AbortController()
|
||||||
let sse: AbortController | undefined
|
let sse: AbortController | undefined
|
||||||
|
|
||||||
function createSDK() {
|
const sdk = props.sdk
|
||||||
return createOpencodeClient({
|
|
||||||
baseUrl: props.url,
|
|
||||||
signal: abort.signal,
|
|
||||||
directory: props.directory,
|
|
||||||
fetch: props.fetch,
|
|
||||||
headers: props.headers,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let sdk = createSDK()
|
|
||||||
|
|
||||||
const handlers = new Set<(event: GlobalEvent) => void>()
|
const handlers = new Set<(event: GlobalEvent) => void>()
|
||||||
const emitter = {
|
const emitter = {
|
||||||
@@ -144,8 +131,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
|||||||
},
|
},
|
||||||
directory: props.directory,
|
directory: props.directory,
|
||||||
event: emitter,
|
event: emitter,
|
||||||
fetch: props.fetch ?? fetch,
|
|
||||||
url: props.url,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createTestRenderer } from "@opentui/core/testing"
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { createTuiResolvedConfig } from "./fixture/tui-runtime"
|
import { createTuiResolvedConfig } from "./fixture/tui-runtime"
|
||||||
import { createEventSource, createFetch, directory } from "./fixture/tui-sdk"
|
import { createClient, createEventSource, createFetch, directory } from "./fixture/tui-sdk"
|
||||||
|
|
||||||
test("SIGHUP clears title and disposes scoped resources once", async () => {
|
test("SIGHUP clears title and disposes scoped resources once", async () => {
|
||||||
const setup = await createTestRenderer({ width: 80, height: 24, useThread: false })
|
const setup = await createTestRenderer({ width: 80, height: 24, useThread: false })
|
||||||
@@ -28,10 +28,9 @@ test("SIGHUP clears title and disposes scoped resources once", async () => {
|
|||||||
const { run } = await import("../src/app")
|
const { run } = await import("../src/app")
|
||||||
const task = Effect.runPromise(
|
const task = Effect.runPromise(
|
||||||
run({
|
run({
|
||||||
url: "http://test",
|
sdk: createClient(calls.fetch),
|
||||||
directory,
|
directory,
|
||||||
config: createTuiResolvedConfig({ plugin_enabled: {} }),
|
config: createTuiResolvedConfig({ plugin_enabled: {} }),
|
||||||
fetch: calls.fetch,
|
|
||||||
events: events.source,
|
events: events.source,
|
||||||
args: {},
|
args: {},
|
||||||
pluginHost: {
|
pluginHost: {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { KVProvider, useKV } from "../../../../src/context/kv"
|
|||||||
import { ProjectProvider, useProject } from "../../../../src/context/project"
|
import { ProjectProvider, useProject } from "../../../../src/context/project"
|
||||||
import { SDKProvider } from "../../../../src/context/sdk"
|
import { SDKProvider } from "../../../../src/context/sdk"
|
||||||
import { SyncProvider, useSync } from "../../../../src/context/sync"
|
import { SyncProvider, useSync } from "../../../../src/context/sync"
|
||||||
import { createEventSource, createFetch, type FetchHandler, directory } from "../../../fixture/tui-sdk"
|
import { createClient, createEventSource, createFetch, type FetchHandler, directory } from "../../../fixture/tui-sdk"
|
||||||
import { TestTuiContexts } from "../../../fixture/tui-environment"
|
import { TestTuiContexts } from "../../../fixture/tui-environment"
|
||||||
export { createEventSource, createFetch, directory, eventSource, json, worktree } from "../../../fixture/tui-sdk"
|
export { createEventSource, createFetch, directory, eventSource, json, worktree } from "../../../fixture/tui-sdk"
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ export async function mount(override?: FetchHandler, state?: string) {
|
|||||||
<TestTuiContexts paths={state ? { state } : undefined}>
|
<TestTuiContexts paths={state ? { state } : undefined}>
|
||||||
<ArgsProvider>
|
<ArgsProvider>
|
||||||
<KVProvider>
|
<KVProvider>
|
||||||
<SDKProvider url="http://test" directory={directory} fetch={calls.fetch} events={events.source}>
|
<SDKProvider sdk={createClient(calls.fetch)} directory={directory} events={events.source}>
|
||||||
<ProjectProvider>
|
<ProjectProvider>
|
||||||
<SyncProvider>
|
<SyncProvider>
|
||||||
<Probe />
|
<Probe />
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { onMount } from "solid-js"
|
|||||||
import { ProjectProvider } from "../../../src/context/project"
|
import { ProjectProvider } from "../../../src/context/project"
|
||||||
import { SDKProvider } from "../../../src/context/sdk"
|
import { SDKProvider } from "../../../src/context/sdk"
|
||||||
import { SyncProviderV2, useSyncV2 } from "../../../src/context/sync-v2"
|
import { SyncProviderV2, useSyncV2 } from "../../../src/context/sync-v2"
|
||||||
import { createEventSource, createFetch, directory, json } from "../../fixture/tui-sdk"
|
import { createClient, createEventSource, createFetch, directory, json } from "../../fixture/tui-sdk"
|
||||||
import { TestTuiContexts } from "../../fixture/tui-environment"
|
import { TestTuiContexts } from "../../fixture/tui-environment"
|
||||||
|
|
||||||
async function wait(fn: () => boolean, timeout = 2000) {
|
async function wait(fn: () => boolean, timeout = 2000) {
|
||||||
@@ -44,7 +44,7 @@ test("sync v2 settles pending tools when a live failure arrives", async () => {
|
|||||||
|
|
||||||
const app = await testRender(() => (
|
const app = await testRender(() => (
|
||||||
<TestTuiContexts>
|
<TestTuiContexts>
|
||||||
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
<SDKProvider sdk={createClient(calls.fetch)} directory={directory} events={events.source}>
|
||||||
<ProjectProvider>
|
<ProjectProvider>
|
||||||
<SyncProviderV2>
|
<SyncProviderV2>
|
||||||
<Probe />
|
<Probe />
|
||||||
@@ -173,7 +173,7 @@ test("sync v2 renders admitted prompts only after promotion", async () => {
|
|||||||
|
|
||||||
const app = await testRender(() => (
|
const app = await testRender(() => (
|
||||||
<TestTuiContexts>
|
<TestTuiContexts>
|
||||||
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
<SDKProvider sdk={createClient(calls.fetch)} directory={directory} events={events.source}>
|
||||||
<ProjectProvider>
|
<ProjectProvider>
|
||||||
<SyncProviderV2>
|
<SyncProviderV2>
|
||||||
<Probe />
|
<Probe />
|
||||||
@@ -237,7 +237,7 @@ test("sync v2 renders a promoted prompt when admission was missed", async () =>
|
|||||||
|
|
||||||
const app = await testRender(() => (
|
const app = await testRender(() => (
|
||||||
<TestTuiContexts>
|
<TestTuiContexts>
|
||||||
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
<SDKProvider sdk={createClient(calls.fetch)} directory={directory} events={events.source}>
|
||||||
<ProjectProvider>
|
<ProjectProvider>
|
||||||
<SyncProviderV2>
|
<SyncProviderV2>
|
||||||
<Probe />
|
<Probe />
|
||||||
@@ -285,7 +285,7 @@ test("sync v2 projects live context updates with their message ID", async () =>
|
|||||||
|
|
||||||
const app = await testRender(() => (
|
const app = await testRender(() => (
|
||||||
<TestTuiContexts>
|
<TestTuiContexts>
|
||||||
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
<SDKProvider sdk={createClient(calls.fetch)} directory={directory} events={events.source}>
|
||||||
<ProjectProvider>
|
<ProjectProvider>
|
||||||
<SyncProviderV2>
|
<SyncProviderV2>
|
||||||
<Probe />
|
<Probe />
|
||||||
@@ -340,7 +340,7 @@ test("sync v2 preserves live events while snapshot hydration is in flight", asyn
|
|||||||
|
|
||||||
const app = await testRender(() => (
|
const app = await testRender(() => (
|
||||||
<TestTuiContexts>
|
<TestTuiContexts>
|
||||||
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
<SDKProvider sdk={createClient(calls.fetch)} directory={directory} events={events.source}>
|
||||||
<ProjectProvider>
|
<ProjectProvider>
|
||||||
<SyncProviderV2>
|
<SyncProviderV2>
|
||||||
<Probe />
|
<Probe />
|
||||||
@@ -390,7 +390,7 @@ test("sync v2 replaces stale cached rows while preserving in-flight live rows",
|
|||||||
|
|
||||||
const app = await testRender(() => (
|
const app = await testRender(() => (
|
||||||
<TestTuiContexts>
|
<TestTuiContexts>
|
||||||
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
<SDKProvider sdk={createClient(calls.fetch)} directory={directory} events={events.source}>
|
||||||
<ProjectProvider>
|
<ProjectProvider>
|
||||||
<SyncProviderV2>
|
<SyncProviderV2>
|
||||||
<Probe />
|
<Probe />
|
||||||
@@ -459,7 +459,7 @@ test("sync v2 preserves snapshot order and metadata for in-flight updates", asyn
|
|||||||
|
|
||||||
const app = await testRender(() => (
|
const app = await testRender(() => (
|
||||||
<TestTuiContexts>
|
<TestTuiContexts>
|
||||||
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
<SDKProvider sdk={createClient(calls.fetch)} directory={directory} events={events.source}>
|
||||||
<ProjectProvider>
|
<ProjectProvider>
|
||||||
<SyncProviderV2>
|
<SyncProviderV2>
|
||||||
<Probe />
|
<Probe />
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { onMount } from "solid-js"
|
|||||||
import { ProjectProvider, useProject } from "../../../src/context/project"
|
import { ProjectProvider, useProject } from "../../../src/context/project"
|
||||||
import { SDKProvider } from "../../../src/context/sdk"
|
import { SDKProvider } from "../../../src/context/sdk"
|
||||||
import { useEvent } from "../../../src/context/event"
|
import { useEvent } from "../../../src/context/event"
|
||||||
import { createEventSource, createFetch, directory } from "../../fixture/tui-sdk"
|
import { createClient, createEventSource, createFetch, directory } from "../../fixture/tui-sdk"
|
||||||
import { TestTuiContexts } from "../../fixture/tui-environment"
|
import { TestTuiContexts } from "../../fixture/tui-environment"
|
||||||
|
|
||||||
const projectID = "proj_test"
|
const projectID = "proj_test"
|
||||||
@@ -61,7 +61,7 @@ async function mount() {
|
|||||||
|
|
||||||
const app = await testRender(() => (
|
const app = await testRender(() => (
|
||||||
<TestTuiContexts>
|
<TestTuiContexts>
|
||||||
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
<SDKProvider sdk={createClient(calls.fetch)} directory={directory} events={events.source}>
|
||||||
<ProjectProvider>
|
<ProjectProvider>
|
||||||
<Probe
|
<Probe
|
||||||
onReady={async (ctx) => {
|
onReady={async (ctx) => {
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import type { GlobalEvent } from "@opencode-ai/sdk/v2"
|
import { createOpencodeClient, type GlobalEvent } from "@opencode-ai/sdk/v2"
|
||||||
import type { EventSource } from "../../src/context/sdk"
|
import type { EventSource } from "../../src/context/sdk"
|
||||||
|
|
||||||
export const worktree = "/tmp/opencode"
|
export const worktree = "/tmp/opencode"
|
||||||
export const directory = `${worktree}/packages/tui`
|
export const directory = `${worktree}/packages/tui`
|
||||||
|
|
||||||
|
export function createClient(fetch: typeof globalThis.fetch) {
|
||||||
|
return createOpencodeClient({ baseUrl: "http://test", directory, fetch })
|
||||||
|
}
|
||||||
|
|
||||||
export function json(data: unknown, init?: ResponseInit) {
|
export function json(data: unknown, init?: ResponseInit) {
|
||||||
return new Response(JSON.stringify(data), {
|
return new Response(JSON.stringify(data), {
|
||||||
...init,
|
...init,
|
||||||
|
|||||||
Reference in New Issue
Block a user