fix(core): restore shell tool fallback
This commit is contained in:
@@ -54,7 +54,7 @@ type Active = {
|
||||
export interface Interface {
|
||||
readonly name: () => Effect.Effect<string>
|
||||
readonly create: <E = never, R = never>(
|
||||
input: Shell.CreateInput,
|
||||
input: CreateInput,
|
||||
before?: (input: ShellCreateBefore) => Effect.Effect<void, E, R>,
|
||||
) => Effect.Effect<Shell.Info, E | AppProcess.AppProcessError, R>
|
||||
// Currently running commands only; exited shells are retained for get/output but excluded here.
|
||||
@@ -69,6 +69,10 @@ export interface Interface {
|
||||
readonly remove: (id: Shell.ID) => Effect.Effect<void, NotFoundError>
|
||||
}
|
||||
|
||||
type CreateInput = Shell.CreateInput & {
|
||||
readonly shell?: string
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Shell") {}
|
||||
|
||||
export const cleanup = Effect.fn("Shell.cleanup")(function* () {
|
||||
@@ -218,7 +222,7 @@ const layer = () =>
|
||||
})
|
||||
|
||||
const create = Effect.fn("Shell.create")(function* <E = never, R = never>(
|
||||
input: Shell.CreateInput,
|
||||
input: CreateInput,
|
||||
before?: (input: ShellCreateBefore) => Effect.Effect<void, E, R>,
|
||||
) {
|
||||
const sessionID = input.metadata?.sessionID
|
||||
@@ -230,7 +234,7 @@ const layer = () =>
|
||||
command: input.command,
|
||||
cwd: input.cwd ?? location.directory,
|
||||
timeout: input.timeout,
|
||||
shell: yield* shell.preferred(),
|
||||
shell: input.shell ?? (yield* shell.preferred()),
|
||||
env: {
|
||||
...(sessionEnvironment ?? process.env),
|
||||
TERM: "xterm-256color",
|
||||
|
||||
@@ -43,6 +43,7 @@ export type Draft = {
|
||||
|
||||
export interface Interface extends State.Transformable<Draft> {
|
||||
readonly preferred: () => Effect.Effect<string>
|
||||
readonly acceptable: () => Effect.Effect<string>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ShellSelect") {}
|
||||
@@ -217,6 +218,7 @@ const layer = (options?: Options) =>
|
||||
transform: state.transform,
|
||||
reload: state.reload,
|
||||
preferred: () => Effect.sync(() => preferred(state.get().shell, options, global.bin)),
|
||||
acceptable: () => Effect.sync(() => acceptable(state.get().shell, options, global.bin)),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ import { NonNegativeInt } from "../../schema.js"
|
||||
import { SessionSchema } from "../../session/schema.js"
|
||||
import { Shell } from "../../shell.js"
|
||||
import { ShellParse } from "../../shell/parse.js"
|
||||
import { ShellSelect } from "../../shell/select.js"
|
||||
import { ToolOutput } from "../../tool-output.js"
|
||||
|
||||
export const name = "shell"
|
||||
@@ -102,6 +103,7 @@ export const Plugin = {
|
||||
const environment = yield* Environment.Service
|
||||
const mutation = yield* LocationMutation.Service
|
||||
const shell = yield* Shell.Service
|
||||
const shellSelect = yield* ShellSelect.Service
|
||||
const permission = yield* Permission.Service
|
||||
const config = yield* Config.Service
|
||||
|
||||
@@ -179,6 +181,7 @@ export const Plugin = {
|
||||
cwd: input.workdir,
|
||||
timeout,
|
||||
metadata: { sessionID: context.sessionID },
|
||||
shell: yield* shellSelect.acceptable(),
|
||||
},
|
||||
(invocation) =>
|
||||
Effect.gen(function* () {
|
||||
@@ -343,7 +346,7 @@ export const Plugin = {
|
||||
Effect.gen(function* () {
|
||||
const tool = event.tools[name]
|
||||
if (!tool) return
|
||||
tool.description = description(yield* shell.name())
|
||||
tool.description = description(ShellSelect.name(yield* shellSelect.acceptable()))
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -32,6 +32,7 @@ import { Permission } from "@opencode-ai/core/permission"
|
||||
import { PluginRuntime } from "@opencode-ai/core/plugin/runtime"
|
||||
import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor"
|
||||
import { Shell } from "@opencode-ai/core/shell"
|
||||
import { ShellSelect } from "@opencode-ai/core/shell/select"
|
||||
import { Shell as ShellSchema } from "@opencode-ai/schema/shell"
|
||||
import { ShellTool } from "@opencode-ai/core/tool/plugin/shell"
|
||||
import { ToolOutput } from "@opencode-ai/core/tool-output"
|
||||
@@ -136,6 +137,7 @@ const shellPluginSupervisor = makeLocationNode({
|
||||
Permission.node,
|
||||
PluginRuntime.node,
|
||||
Shell.node,
|
||||
ShellSelect.node,
|
||||
Tool.node,
|
||||
],
|
||||
})
|
||||
@@ -167,6 +169,7 @@ const call = (input: typeof ShellTool.Input.Type, id = "call-shell") => ({
|
||||
})
|
||||
|
||||
const isWindows = process.platform === "win32"
|
||||
const terminalOnlyIt = isWindows ? productionIt.live.skip : productionIt.live
|
||||
const cwdCommand = isWindows ? "(Get-Location).Path; Start-Sleep -Milliseconds 100" : "pwd"
|
||||
const helloCommand = isWindows ? "[Console]::Out.Write('hello'); Start-Sleep -Milliseconds 100" : "printf hello"
|
||||
const stderrCommand = isWindows
|
||||
@@ -214,6 +217,38 @@ const withSession = <A, E, R>(directory: string, body: (registry: Tool.Interface
|
||||
})
|
||||
|
||||
describe("ShellTool", () => {
|
||||
terminalOnlyIt("falls back from a terminal-only shell", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => {
|
||||
reset()
|
||||
return withSession(tmp.path, (registry) =>
|
||||
Effect.gen(function* () {
|
||||
const shellSelect = yield* ShellSelect.Service
|
||||
const shell = yield* Shell.Service
|
||||
const configured = path.join(tmp.path, "fish")
|
||||
const preferred = yield* shellSelect.preferred()
|
||||
yield* Effect.promise(() => fs.symlink(preferred, configured))
|
||||
yield* shellSelect.transform((draft) => draft.configure(configured))
|
||||
const acceptable = yield* shellSelect.acceptable()
|
||||
expect(yield* shellSelect.preferred()).toBe(configured)
|
||||
expect(acceptable).not.toBe(configured)
|
||||
|
||||
const progress: Tool.Metadata[] = []
|
||||
yield* executeTool(registry, {
|
||||
...call({ command: "printf tool" }),
|
||||
progress: (update) => Effect.sync(() => progress.push(update)),
|
||||
})
|
||||
const shellID = progress[0]?.shellID
|
||||
if (typeof shellID !== "string") yield* Effect.die(new Error("Missing shell ID"))
|
||||
expect((yield* shell.get(ShellSchema.ID.make(shellID))).shell).toBe(acceptable)
|
||||
}),
|
||||
)
|
||||
},
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
),
|
||||
)
|
||||
|
||||
productionIt.live(
|
||||
"registers and returns real successful output from the active Location",
|
||||
() =>
|
||||
|
||||
Reference in New Issue
Block a user