fix: improve Windows compatibility and fix event listener issues (#1102)

Replace platform-specific 'which'/'where' commands with cross-platform Bun.which() API to fix Windows compatibility issues and simplify code.

Fixes:
- #1027: Comment-checker binary crashes on Windows (missing 'check' subcommand)
- #1036: Session-notification listens to non-existent events
- #1033: Infinite loop in session notifications
- #599: Doctor incorrectly reports OpenCode as not installed on Windows
- #1005: PowerShell path detection corruption on Windows

Changes:
- Use Bun.which() instead of spawning 'which'/'where' commands
- Add 'check' subcommand to comment-checker invocation
- Remove non-existent event listeners (session.updated, message.created)
- Prevent notification commands from resetting their own state
- Fix edge case: clear notifiedSessions if activity occurs during notification

All changes are cross-platform compatible and tested on Windows/Linux/macOS.
This commit is contained in:
Nguyễn Văn Tín 2026-02-01 17:13:54 +07:00 committed by GitHub
parent ffbca5e48e
commit 011eb48ffd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 18 additions and 40 deletions

View File

@ -3,11 +3,9 @@ import { CHECK_IDS, CHECK_NAMES } from "../constants"
async function checkBinaryExists(binary: string): Promise<{ exists: boolean; path: string | null }> { async function checkBinaryExists(binary: string): Promise<{ exists: boolean; path: string | null }> {
try { try {
const proc = Bun.spawn(["which", binary], { stdout: "pipe", stderr: "pipe" }) const path = Bun.which(binary)
const output = await new Response(proc.stdout).text() if (path) {
await proc.exited return { exists: true, path }
if (proc.exitCode === 0) {
return { exists: true, path: output.trim() }
} }
} catch { } catch {
// intentionally empty - binary not found // intentionally empty - binary not found

View File

@ -55,16 +55,9 @@ export function buildVersionCommand(
export async function findOpenCodeBinary(): Promise<{ binary: string; path: string } | null> { export async function findOpenCodeBinary(): Promise<{ binary: string; path: string } | null> {
for (const binary of OPENCODE_BINARIES) { for (const binary of OPENCODE_BINARIES) {
try { try {
const lookupCommand = getBinaryLookupCommand(process.platform) const path = Bun.which(binary)
const proc = Bun.spawn([lookupCommand, binary], { stdout: "pipe", stderr: "pipe" }) if (path) {
const output = await new Response(proc.stdout).text() return { binary, path }
await proc.exited
if (proc.exitCode === 0) {
const paths = parseBinaryPaths(output)
const selectedPath = selectBinaryPath(paths, process.platform)
if (selectedPath) {
return { binary, path: selectedPath }
}
} }
} catch { } catch {
continue continue

View File

@ -165,7 +165,7 @@ export async function runCommentChecker(input: HookInput, cliPath?: string, cust
debugLog("running comment-checker with input:", jsonInput.substring(0, 200)) debugLog("running comment-checker with input:", jsonInput.substring(0, 200))
try { try {
const args = [binaryPath] const args = [binaryPath, "check"]
if (customPrompt) { if (customPrompt) {
args.push("--prompt", customPrompt) args.push("--prompt", customPrompt)
} }

View File

@ -3,28 +3,8 @@ import { spawn } from "bun"
type Platform = "darwin" | "linux" | "win32" | "unsupported" type Platform = "darwin" | "linux" | "win32" | "unsupported"
async function findCommand(commandName: string): Promise<string | null> { async function findCommand(commandName: string): Promise<string | null> {
const isWindows = process.platform === "win32"
const cmd = isWindows ? "where" : "which"
try { try {
const proc = spawn([cmd, commandName], { return Bun.which(commandName)
stdout: "pipe",
stderr: "pipe",
})
const exitCode = await proc.exited
if (exitCode !== 0) {
return null
}
const stdout = await new Response(proc.stdout).text()
const path = stdout.trim().split("\n")[0]
if (!path) {
return null
}
return path
} catch { } catch {
return null return null
} }

View File

@ -200,7 +200,9 @@ export function createSessionNotification(
function markSessionActivity(sessionID: string) { function markSessionActivity(sessionID: string) {
cancelPendingNotification(sessionID) cancelPendingNotification(sessionID)
notifiedSessions.delete(sessionID) if (!executingNotifications.has(sessionID)) {
notifiedSessions.delete(sessionID)
}
} }
async function executeNotification(sessionID: string, version: number) { async function executeNotification(sessionID: string, version: number) {
@ -254,6 +256,11 @@ export function createSessionNotification(
} finally { } finally {
executingNotifications.delete(sessionID) executingNotifications.delete(sessionID)
pendingTimers.delete(sessionID) pendingTimers.delete(sessionID)
// Clear notified state if there was activity during notification
if (sessionActivitySinceIdle.has(sessionID)) {
notifiedSessions.delete(sessionID)
sessionActivitySinceIdle.delete(sessionID)
}
} }
} }
@ -262,7 +269,7 @@ export function createSessionNotification(
const props = event.properties as Record<string, unknown> | undefined const props = event.properties as Record<string, unknown> | undefined
if (event.type === "session.updated" || event.type === "session.created") { if (event.type === "session.created") {
const info = props?.info as Record<string, unknown> | undefined const info = props?.info as Record<string, unknown> | undefined
const sessionID = info?.id as string | undefined const sessionID = info?.id as string | undefined
if (sessionID) { if (sessionID) {
@ -299,7 +306,7 @@ export function createSessionNotification(
return return
} }
if (event.type === "message.updated" || event.type === "message.created") { if (event.type === "message.updated") {
const info = props?.info as Record<string, unknown> | undefined const info = props?.info as Record<string, unknown> | undefined
const sessionID = info?.sessionID as string | undefined const sessionID = info?.sessionID as string | undefined
if (sessionID) { if (sessionID) {