Skip to content

Commit b234370

Browse files
authored
feat(windows): add first-class pwsh/powershell support (#16069)
1 parent 5d2dc88 commit b234370

File tree

18 files changed

+1487
-367
lines changed

18 files changed

+1487
-367
lines changed

bun.lock

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
"protobufjs",
105105
"tree-sitter",
106106
"tree-sitter-bash",
107+
"tree-sitter-powershell",
107108
"web-tree-sitter",
108109
"electron"
109110
],

packages/opencode/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@
142142
"solid-js": "catalog:",
143143
"strip-ansi": "7.1.2",
144144
"tree-sitter-bash": "0.25.0",
145+
"tree-sitter-powershell": "0.25.10",
145146
"turndown": "7.2.0",
146147
"ulid": "catalog:",
147148
"vscode-jsonrpc": "8.2.1",

packages/opencode/src/pty/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ export namespace Pty {
176176
const id = PtyID.ascending()
177177
const command = input.command || Shell.preferred()
178178
const args = input.args || []
179-
if (command.endsWith("sh")) {
179+
if (Shell.login(command)) {
180180
args.push("-l")
181181
}
182182

packages/opencode/src/session/prompt.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1648,9 +1648,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
16481648
}
16491649
await Session.updatePart(part)
16501650
const shell = Shell.preferred()
1651-
const shellName = (
1652-
process.platform === "win32" ? path.win32.basename(shell, ".exe") : path.basename(shell)
1653-
).toLowerCase()
1651+
const shellName = Shell.name(shell)
16541652

16551653
const invocations: Record<string, { args: string[] }> = {
16561654
nu: {

packages/opencode/src/shell/shell.ts

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import { setTimeout as sleep } from "node:timers/promises"
99
const SIGKILL_TIMEOUT_MS = 200
1010

1111
export namespace Shell {
12+
const BLACKLIST = new Set(["fish", "nu"])
13+
const LOGIN = new Set(["bash", "dash", "fish", "ksh", "sh", "zsh"])
14+
const POSIX = new Set(["bash", "dash", "ksh", "sh", "zsh"])
15+
1216
export async function killTree(proc: ChildProcess, opts?: { exited?: () => boolean }): Promise<void> {
1317
const pid = proc.pid
1418
if (!pid || opts?.exited?.()) return
@@ -39,18 +43,46 @@ export namespace Shell {
3943
}
4044
}
4145
}
42-
const BLACKLIST = new Set(["fish", "nu"])
46+
47+
function full(file: string) {
48+
if (process.platform !== "win32") return file
49+
const shell = Filesystem.windowsPath(file)
50+
if (path.win32.dirname(shell) !== ".") {
51+
if (shell.startsWith("/") && name(shell) === "bash") return gitbash() || shell
52+
return shell
53+
}
54+
return Bun.which(shell) || shell
55+
}
56+
57+
function pick() {
58+
const pwsh = Bun.which("pwsh")
59+
if (pwsh) return pwsh
60+
const powershell = Bun.which("powershell")
61+
if (powershell) return powershell
62+
}
63+
64+
function select(file: string | undefined, opts?: { acceptable?: boolean }) {
65+
if (file && (!opts?.acceptable || !BLACKLIST.has(name(file)))) return full(file)
66+
if (process.platform === "win32") {
67+
const shell = pick()
68+
if (shell) return shell
69+
}
70+
return fallback()
71+
}
72+
73+
export function gitbash() {
74+
if (process.platform !== "win32") return
75+
if (Flag.OPENCODE_GIT_BASH_PATH) return Flag.OPENCODE_GIT_BASH_PATH
76+
const git = which("git")
77+
if (!git) return
78+
const file = path.join(git, "..", "..", "bin", "bash.exe")
79+
if (Filesystem.stat(file)?.size) return file
80+
}
4381

4482
function fallback() {
4583
if (process.platform === "win32") {
46-
if (Flag.OPENCODE_GIT_BASH_PATH) return Flag.OPENCODE_GIT_BASH_PATH
47-
const git = which("git")
48-
if (git) {
49-
// git.exe is typically at: C:\Program Files\Git\cmd\git.exe
50-
// bash.exe is at: C:\Program Files\Git\bin\bash.exe
51-
const bash = path.join(git, "..", "..", "bin", "bash.exe")
52-
if (Filesystem.stat(bash)?.size) return bash
53-
}
84+
const file = gitbash()
85+
if (file) return file
5486
return process.env.COMSPEC || "cmd.exe"
5587
}
5688
if (process.platform === "darwin") return "/bin/zsh"
@@ -59,15 +91,20 @@ export namespace Shell {
5991
return "/bin/sh"
6092
}
6193

62-
export const preferred = lazy(() => {
63-
const s = process.env.SHELL
64-
if (s) return s
65-
return fallback()
66-
})
94+
export function name(file: string) {
95+
if (process.platform === "win32") return path.win32.parse(Filesystem.windowsPath(file)).name.toLowerCase()
96+
return path.basename(file).toLowerCase()
97+
}
6798

68-
export const acceptable = lazy(() => {
69-
const s = process.env.SHELL
70-
if (s && !BLACKLIST.has(process.platform === "win32" ? path.win32.basename(s) : path.basename(s))) return s
71-
return fallback()
72-
})
99+
export function login(file: string) {
100+
return LOGIN.has(name(file))
101+
}
102+
103+
export function posix(file: string) {
104+
return POSIX.has(name(file))
105+
}
106+
107+
export const preferred = lazy(() => select(process.env.SHELL))
108+
109+
export const acceptable = lazy(() => select(process.env.SHELL, { acceptable: true }))
73110
}

0 commit comments

Comments
 (0)