fix(server): apply plugin pty environment (#32296)
This commit is contained in:
parent
8cc2276dba
commit
7ad68f8150
8 changed files with 135 additions and 3 deletions
24
packages/opencode/src/plugin/pty-environment.ts
Normal file
24
packages/opencode/src/plugin/pty-environment.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
export * as PluginPtyEnvironment from "./pty-environment"
|
||||
|
||||
import { PtyEnvironment } from "@opencode-ai/server/pty-environment"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
import { Plugin } from "."
|
||||
|
||||
export const layer = Layer.effect(
|
||||
PtyEnvironment.Service,
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* Plugin.Service
|
||||
const instances = yield* InstanceStore.Service
|
||||
return PtyEnvironment.Service.of({
|
||||
get: Effect.fn("PtyEnvironment.get")(function* (input) {
|
||||
return yield* instances.provide(
|
||||
{ directory: input.directory },
|
||||
plugin
|
||||
.trigger("shell.env", { cwd: input.cwd }, { env: {} as Record<string, string> })
|
||||
.pipe(Effect.map((result) => result.env)),
|
||||
)
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
|
@ -21,6 +21,7 @@ import { MCP } from "@/mcp"
|
|||
import { McpAuth } from "@/mcp/auth"
|
||||
import { Permission } from "@/permission"
|
||||
import { Plugin } from "@/plugin"
|
||||
import { PluginPtyEnvironment } from "@/plugin/pty-environment"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
import { Project } from "@/project/project"
|
||||
import { Vcs } from "@/project/vcs"
|
||||
|
|
@ -166,6 +167,7 @@ const instanceRoutes = instanceApiRoutes.pipe(
|
|||
)
|
||||
const serverRoutes = HttpApiBuilder.layer(Api).pipe(
|
||||
Layer.provide(handlers),
|
||||
Layer.provide(PluginPtyEnvironment.layer),
|
||||
Layer.provide([serverHttpApiAuthLayer, v2SchemaErrorLayer]),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ import { Context, Config as EffectConfig, Effect, Layer, Queue, Schema } from "e
|
|||
import { NodeHttpServer, NodeServices } from "@effect/platform-node"
|
||||
import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http"
|
||||
import * as Socket from "effect/unstable/socket/Socket"
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
import { mkdir } from "fs/promises"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { Pty } from "@opencode-ai/core/pty"
|
||||
import { PtyTicket } from "@opencode-ai/core/pty/ticket"
|
||||
|
|
@ -171,4 +174,78 @@ describe("v2 pty HttpApi", () => {
|
|||
expect(removed.status).toBe(204)
|
||||
}),
|
||||
)
|
||||
|
||||
;(process.platform === "win32" ? effectIt.live.skip : effectIt.live)(
|
||||
"applies plugin shell environment before forced PTY values",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped({ git: true, config: { formatter: false, lsp: false } })
|
||||
const plugin = path.join(dir, "plugin.ts")
|
||||
const cwd = path.join(dir, "child")
|
||||
yield* Effect.promise(() => mkdir(cwd))
|
||||
yield* Effect.promise(() =>
|
||||
Bun.write(
|
||||
plugin,
|
||||
[
|
||||
"export default async () => ({",
|
||||
' "shell.env": (input, output) => {',
|
||||
' output.env.SHARED = "plugin"',
|
||||
' output.env.PLUGIN = "plugin"',
|
||||
' output.env.TERM = "plugin"',
|
||||
" output.env.HOOK_CWD = input.cwd",
|
||||
" },",
|
||||
"})",
|
||||
"",
|
||||
].join("\n"),
|
||||
),
|
||||
)
|
||||
yield* Effect.promise(() =>
|
||||
Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({ plugin: [pathToFileURL(plugin).href], formatter: false, lsp: false }),
|
||||
),
|
||||
)
|
||||
|
||||
const created = yield* HttpClientRequest.post("/api/pty").pipe(
|
||||
directoryHeader(dir),
|
||||
HttpClientRequest.bodyJson({
|
||||
command: "/bin/sh",
|
||||
args: [
|
||||
"-c",
|
||||
'printf "%s|%s|%s|%s|%s\\n" "$CALLER" "$SHARED" "$PLUGIN" "$TERM" "$HOOK_CWD"; sleep 5',
|
||||
],
|
||||
cwd,
|
||||
env: { CALLER: "caller", SHARED: "caller", TERM: "caller" },
|
||||
}),
|
||||
Effect.flatMap(HttpClient.execute),
|
||||
)
|
||||
expect(created.status).toBe(200)
|
||||
const info = (yield* Schema.decodeUnknownEffect(Location.response(Pty.Info))(yield* created.json)).data
|
||||
|
||||
const socket = yield* Socket.makeWebSocket(
|
||||
`${(yield* serverUrl()).replace(/^http/, "ws")}/api/pty/${info.id}/connect?cursor=0&location[directory]=${encodeURIComponent(dir)}`,
|
||||
{ closeCodeIsError: () => false },
|
||||
)
|
||||
const messages = yield* Queue.unbounded<string>()
|
||||
yield* socket
|
||||
.runRaw((message) =>
|
||||
Queue.offer(messages, typeof message === "string" ? message : new TextDecoder().decode(message)),
|
||||
)
|
||||
.pipe(Effect.catch(() => Effect.void), Effect.forkScoped)
|
||||
const write = yield* socket.writer
|
||||
|
||||
const takeUntil = (expected: string, seen = ""): Effect.Effect<string, unknown> =>
|
||||
Effect.gen(function* () {
|
||||
const next = seen + (yield* Queue.take(messages).pipe(Effect.timeout("5 seconds")))
|
||||
if (next.includes(expected)) return next
|
||||
return yield* takeUntil(expected, next)
|
||||
})
|
||||
|
||||
expect(yield* takeUntil(`caller|plugin|plugin|xterm-256color|${cwd}`)).toContain(
|
||||
`caller|plugin|plugin|xterm-256color|${cwd}`,
|
||||
)
|
||||
yield* write(new Socket.CloseEvent(1000, "done")).pipe(Effect.catch(() => Effect.void))
|
||||
yield* HttpClientRequest.delete(`/api/pty/${info.id}`).pipe(directoryHeader(dir), HttpClient.execute)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue