fix(cli): safely handle /dev/tty access on macOS (#16531)

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
Krushna Korade
2026-01-16 01:40:53 +05:30
committed by GitHub
parent b0d2ec55cc
commit 8a627d6c9a
2 changed files with 128 additions and 24 deletions

View File

@@ -65,20 +65,50 @@ const SCREEN_DCS_CHUNK_SIZE = 240;
type TtyTarget = { stream: Writable; closeAfter: boolean } | null;
const pickTty = (): TtyTarget => {
// /dev/tty is only available on Unix-like systems (Linux, macOS, BSD, etc.)
if (process.platform !== 'win32') {
// Prefer the controlling TTY to avoid interleaving escape sequences with piped stdout.
try {
const devTty = fs.createWriteStream('/dev/tty');
// Prevent unhandled 'error' events from crashing the process.
devTty.on('error', () => {});
return { stream: devTty, closeAfter: true };
} catch {
// fall through - /dev/tty not accessible
}
}
const pickTty = (): Promise<TtyTarget> =>
new Promise((resolve) => {
// /dev/tty is only available on Unix-like systems (Linux, macOS, BSD, etc.)
if (process.platform !== 'win32') {
// Prefer the controlling TTY to avoid interleaving escape sequences with piped stdout.
try {
const devTty = fs.createWriteStream('/dev/tty');
// Safety timeout: if /dev/tty doesn't respond quickly, fallback to avoid hanging.
const timeout = setTimeout(() => {
// Remove listeners to prevent them from firing after timeout.
devTty.removeAllListeners('open');
devTty.removeAllListeners('error');
devTty.destroy();
resolve(getStdioTty());
}, 100);
// If we can't open it (e.g. sandbox), we'll get an error.
// We wait for 'open' to confirm it's usable, or 'error' to fallback.
// If it opens, we resolve with the stream.
devTty.once('open', () => {
clearTimeout(timeout);
devTty.removeAllListeners('error');
// Prevent future unhandled 'error' events from crashing the process
devTty.on('error', () => {});
resolve({ stream: devTty, closeAfter: true });
});
// If it errors immediately (or quickly), we fallback.
devTty.once('error', () => {
clearTimeout(timeout);
devTty.removeAllListeners('open');
resolve(getStdioTty());
});
return;
} catch {
// fall through - synchronous failure
}
}
resolve(getStdioTty());
});
const getStdioTty = (): TtyTarget => {
if (process.stderr?.isTTY)
return { stream: process.stderr, closeAfter: false };
if (process.stdout?.isTTY)
@@ -172,7 +202,7 @@ const writeAll = (stream: Writable, data: string): Promise<void> =>
export const copyToClipboard = async (text: string): Promise<void> => {
if (!text) return;
const tty = pickTty();
const tty = await pickTty();
if (shouldUseOsc52(tty)) {
const osc = buildOsc52(text);