| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- export * as Shell from "./shell"
- import path from "path"
- import { spawn, type ChildProcess } from "child_process"
- import { readFile } from "fs/promises"
- import { statSync } from "fs"
- import { setTimeout as sleep } from "node:timers/promises"
- import { Flag } from "./flag/flag"
- import { FSUtil } from "./fs-util"
- import { which } from "./util/which"
- const SIGKILL_TIMEOUT_MS = 200
- const META: Record<string, { deny?: boolean; login?: boolean; posix?: boolean; ps?: boolean }> = {
- bash: { login: true, posix: true },
- dash: { login: true, posix: true },
- fish: { deny: true, login: true },
- ksh: { login: true, posix: true },
- nu: { deny: true },
- powershell: { ps: true },
- pwsh: { ps: true },
- sh: { login: true, posix: true },
- zsh: { login: true, posix: true },
- }
- export type Item = {
- path: string
- name: string
- acceptable: boolean
- }
- export async function killTree(proc: ChildProcess, opts?: { exited?: () => boolean }): Promise<void> {
- const pid = proc.pid
- if (!pid || opts?.exited?.()) return
- if (process.platform === "win32") {
- await new Promise<void>((resolve) => {
- const killer = spawn("taskkill", ["/pid", String(pid), "/f", "/t"], {
- stdio: "ignore",
- windowsHide: true,
- })
- killer.once("exit", () => resolve())
- killer.once("error", () => resolve())
- })
- return
- }
- try {
- process.kill(-pid, "SIGTERM")
- await sleep(SIGKILL_TIMEOUT_MS)
- if (!opts?.exited?.()) {
- process.kill(-pid, "SIGKILL")
- }
- } catch {
- proc.kill("SIGTERM")
- await sleep(SIGKILL_TIMEOUT_MS)
- if (!opts?.exited?.()) {
- proc.kill("SIGKILL")
- }
- }
- }
- function stat(file: string) {
- return statSync(file, { throwIfNoEntry: false }) ?? undefined
- }
- function full(file: string) {
- if (process.platform !== "win32") return file
- const shell = FSUtil.windowsPath(file)
- if (path.win32.dirname(shell) !== ".") {
- if (shell.startsWith("/") && name(shell) === "bash") return gitbash() || shell
- return shell
- }
- if (name(shell) === "bash") return gitbash() || which(shell) || shell
- return which(shell) || shell
- }
- function meta(file: string) {
- return META[name(file)]
- }
- function ok(file: string) {
- return meta(file)?.deny !== true
- }
- function rooted(file: string) {
- return path.isAbsolute(FSUtil.windowsPath(file))
- }
- function resolve(file: string) {
- const shell = full(file)
- if (rooted(shell)) {
- if (stat(shell)?.isFile()) return shell
- return
- }
- return which(shell) ?? undefined
- }
- function win() {
- return Array.from(
- new Set(
- [which("pwsh"), which("powershell"), gitbash(), process.env.COMSPEC || "cmd.exe"]
- .filter((item): item is string => Boolean(item))
- .map(full),
- ),
- )
- }
- async function unix() {
- const text = await readFile("/etc/shells", "utf8").catch(() => "")
- if (text) return Array.from(new Set(text.split("\n").filter((line) => line.trim() && !line.startsWith("#"))))
- return ["/bin/bash", "/bin/zsh", "/bin/sh"]
- }
- function select(file: string | undefined, opts?: { acceptable?: boolean }) {
- if (file && (!opts?.acceptable || ok(file))) {
- const shell = resolve(file)
- if (shell) return shell
- }
- if (process.platform === "win32") return win()[0]
- return fallback()
- }
- export function gitbash() {
- if (process.platform !== "win32") return
- if (Flag.OPENCODE_GIT_BASH_PATH) return Flag.OPENCODE_GIT_BASH_PATH
- const git = which("git")
- if (!git) return
- const file = path.join(git, "..", "..", "bin", "bash.exe")
- if (stat(file)?.size) return file
- }
- function fallback() {
- if (process.platform === "darwin") return "/bin/zsh"
- const bash = which("bash")
- if (bash) return bash
- return "/bin/sh"
- }
- export function name(file: string) {
- if (process.platform === "win32") return path.win32.parse(FSUtil.windowsPath(file)).name.toLowerCase()
- return path.basename(file).toLowerCase()
- }
- export function login(file: string) {
- return meta(file)?.login === true
- }
- export function posix(file: string) {
- return meta(file)?.posix === true
- }
- export function ps(file: string) {
- return meta(file)?.ps === true
- }
- function info(file: string): Item {
- const item = full(file)
- const n = name(item)
- return {
- path: item,
- name: resolve(n) ? n : item,
- acceptable: ok(item),
- }
- }
- export function args(file: string, command: string, cwd: string) {
- const n = name(file)
- if (n === "nu" || n === "fish") return ["-c", command]
- if (n === "zsh") {
- return [
- "-l",
- "-c",
- `
- [[ -f ~/.zshenv ]] && source ~/.zshenv >/dev/null 2>&1 || true
- [[ -f "\${ZDOTDIR:-$HOME}/.zshrc" ]] && source "\${ZDOTDIR:-$HOME}/.zshrc" >/dev/null 2>&1 || true
- cd -- "$1"
- eval ${JSON.stringify(command)}
- `,
- "opencode",
- cwd,
- ]
- }
- if (n === "bash") {
- return [
- "-l",
- "-c",
- `
- shopt -s expand_aliases
- [[ -f ~/.bashrc ]] && source ~/.bashrc >/dev/null 2>&1 || true
- cd -- "$1"
- eval ${JSON.stringify(command)}
- `,
- "opencode",
- cwd,
- ]
- }
- if (n === "cmd") return ["/c", command]
- if (ps(file)) return ["-NoProfile", "-Command", command]
- return ["-c", command]
- }
- let defaultPreferred: string | undefined
- let defaultAcceptable: string | undefined
- export function preferred(configShell?: string) {
- if (configShell) return select(configShell)
- defaultPreferred ??= select(process.env.SHELL)
- return defaultPreferred
- }
- preferred.reset = () => {
- defaultPreferred = undefined
- }
- export function acceptable(configShell?: string) {
- if (configShell) return select(configShell, { acceptable: true })
- defaultAcceptable ??= select(process.env.SHELL, { acceptable: true })
- return defaultAcceptable
- }
- acceptable.reset = () => {
- defaultAcceptable = undefined
- }
- export async function list(): Promise<Item[]> {
- const shells = process.platform === "win32" ? win() : await unix()
- return shells.filter((s) => resolve(s)).map(info)
- }
|