fix(windows): add windowsHide to Bun.spawn calls to prevent stray terminal windows
Closes #1915
This commit is contained in:
parent
07e8d965a8
commit
269f37af1c
@ -1,4 +1,5 @@
|
|||||||
import { getConfigDir } from "./config-context"
|
import { getConfigDir } from "./config-context"
|
||||||
|
import { spawnWithWindowsHide } from "../../shared/spawn-with-windows-hide"
|
||||||
|
|
||||||
const BUN_INSTALL_TIMEOUT_SECONDS = 60
|
const BUN_INSTALL_TIMEOUT_SECONDS = 60
|
||||||
const BUN_INSTALL_TIMEOUT_MS = BUN_INSTALL_TIMEOUT_SECONDS * 1000
|
const BUN_INSTALL_TIMEOUT_MS = BUN_INSTALL_TIMEOUT_SECONDS * 1000
|
||||||
@ -16,7 +17,7 @@ export async function runBunInstall(): Promise<boolean> {
|
|||||||
|
|
||||||
export async function runBunInstallWithDetails(): Promise<BunInstallResult> {
|
export async function runBunInstallWithDetails(): Promise<BunInstallResult> {
|
||||||
try {
|
try {
|
||||||
const proc = Bun.spawn(["bun", "install"], {
|
const proc = spawnWithWindowsHide(["bun", "install"], {
|
||||||
cwd: getConfigDir(),
|
cwd: getConfigDir(),
|
||||||
stdout: "inherit",
|
stdout: "inherit",
|
||||||
stderr: "inherit",
|
stderr: "inherit",
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import type { OpenCodeBinaryType } from "../../shared/opencode-config-dir-types"
|
import type { OpenCodeBinaryType } from "../../shared/opencode-config-dir-types"
|
||||||
|
import { spawnWithWindowsHide } from "../../shared/spawn-with-windows-hide"
|
||||||
import { initConfigContext } from "./config-context"
|
import { initConfigContext } from "./config-context"
|
||||||
|
|
||||||
const OPENCODE_BINARIES = ["opencode", "opencode-desktop"] as const
|
const OPENCODE_BINARIES = ["opencode", "opencode-desktop"] as const
|
||||||
@ -11,7 +12,7 @@ interface OpenCodeBinaryResult {
|
|||||||
async function findOpenCodeBinaryWithVersion(): Promise<OpenCodeBinaryResult | null> {
|
async function findOpenCodeBinaryWithVersion(): Promise<OpenCodeBinaryResult | null> {
|
||||||
for (const binary of OPENCODE_BINARIES) {
|
for (const binary of OPENCODE_BINARIES) {
|
||||||
try {
|
try {
|
||||||
const proc = Bun.spawn([binary, "--version"], {
|
const proc = spawnWithWindowsHide([binary, "--version"], {
|
||||||
stdout: "pipe",
|
stdout: "pipe",
|
||||||
stderr: "pipe",
|
stderr: "pipe",
|
||||||
})
|
})
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { createRequire } from "node:module"
|
|||||||
import { dirname, join } from "node:path"
|
import { dirname, join } from "node:path"
|
||||||
|
|
||||||
import type { DependencyInfo } from "../types"
|
import type { DependencyInfo } from "../types"
|
||||||
|
import { spawnWithWindowsHide } from "../../../shared/spawn-with-windows-hide"
|
||||||
|
|
||||||
async function checkBinaryExists(binary: string): Promise<{ exists: boolean; path: string | null }> {
|
async function checkBinaryExists(binary: string): Promise<{ exists: boolean; path: string | null }> {
|
||||||
try {
|
try {
|
||||||
@ -18,7 +19,7 @@ async function checkBinaryExists(binary: string): Promise<{ exists: boolean; pat
|
|||||||
|
|
||||||
async function getBinaryVersion(binary: string): Promise<string | null> {
|
async function getBinaryVersion(binary: string): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const proc = Bun.spawn([binary, "--version"], { stdout: "pipe", stderr: "pipe" })
|
const proc = spawnWithWindowsHide([binary, "--version"], { stdout: "pipe", stderr: "pipe" })
|
||||||
const output = await new Response(proc.stdout).text()
|
const output = await new Response(proc.stdout).text()
|
||||||
await proc.exited
|
await proc.exited
|
||||||
if (proc.exitCode === 0) {
|
if (proc.exitCode === 0) {
|
||||||
@ -140,4 +141,3 @@ export async function checkCommentChecker(): Promise<DependencyInfo> {
|
|||||||
path: resolvedPath,
|
path: resolvedPath,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { existsSync } from "node:fs"
|
import { existsSync } from "node:fs"
|
||||||
import { homedir } from "node:os"
|
import { homedir } from "node:os"
|
||||||
import { join } from "node:path"
|
import { join } from "node:path"
|
||||||
|
import { spawnWithWindowsHide } from "../../../shared/spawn-with-windows-hide"
|
||||||
|
|
||||||
import { OPENCODE_BINARIES } from "../constants"
|
import { OPENCODE_BINARIES } from "../constants"
|
||||||
|
|
||||||
@ -110,7 +111,7 @@ export async function getOpenCodeVersion(
|
|||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const command = buildVersionCommand(binaryPath, platform)
|
const command = buildVersionCommand(binaryPath, platform)
|
||||||
const processResult = Bun.spawn(command, { stdout: "pipe", stderr: "pipe" })
|
const processResult = spawnWithWindowsHide(command, { stdout: "pipe", stderr: "pipe" })
|
||||||
const output = await new Response(processResult.stdout).text()
|
const output = await new Response(processResult.stdout).text()
|
||||||
await processResult.exited
|
await processResult.exited
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { spawnWithWindowsHide } from "../../../shared/spawn-with-windows-hide"
|
||||||
|
|
||||||
export interface GhCliInfo {
|
export interface GhCliInfo {
|
||||||
installed: boolean
|
installed: boolean
|
||||||
version: string | null
|
version: string | null
|
||||||
@ -19,7 +21,7 @@ async function checkBinaryExists(binary: string): Promise<{ exists: boolean; pat
|
|||||||
|
|
||||||
async function getGhVersion(): Promise<string | null> {
|
async function getGhVersion(): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const processResult = Bun.spawn(["gh", "--version"], { stdout: "pipe", stderr: "pipe" })
|
const processResult = spawnWithWindowsHide(["gh", "--version"], { stdout: "pipe", stderr: "pipe" })
|
||||||
const output = await new Response(processResult.stdout).text()
|
const output = await new Response(processResult.stdout).text()
|
||||||
await processResult.exited
|
await processResult.exited
|
||||||
if (processResult.exitCode !== 0) return null
|
if (processResult.exitCode !== 0) return null
|
||||||
@ -38,7 +40,7 @@ async function getGhAuthStatus(): Promise<{
|
|||||||
error: string | null
|
error: string | null
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
const processResult = Bun.spawn(["gh", "auth", "status"], {
|
const processResult = spawnWithWindowsHide(["gh", "auth", "status"], {
|
||||||
stdout: "pipe",
|
stdout: "pipe",
|
||||||
stderr: "pipe",
|
stderr: "pipe",
|
||||||
env: { ...process.env, GH_NO_UPDATE_NOTIFIER: "1" },
|
env: { ...process.env, GH_NO_UPDATE_NOTIFIER: "1" },
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import type { RunResult } from "./types"
|
|||||||
import { createJsonOutputManager } from "./json-output"
|
import { createJsonOutputManager } from "./json-output"
|
||||||
import { resolveSession } from "./session-resolver"
|
import { resolveSession } from "./session-resolver"
|
||||||
import { executeOnCompleteHook } from "./on-complete-hook"
|
import { executeOnCompleteHook } from "./on-complete-hook"
|
||||||
|
import * as spawnWithWindowsHideModule from "../../shared/spawn-with-windows-hide"
|
||||||
import type { OpencodeClient } from "./types"
|
import type { OpencodeClient } from "./types"
|
||||||
import * as originalSdk from "@opencode-ai/sdk"
|
import * as originalSdk from "@opencode-ai/sdk"
|
||||||
import * as originalPortUtils from "../../shared/port-utils"
|
import * as originalPortUtils from "../../shared/port-utils"
|
||||||
@ -147,7 +148,7 @@ describe("integration: --session-id", () => {
|
|||||||
const result = resolveSession({ client: mockClient, sessionId, directory: "/test" })
|
const result = resolveSession({ client: mockClient, sessionId, directory: "/test" })
|
||||||
|
|
||||||
// then
|
// then
|
||||||
await expect(result).rejects.toThrow(`Session not found: ${sessionId}`)
|
expect(result).rejects.toThrow(`Session not found: ${sessionId}`)
|
||||||
expect(mockClient.session.get).toHaveBeenCalledWith({
|
expect(mockClient.session.get).toHaveBeenCalledWith({
|
||||||
path: { id: sessionId },
|
path: { id: sessionId },
|
||||||
query: { directory: "/test" },
|
query: { directory: "/test" },
|
||||||
@ -161,10 +162,13 @@ describe("integration: --on-complete", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(console, "error").mockImplementation(() => {})
|
spyOn(console, "error").mockImplementation(() => {})
|
||||||
spawnSpy = spyOn(Bun, "spawn").mockReturnValue({
|
spawnSpy = spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockReturnValue({
|
||||||
exited: Promise.resolve(0),
|
exited: Promise.resolve(0),
|
||||||
exitCode: 0,
|
exitCode: 0,
|
||||||
} as unknown as ReturnType<typeof Bun.spawn>)
|
stdout: undefined,
|
||||||
|
stderr: undefined,
|
||||||
|
kill: () => {},
|
||||||
|
} satisfies ReturnType<typeof spawnWithWindowsHideModule.spawnWithWindowsHide>)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -186,7 +190,7 @@ describe("integration: --on-complete", () => {
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
expect(spawnSpy).toHaveBeenCalledTimes(1)
|
expect(spawnSpy).toHaveBeenCalledTimes(1)
|
||||||
const [_, options] = spawnSpy.mock.calls[0] as Parameters<typeof Bun.spawn>
|
const [_, options] = spawnSpy.mock.calls[0] as Parameters<typeof spawnWithWindowsHideModule.spawnWithWindowsHide>
|
||||||
expect(options?.env?.SESSION_ID).toBe("session-123")
|
expect(options?.env?.SESSION_ID).toBe("session-123")
|
||||||
expect(options?.env?.EXIT_CODE).toBe("0")
|
expect(options?.env?.EXIT_CODE).toBe("0")
|
||||||
expect(options?.env?.DURATION_MS).toBe("5000")
|
expect(options?.env?.DURATION_MS).toBe("5000")
|
||||||
@ -208,10 +212,13 @@ describe("integration: option combinations", () => {
|
|||||||
spyOn(console, "error").mockImplementation(() => {})
|
spyOn(console, "error").mockImplementation(() => {})
|
||||||
mockStdout = createMockWriteStream()
|
mockStdout = createMockWriteStream()
|
||||||
mockStderr = createMockWriteStream()
|
mockStderr = createMockWriteStream()
|
||||||
spawnSpy = spyOn(Bun, "spawn").mockReturnValue({
|
spawnSpy = spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockReturnValue({
|
||||||
exited: Promise.resolve(0),
|
exited: Promise.resolve(0),
|
||||||
exitCode: 0,
|
exitCode: 0,
|
||||||
} as unknown as ReturnType<typeof Bun.spawn>)
|
stdout: undefined,
|
||||||
|
stderr: undefined,
|
||||||
|
kill: () => {},
|
||||||
|
} satisfies ReturnType<typeof spawnWithWindowsHideModule.spawnWithWindowsHide>)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -249,9 +256,9 @@ describe("integration: option combinations", () => {
|
|||||||
const emitted = mockStdout.writes[0]!
|
const emitted = mockStdout.writes[0]!
|
||||||
expect(() => JSON.parse(emitted)).not.toThrow()
|
expect(() => JSON.parse(emitted)).not.toThrow()
|
||||||
expect(spawnSpy).toHaveBeenCalledTimes(1)
|
expect(spawnSpy).toHaveBeenCalledTimes(1)
|
||||||
const [args] = spawnSpy.mock.calls[0] as Parameters<typeof Bun.spawn>
|
const [args] = spawnSpy.mock.calls[0] as Parameters<typeof spawnWithWindowsHideModule.spawnWithWindowsHide>
|
||||||
expect(args).toEqual(["sh", "-c", "echo done"])
|
expect(args).toEqual(["sh", "-c", "echo done"])
|
||||||
const [_, options] = spawnSpy.mock.calls[0] as Parameters<typeof Bun.spawn>
|
const [_, options] = spawnSpy.mock.calls[0] as Parameters<typeof spawnWithWindowsHideModule.spawnWithWindowsHide>
|
||||||
expect(options?.env?.SESSION_ID).toBe("session-123")
|
expect(options?.env?.SESSION_ID).toBe("session-123")
|
||||||
expect(options?.env?.EXIT_CODE).toBe("0")
|
expect(options?.env?.EXIT_CODE).toBe("0")
|
||||||
expect(options?.env?.DURATION_MS).toBe("5000")
|
expect(options?.env?.DURATION_MS).toBe("5000")
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { describe, it, expect, spyOn, beforeEach, afterEach } from "bun:test"
|
import { describe, it, expect, spyOn, beforeEach, afterEach } from "bun:test"
|
||||||
|
import * as spawnWithWindowsHideModule from "../../shared/spawn-with-windows-hide"
|
||||||
import { executeOnCompleteHook } from "./on-complete-hook"
|
import { executeOnCompleteHook } from "./on-complete-hook"
|
||||||
|
|
||||||
describe("executeOnCompleteHook", () => {
|
describe("executeOnCompleteHook", () => {
|
||||||
@ -6,7 +7,10 @@ describe("executeOnCompleteHook", () => {
|
|||||||
return {
|
return {
|
||||||
exited: Promise.resolve(exitCode),
|
exited: Promise.resolve(exitCode),
|
||||||
exitCode,
|
exitCode,
|
||||||
} as unknown as ReturnType<typeof Bun.spawn>
|
stdout: undefined,
|
||||||
|
stderr: undefined,
|
||||||
|
kill: () => {},
|
||||||
|
} satisfies ReturnType<typeof spawnWithWindowsHideModule.spawnWithWindowsHide>
|
||||||
}
|
}
|
||||||
|
|
||||||
let consoleErrorSpy: ReturnType<typeof spyOn<typeof console, "error">>
|
let consoleErrorSpy: ReturnType<typeof spyOn<typeof console, "error">>
|
||||||
@ -21,7 +25,7 @@ describe("executeOnCompleteHook", () => {
|
|||||||
|
|
||||||
it("executes command with correct env vars", async () => {
|
it("executes command with correct env vars", async () => {
|
||||||
// given
|
// given
|
||||||
const spawnSpy = spyOn(Bun, "spawn").mockReturnValue(createProc(0))
|
const spawnSpy = spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockReturnValue(createProc(0))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// when
|
// when
|
||||||
@ -35,7 +39,7 @@ describe("executeOnCompleteHook", () => {
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
expect(spawnSpy).toHaveBeenCalledTimes(1)
|
expect(spawnSpy).toHaveBeenCalledTimes(1)
|
||||||
const [args, options] = spawnSpy.mock.calls[0] as Parameters<typeof Bun.spawn>
|
const [args, options] = spawnSpy.mock.calls[0] as Parameters<typeof spawnWithWindowsHideModule.spawnWithWindowsHide>
|
||||||
|
|
||||||
expect(args).toEqual(["sh", "-c", "echo test"])
|
expect(args).toEqual(["sh", "-c", "echo test"])
|
||||||
expect(options?.env?.SESSION_ID).toBe("session-123")
|
expect(options?.env?.SESSION_ID).toBe("session-123")
|
||||||
@ -51,7 +55,7 @@ describe("executeOnCompleteHook", () => {
|
|||||||
|
|
||||||
it("env var values are strings", async () => {
|
it("env var values are strings", async () => {
|
||||||
// given
|
// given
|
||||||
const spawnSpy = spyOn(Bun, "spawn").mockReturnValue(createProc(0))
|
const spawnSpy = spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockReturnValue(createProc(0))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// when
|
// when
|
||||||
@ -64,7 +68,7 @@ describe("executeOnCompleteHook", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// then
|
// then
|
||||||
const [_, options] = spawnSpy.mock.calls[0] as Parameters<typeof Bun.spawn>
|
const [_, options] = spawnSpy.mock.calls[0] as Parameters<typeof spawnWithWindowsHideModule.spawnWithWindowsHide>
|
||||||
|
|
||||||
expect(options?.env?.EXIT_CODE).toBe("1")
|
expect(options?.env?.EXIT_CODE).toBe("1")
|
||||||
expect(options?.env?.EXIT_CODE).toBeTypeOf("string")
|
expect(options?.env?.EXIT_CODE).toBeTypeOf("string")
|
||||||
@ -79,7 +83,7 @@ describe("executeOnCompleteHook", () => {
|
|||||||
|
|
||||||
it("empty command string is no-op", async () => {
|
it("empty command string is no-op", async () => {
|
||||||
// given
|
// given
|
||||||
const spawnSpy = spyOn(Bun, "spawn").mockReturnValue(createProc(0))
|
const spawnSpy = spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockReturnValue(createProc(0))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// when
|
// when
|
||||||
@ -100,7 +104,7 @@ describe("executeOnCompleteHook", () => {
|
|||||||
|
|
||||||
it("whitespace-only command is no-op", async () => {
|
it("whitespace-only command is no-op", async () => {
|
||||||
// given
|
// given
|
||||||
const spawnSpy = spyOn(Bun, "spawn").mockReturnValue(createProc(0))
|
const spawnSpy = spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockReturnValue(createProc(0))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// when
|
// when
|
||||||
@ -121,11 +125,11 @@ describe("executeOnCompleteHook", () => {
|
|||||||
|
|
||||||
it("command failure logs warning but does not throw", async () => {
|
it("command failure logs warning but does not throw", async () => {
|
||||||
// given
|
// given
|
||||||
const spawnSpy = spyOn(Bun, "spawn").mockReturnValue(createProc(1))
|
const spawnSpy = spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockReturnValue(createProc(1))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// when
|
// when
|
||||||
await expect(
|
expect(
|
||||||
executeOnCompleteHook({
|
executeOnCompleteHook({
|
||||||
command: "false",
|
command: "false",
|
||||||
sessionId: "session-123",
|
sessionId: "session-123",
|
||||||
@ -149,13 +153,13 @@ describe("executeOnCompleteHook", () => {
|
|||||||
it("spawn error logs warning but does not throw", async () => {
|
it("spawn error logs warning but does not throw", async () => {
|
||||||
// given
|
// given
|
||||||
const spawnError = new Error("Command not found")
|
const spawnError = new Error("Command not found")
|
||||||
const spawnSpy = spyOn(Bun, "spawn").mockImplementation(() => {
|
const spawnSpy = spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockImplementation(() => {
|
||||||
throw spawnError
|
throw spawnError
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// when
|
// when
|
||||||
await expect(
|
expect(
|
||||||
executeOnCompleteHook({
|
executeOnCompleteHook({
|
||||||
command: "nonexistent-command",
|
command: "nonexistent-command",
|
||||||
sessionId: "session-123",
|
sessionId: "session-123",
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import pc from "picocolors"
|
import pc from "picocolors"
|
||||||
|
import { spawnWithWindowsHide } from "../../shared/spawn-with-windows-hide"
|
||||||
|
|
||||||
export async function executeOnCompleteHook(options: {
|
export async function executeOnCompleteHook(options: {
|
||||||
command: string
|
command: string
|
||||||
@ -17,7 +18,7 @@ export async function executeOnCompleteHook(options: {
|
|||||||
console.error(pc.dim(`Running on-complete hook: ${trimmedCommand}`))
|
console.error(pc.dim(`Running on-complete hook: ${trimmedCommand}`))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const proc = Bun.spawn(["sh", "-c", trimmedCommand], {
|
const proc = spawnWithWindowsHide(["sh", "-c", trimmedCommand], {
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
SESSION_ID: sessionId,
|
SESSION_ID: sessionId,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { delimiter, dirname, join } from "node:path"
|
import { delimiter, dirname, join } from "node:path"
|
||||||
|
import { spawnWithWindowsHide } from "../../shared/spawn-with-windows-hide"
|
||||||
|
|
||||||
const OPENCODE_COMMANDS = ["opencode", "opencode-desktop"] as const
|
const OPENCODE_COMMANDS = ["opencode", "opencode-desktop"] as const
|
||||||
const WINDOWS_SUFFIXES = ["", ".exe", ".cmd", ".bat", ".ps1"] as const
|
const WINDOWS_SUFFIXES = ["", ".exe", ".cmd", ".bat", ".ps1"] as const
|
||||||
@ -41,7 +42,7 @@ export function collectCandidateBinaryPaths(
|
|||||||
|
|
||||||
export async function canExecuteBinary(binaryPath: string): Promise<boolean> {
|
export async function canExecuteBinary(binaryPath: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const proc = Bun.spawn([binaryPath, "--version"], {
|
const proc = spawnWithWindowsHide([binaryPath, "--version"], {
|
||||||
stdout: "pipe",
|
stdout: "pipe",
|
||||||
stderr: "pipe",
|
stderr: "pipe",
|
||||||
})
|
})
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
import { OMO_SESSION_PREFIX, buildSessionReminderMessage } from "./constants";
|
import { OMO_SESSION_PREFIX, buildSessionReminderMessage } from "./constants";
|
||||||
import type { InteractiveBashSessionState } from "./types";
|
import type { InteractiveBashSessionState } from "./types";
|
||||||
import { subagentSessions } from "../../features/claude-code-session-state";
|
import { subagentSessions } from "../../features/claude-code-session-state";
|
||||||
|
import { spawnWithWindowsHide } from "../../shared/spawn-with-windows-hide";
|
||||||
|
|
||||||
type AbortSession = (args: { path: { id: string } }) => Promise<unknown>
|
type AbortSession = (args: { path: { id: string } }) => Promise<unknown>
|
||||||
|
|
||||||
@ -19,7 +20,7 @@ async function killAllTrackedSessions(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
for (const sessionName of state.tmuxSessions) {
|
for (const sessionName of state.tmuxSessions) {
|
||||||
try {
|
try {
|
||||||
const proc = Bun.spawn(["tmux", "kill-session", "-t", sessionName], {
|
const proc = spawnWithWindowsHide(["tmux", "kill-session", "-t", sessionName], {
|
||||||
stdout: "ignore",
|
stdout: "ignore",
|
||||||
stderr: "ignore",
|
stderr: "ignore",
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import type { InteractiveBashSessionState } from "./types";
|
import type { InteractiveBashSessionState } from "./types";
|
||||||
import { loadInteractiveBashSessionState } from "./storage";
|
import { loadInteractiveBashSessionState } from "./storage";
|
||||||
import { OMO_SESSION_PREFIX } from "./constants";
|
import { OMO_SESSION_PREFIX } from "./constants";
|
||||||
|
import { spawnWithWindowsHide } from "../../shared/spawn-with-windows-hide";
|
||||||
|
|
||||||
export function getOrCreateState(sessionID: string, sessionStates: Map<string, InteractiveBashSessionState>): InteractiveBashSessionState {
|
export function getOrCreateState(sessionID: string, sessionStates: Map<string, InteractiveBashSessionState>): InteractiveBashSessionState {
|
||||||
if (!sessionStates.has(sessionID)) {
|
if (!sessionStates.has(sessionID)) {
|
||||||
@ -24,7 +25,7 @@ export async function killAllTrackedSessions(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
for (const sessionName of state.tmuxSessions) {
|
for (const sessionName of state.tmuxSessions) {
|
||||||
try {
|
try {
|
||||||
const proc = Bun.spawn(["tmux", "kill-session", "-t", sessionName], {
|
const proc = spawnWithWindowsHide(["tmux", "kill-session", "-t", sessionName], {
|
||||||
stdout: "ignore",
|
stdout: "ignore",
|
||||||
stderr: "ignore",
|
stderr: "ignore",
|
||||||
});
|
});
|
||||||
|
|||||||
84
src/shared/spawn-with-windows-hide.ts
Normal file
84
src/shared/spawn-with-windows-hide.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { spawn as bunSpawn } from "bun"
|
||||||
|
import { spawn as nodeSpawn, type ChildProcess } from "node:child_process"
|
||||||
|
import { Readable } from "node:stream"
|
||||||
|
|
||||||
|
export interface SpawnOptions {
|
||||||
|
cwd?: string
|
||||||
|
env?: Record<string, string | undefined>
|
||||||
|
stdin?: "pipe" | "inherit" | "ignore"
|
||||||
|
stdout?: "pipe" | "inherit" | "ignore"
|
||||||
|
stderr?: "pipe" | "inherit" | "ignore"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpawnedProcess {
|
||||||
|
readonly exitCode: number | null
|
||||||
|
readonly exited: Promise<number>
|
||||||
|
readonly stdout: ReadableStream<Uint8Array> | undefined
|
||||||
|
readonly stderr: ReadableStream<Uint8Array> | undefined
|
||||||
|
kill(signal?: NodeJS.Signals): void
|
||||||
|
}
|
||||||
|
|
||||||
|
function toReadableStream(stream: NodeJS.ReadableStream | null): ReadableStream<Uint8Array> | undefined {
|
||||||
|
if (!stream) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return Readable.toWeb(stream as Readable) as ReadableStream<Uint8Array>
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapNodeProcess(proc: ChildProcess): SpawnedProcess {
|
||||||
|
let resolveExited: (exitCode: number) => void
|
||||||
|
let exitCode: number | null = null
|
||||||
|
|
||||||
|
const exited = new Promise<number>((resolve) => {
|
||||||
|
resolveExited = resolve
|
||||||
|
})
|
||||||
|
|
||||||
|
proc.on("exit", (code) => {
|
||||||
|
exitCode = code ?? 1
|
||||||
|
resolveExited(exitCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
proc.on("error", () => {
|
||||||
|
if (exitCode === null) {
|
||||||
|
exitCode = 1
|
||||||
|
resolveExited(1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
get exitCode() {
|
||||||
|
return exitCode
|
||||||
|
},
|
||||||
|
exited,
|
||||||
|
stdout: toReadableStream(proc.stdout),
|
||||||
|
stderr: toReadableStream(proc.stderr),
|
||||||
|
kill(signal?: NodeJS.Signals): void {
|
||||||
|
try {
|
||||||
|
if (!signal) {
|
||||||
|
proc.kill()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
proc.kill(signal)
|
||||||
|
} catch {}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function spawnWithWindowsHide(command: string[], options: SpawnOptions): SpawnedProcess {
|
||||||
|
if (process.platform !== "win32") {
|
||||||
|
return bunSpawn(command, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [cmd, ...args] = command
|
||||||
|
const proc = nodeSpawn(cmd, args, {
|
||||||
|
cwd: options.cwd,
|
||||||
|
env: options.env,
|
||||||
|
stdio: [options.stdin ?? "pipe", options.stdout ?? "pipe", options.stderr ?? "pipe"],
|
||||||
|
windowsHide: true,
|
||||||
|
shell: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
return wrapNodeProcess(proc)
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
||||||
|
import { spawnWithWindowsHide } from "../../shared/spawn-with-windows-hide"
|
||||||
import { BLOCKED_TMUX_SUBCOMMANDS, DEFAULT_TIMEOUT_MS, INTERACTIVE_BASH_DESCRIPTION } from "./constants"
|
import { BLOCKED_TMUX_SUBCOMMANDS, DEFAULT_TIMEOUT_MS, INTERACTIVE_BASH_DESCRIPTION } from "./constants"
|
||||||
import { getCachedTmuxPath } from "./tmux-path-resolver"
|
import { getCachedTmuxPath } from "./tmux-path-resolver"
|
||||||
|
|
||||||
@ -89,7 +90,7 @@ tmux capture-pane -p -t ${sessionName} -S -1000
|
|||||||
The Bash tool can execute these commands directly. Do NOT retry with interactive_bash.`
|
The Bash tool can execute these commands directly. Do NOT retry with interactive_bash.`
|
||||||
}
|
}
|
||||||
|
|
||||||
const proc = Bun.spawn([tmuxPath, ...parts], {
|
const proc = spawnWithWindowsHide([tmuxPath, ...parts], {
|
||||||
stdout: "pipe",
|
stdout: "pipe",
|
||||||
stderr: "pipe",
|
stderr: "pipe",
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user