mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
wip
This commit is contained in:
parent
7ec48dfd15
commit
891a5c53f1
3 changed files with 198 additions and 27 deletions
|
|
@ -1,5 +1,6 @@
|
|||
export namespace Flag {
|
||||
export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE")
|
||||
export const OPENCODE_GIT_BASH_PATH = process.env["OPENCODE_GIT_BASH_PATH"]
|
||||
export const OPENCODE_CONFIG = process.env["OPENCODE_CONFIG"]
|
||||
export const OPENCODE_CONFIG_DIR = process.env["OPENCODE_CONFIG_DIR"]
|
||||
export const OPENCODE_CONFIG_CONTENT = process.env["OPENCODE_CONFIG_CONTENT"]
|
||||
|
|
|
|||
195
packages/opencode/src/shell/shell.ts
Normal file
195
packages/opencode/src/shell/shell.ts
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
import { Flag } from "@/flag/flag"
|
||||
import { lazy } from "@/util/lazy"
|
||||
import { spawn as nodeSpawn } from "child_process"
|
||||
import path from "path"
|
||||
|
||||
export namespace Shell {
|
||||
const BLACKLIST = new Set(["fish", "nu"])
|
||||
const SIGKILL_TIMEOUT_MS = 200
|
||||
|
||||
function fallback() {
|
||||
if (process.platform === "win32") {
|
||||
if (Flag.OPENCODE_GIT_BASH_PATH) return Flag.OPENCODE_GIT_BASH_PATH
|
||||
const git = Bun.which("git")
|
||||
if (git) {
|
||||
// git.exe is typically at: C:\Program Files\Git\cmd\git.exe
|
||||
// bash.exe is at: C:\Program Files\Git\bin\bash.exe
|
||||
const bash = path.join(git, "..", "..", "bin", "bash.exe")
|
||||
if (Bun.file(bash).size) return bash
|
||||
}
|
||||
return process.env.COMSPEC || "cmd.exe"
|
||||
}
|
||||
if (process.platform === "darwin") return "/bin/zsh"
|
||||
const bash = Bun.which("bash")
|
||||
if (bash) return bash
|
||||
return "/bin/sh"
|
||||
}
|
||||
|
||||
export const preferred = lazy(() => {
|
||||
const s = process.env.SHELL
|
||||
if (s) return s
|
||||
return fallback()
|
||||
})
|
||||
|
||||
export const acceptable = lazy(() => {
|
||||
const s = process.env.SHELL
|
||||
if (s && !BLACKLIST.has(path.basename(s))) return s
|
||||
return fallback()
|
||||
})
|
||||
|
||||
export interface SpawnOptions {
|
||||
command: string
|
||||
cwd: string
|
||||
shell?: string
|
||||
source?: boolean
|
||||
env?: Record<string, string>
|
||||
timeout?: number
|
||||
abort?: AbortSignal
|
||||
onData?: (chunk: Buffer) => void
|
||||
}
|
||||
|
||||
export interface SpawnResult {
|
||||
output: string
|
||||
exitCode: number | null
|
||||
timedOut: boolean
|
||||
aborted: boolean
|
||||
}
|
||||
|
||||
function args(shell: string, command: string, source?: boolean): string[] {
|
||||
const name = path.basename(shell).toLowerCase()
|
||||
|
||||
if (name === "nu") return ["-c", command]
|
||||
if (name === "fish") return ["-c", command]
|
||||
|
||||
if (name === "zsh") {
|
||||
if (!source) return ["-c", command]
|
||||
return [
|
||||
"-c",
|
||||
"-l",
|
||||
`
|
||||
[[ -f ~/.zshenv ]] && source ~/.zshenv >/dev/null 2>&1 || true
|
||||
[[ -f "\${ZDOTDIR:-$HOME}/.zshrc" ]] && source "\${ZDOTDIR:-$HOME}/.zshrc" >/dev/null 2>&1 || true
|
||||
${command}
|
||||
`.trim(),
|
||||
]
|
||||
}
|
||||
|
||||
if (name === "bash" || name === "bash.exe") {
|
||||
if (!source) return ["-c", command]
|
||||
return [
|
||||
"-c",
|
||||
"-l",
|
||||
`
|
||||
[[ -f ~/.bashrc ]] && source ~/.bashrc >/dev/null 2>&1 || true
|
||||
${command}
|
||||
`.trim(),
|
||||
]
|
||||
}
|
||||
|
||||
if (name === "cmd.exe") return ["/c", command]
|
||||
if (name === "powershell.exe") return ["-NoProfile", "-Command", command]
|
||||
|
||||
// fallback
|
||||
if (!source) return ["-c", command]
|
||||
return ["-c", "-l", command]
|
||||
}
|
||||
|
||||
export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
||||
const shell = options.shell ?? acceptable()
|
||||
const proc = nodeSpawn(shell, args(shell, options.command, options.source), {
|
||||
cwd: options.cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
...options.env,
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: process.platform !== "win32",
|
||||
})
|
||||
|
||||
let output = ""
|
||||
let timedOut = false
|
||||
let aborted = false
|
||||
let exited = false
|
||||
|
||||
const append = (chunk: Buffer) => {
|
||||
output += chunk.toString()
|
||||
options.onData?.(chunk)
|
||||
}
|
||||
|
||||
proc.stdout?.on("data", append)
|
||||
proc.stderr?.on("data", append)
|
||||
|
||||
const killTree = async () => {
|
||||
const pid = proc.pid
|
||||
if (!pid || exited) return
|
||||
|
||||
if (process.platform === "win32") {
|
||||
await new Promise<void>((resolve) => {
|
||||
const killer = nodeSpawn("taskkill", ["/pid", String(pid), "/f", "/t"], { stdio: "ignore" })
|
||||
killer.once("exit", resolve)
|
||||
killer.once("error", resolve)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
process.kill(-pid, "SIGTERM")
|
||||
await Bun.sleep(SIGKILL_TIMEOUT_MS)
|
||||
if (!exited) {
|
||||
process.kill(-pid, "SIGKILL")
|
||||
}
|
||||
} catch {
|
||||
proc.kill("SIGTERM")
|
||||
await Bun.sleep(SIGKILL_TIMEOUT_MS)
|
||||
if (!exited) {
|
||||
proc.kill("SIGKILL")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (options.abort?.aborted) {
|
||||
aborted = true
|
||||
await killTree()
|
||||
}
|
||||
|
||||
const abortHandler = () => {
|
||||
aborted = true
|
||||
void killTree()
|
||||
}
|
||||
|
||||
options.abort?.addEventListener("abort", abortHandler, { once: true })
|
||||
|
||||
const timeoutTimer = options.timeout
|
||||
? setTimeout(() => {
|
||||
timedOut = true
|
||||
void killTree()
|
||||
}, options.timeout)
|
||||
: undefined
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const cleanup = () => {
|
||||
if (timeoutTimer) clearTimeout(timeoutTimer)
|
||||
options.abort?.removeEventListener("abort", abortHandler)
|
||||
}
|
||||
|
||||
proc.once("exit", () => {
|
||||
exited = true
|
||||
cleanup()
|
||||
resolve()
|
||||
})
|
||||
|
||||
proc.once("error", (error) => {
|
||||
exited = true
|
||||
cleanup()
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
output,
|
||||
exitCode: proc.exitCode,
|
||||
timedOut,
|
||||
aborted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ import { Permission } from "@/permission"
|
|||
import { fileURLToPath } from "url"
|
||||
import { Flag } from "@/flag/flag.ts"
|
||||
import path from "path"
|
||||
import { iife } from "@/util/iife"
|
||||
import { Shell } from "@/shell/shell"
|
||||
|
||||
const MAX_OUTPUT_LENGTH = Flag.OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH || 30_000
|
||||
const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000
|
||||
|
|
@ -53,32 +53,7 @@ const parser = lazy(async () => {
|
|||
// TODO: we may wanna rename this tool so it works better on other shells
|
||||
|
||||
export const BashTool = Tool.define("bash", async () => {
|
||||
const shell = iife(() => {
|
||||
const s = process.env.SHELL
|
||||
if (s) {
|
||||
const basename = path.basename(s)
|
||||
if (!new Set(["fish", "nu"]).has(basename)) {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
return "/bin/zsh"
|
||||
}
|
||||
|
||||
if (process.platform === "win32") {
|
||||
// Let Bun / Node pick COMSPEC (usually cmd.exe)
|
||||
// or explicitly:
|
||||
return process.env.COMSPEC || true
|
||||
}
|
||||
|
||||
const bash = Bun.which("bash")
|
||||
if (bash) {
|
||||
return bash
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
const shell = Shell.acceptable()
|
||||
log.info("bash tool using shell", { shell })
|
||||
|
||||
return {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue