fix: support cmux tmux compatibility
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { afterAll, beforeEach, describe, expect, test } from "bun:test"
|
||||
import fs from "node:fs/promises"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
|
||||
import { getTmuxPath, resetTmuxPathCacheForTesting } from "./tmux-path-resolver"
|
||||
|
||||
const temporaryDirectories: string[] = []
|
||||
const originalCmuxSocketPath = process.env.CMUX_SOCKET_PATH
|
||||
const originalTmux = process.env.TMUX
|
||||
const originalPath = process.env.PATH
|
||||
|
||||
async function createTemporaryDirectory(): Promise<string> {
|
||||
const directoryPath = await fs.mkdtemp(path.join(os.tmpdir(), "tmux-path-resolver-"))
|
||||
temporaryDirectories.push(directoryPath)
|
||||
return directoryPath
|
||||
}
|
||||
|
||||
async function createExecutable(directoryPath: string, name: string, script: string): Promise<string> {
|
||||
const executablePath = path.join(directoryPath, name)
|
||||
await fs.writeFile(executablePath, script, "utf8")
|
||||
await fs.chmod(executablePath, 0o755)
|
||||
return executablePath
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
resetTmuxPathCacheForTesting()
|
||||
delete process.env.CMUX_SOCKET_PATH
|
||||
delete process.env.TMUX
|
||||
process.env.PATH = originalPath
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
resetTmuxPathCacheForTesting()
|
||||
|
||||
if (originalCmuxSocketPath === undefined) {
|
||||
delete process.env.CMUX_SOCKET_PATH
|
||||
} else {
|
||||
process.env.CMUX_SOCKET_PATH = originalCmuxSocketPath
|
||||
}
|
||||
|
||||
if (originalTmux === undefined) {
|
||||
delete process.env.TMUX
|
||||
} else {
|
||||
process.env.TMUX = originalTmux
|
||||
}
|
||||
|
||||
process.env.PATH = originalPath
|
||||
|
||||
for (const directoryPath of temporaryDirectories) {
|
||||
await fs.rm(directoryPath, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
describe("getTmuxPath", () => {
|
||||
test("#given cmux environment #when cmux is available #then returns cmux without requiring a real tmux binary", async () => {
|
||||
// given
|
||||
const temporaryDirectory = await createTemporaryDirectory()
|
||||
const cmuxPath = await createExecutable(temporaryDirectory, "cmux", "#!/bin/sh\nexit 0\n")
|
||||
await createExecutable(temporaryDirectory, "tmux", "#!/bin/sh\nexit 1\n")
|
||||
process.env.CMUX_SOCKET_PATH = path.join(temporaryDirectory, "cmux.sock")
|
||||
process.env.PATH = `${temporaryDirectory}${path.delimiter}${originalPath ?? ""}`
|
||||
|
||||
// when
|
||||
const resolvedPath = await getTmuxPath()
|
||||
|
||||
// then
|
||||
expect(path.basename(resolvedPath ?? "")).toBe(path.basename(cmuxPath))
|
||||
})
|
||||
})
|
||||
@@ -2,13 +2,24 @@ import { spawn } from "../../shared/bun-spawn-shim"
|
||||
|
||||
let tmuxPath: string | null = null
|
||||
let initPromise: Promise<string | null> | null = null
|
||||
let tmuxPathEnvironmentKey: "cmux" | "tmux" | null = null
|
||||
|
||||
async function findTmuxPath(): Promise<string | null> {
|
||||
function isCmuxCompatEnvironment(): boolean {
|
||||
return Boolean(process.env.CMUX_SOCKET_PATH) ||
|
||||
process.env.TMUX?.includes("cmuxterm") === true
|
||||
}
|
||||
|
||||
function getEnvironmentKey(): "cmux" | "tmux" {
|
||||
return isCmuxCompatEnvironment() ? "cmux" : "tmux"
|
||||
}
|
||||
|
||||
async function findCommandPath(command: string): Promise<string | null> {
|
||||
const isWindows = process.platform === "win32"
|
||||
const cmd = isWindows ? "where" : "which"
|
||||
|
||||
try {
|
||||
const proc = spawn([cmd, "tmux"], {
|
||||
const proc = spawn([cmd, command], {
|
||||
env: process.env,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
@@ -25,7 +36,21 @@ async function findTmuxPath(): Promise<string | null> {
|
||||
return null
|
||||
}
|
||||
|
||||
return path
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function findVerifiedTmuxPath(): Promise<string | null> {
|
||||
const path = await findCommandPath("tmux")
|
||||
if (!path) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const verifyProc = spawn([path, "-V"], {
|
||||
env: process.env,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
@@ -41,18 +66,34 @@ async function findTmuxPath(): Promise<string | null> {
|
||||
}
|
||||
}
|
||||
|
||||
async function findTmuxPath(): Promise<string | null> {
|
||||
if (isCmuxCompatEnvironment()) {
|
||||
const cmuxPath = await findCommandPath("cmux")
|
||||
if (cmuxPath) {
|
||||
return cmuxPath
|
||||
}
|
||||
}
|
||||
|
||||
return findVerifiedTmuxPath()
|
||||
}
|
||||
|
||||
export async function getTmuxPath(): Promise<string | null> {
|
||||
if (tmuxPath !== null) {
|
||||
const environmentKey = getEnvironmentKey()
|
||||
if (tmuxPath !== null && tmuxPathEnvironmentKey === environmentKey) {
|
||||
return tmuxPath
|
||||
}
|
||||
|
||||
if (initPromise) {
|
||||
if (initPromise && tmuxPathEnvironmentKey === environmentKey) {
|
||||
return initPromise
|
||||
}
|
||||
|
||||
tmuxPathEnvironmentKey = environmentKey
|
||||
const promiseEnvironmentKey = environmentKey
|
||||
initPromise = (async () => {
|
||||
const path = await findTmuxPath()
|
||||
tmuxPath = path
|
||||
if (tmuxPathEnvironmentKey === promiseEnvironmentKey) {
|
||||
tmuxPath = path
|
||||
}
|
||||
return path
|
||||
})()
|
||||
|
||||
@@ -63,6 +104,12 @@ export function getCachedTmuxPath(): string | null {
|
||||
return tmuxPath
|
||||
}
|
||||
|
||||
export function resetTmuxPathCacheForTesting(): void {
|
||||
tmuxPath = null
|
||||
initPromise = null
|
||||
tmuxPathEnvironmentKey = null
|
||||
}
|
||||
|
||||
export function startBackgroundCheck(): void {
|
||||
if (!initPromise) {
|
||||
initPromise = getTmuxPath()
|
||||
|
||||
Reference in New Issue
Block a user