feat(plugin): add shell.create.before hook (#39547)

This commit is contained in:
Aiden Cline 2026-07-29 14:11:52 -05:00 committed by GitHub
commit c8dca936b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 142 additions and 49 deletions

View file

@ -12,6 +12,8 @@ import { Bus } from "./bus"
import { Location } from "./location"
import { Global } from "@opencode-ai/util/global"
import { ShellSelect } from "./shell/select"
import type { ShellCreateBefore } from "@opencode-ai/plugin/effect/shell"
import { PluginHooks } from "./plugin/hooks"
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Shell.NotFoundError", {
id: Shell.ID,
@ -45,7 +47,10 @@ type Active = {
*/
export interface Interface {
readonly name: () => Effect.Effect<string>
readonly create: (input: Shell.CreateInput) => Effect.Effect<Shell.Info>
readonly create: <E = never, R = never>(
input: Shell.CreateInput,
before?: (input: ShellCreateBefore) => Effect.Effect<void, E, R>,
) => Effect.Effect<Shell.Info, E, R>
// Currently running commands only; exited shells are retained for get/output but excluded here.
readonly list: () => Effect.Effect<Shell.Info[]>
readonly get: (id: Shell.ID) => Effect.Effect<Shell.Info, NotFoundError>
@ -68,6 +73,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect(
const config = yield* Config.Service
const global = yield* Global.Service
const appProcess = yield* AppProcess.Service
const hooks = yield* PluginHooks.Service
const context = yield* Effect.context()
const runFork = Effect.runForkWith(context)
const sessions = new Map<string, Active>()
@ -172,24 +178,34 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect(
}
})
const create = Effect.fn("Shell.create")(function* (input: Shell.CreateInput) {
const create = Effect.fn("Shell.create")(function* <E = never, R = never>(
input: Shell.CreateInput,
before?: (input: ShellCreateBefore) => Effect.Effect<void, E, R>,
) {
const invocation: ShellCreateBefore = {
command: input.command,
cwd: input.cwd ?? location.directory,
timeout: input.timeout,
shell: yield* resolve(),
env: {
...process.env,
TERM: "xterm-256color",
OPENCODE_TERMINAL: "1",
},
}
yield* hooks.trigger("shell", "create.before", invocation)
if (before) yield* before(invocation)
const id = Shell.ID.ascending()
const cwd = input.cwd ?? location.directory
const shell = yield* resolve()
const args = ShellSelect.args(shell, input.command)
const args = ShellSelect.args(invocation.shell, invocation.command)
const file = path.join(outputDir, `${id}.out`)
const env = {
...process.env,
TERM: "xterm-256color",
OPENCODE_TERMINAL: "1",
} as Record<string, string>
const info: Info = {
id,
status: "running",
command: input.command,
cwd,
shell,
command: invocation.command,
cwd: invocation.cwd,
shell: invocation.shell,
file,
metadata: input.metadata ?? {},
time: { started: Date.now() },
@ -203,9 +219,9 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect(
Effect.scoped(
Effect.gen(function* () {
const handle = yield* appProcess.spawn(
ChildProcess.make(shell, args, {
cwd,
env,
ChildProcess.make(invocation.shell, args, {
cwd: invocation.cwd,
env: invocation.env,
stdin: "ignore",
detached: process.platform !== "win32",
forceKillAfter: Duration.seconds(3),
@ -297,7 +313,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect(
)
})
yield* session.timeout(input.timeout)
yield* session.timeout(invocation.timeout)
runFork(
handle.exitCode.pipe(
@ -327,7 +343,7 @@ export function configured(options?: ShellSelect.Options) {
return makeLocationNode({
service: Service,
layer: layer(options),
deps: [Bus.node, Location.node, Config.node, Global.node, AppProcess.node],
deps: [Bus.node, Location.node, Config.node, Global.node, AppProcess.node, PluginHooks.node],
})
}