From 776bf63071994012edf381681f6722c9c53e75f8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 04:50:54 +0000 Subject: [PATCH 1/2] Initial plan From 90b0a59b44600a85127addaa51346fb2cddf5ceb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 04:59:34 +0000 Subject: [PATCH 2/2] Fix /copy command crash on Windows by checking /dev/tty accessibility Co-authored-by: admannon <1027194+admannon@users.noreply.github.com> --- .../cli/src/ui/utils/commandUtils.test.ts | 52 ++++++++++++++++++- packages/cli/src/ui/utils/commandUtils.ts | 15 ++++-- 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/ui/utils/commandUtils.test.ts b/packages/cli/src/ui/utils/commandUtils.test.ts index 0694a7715d3..5100511e02e 100644 --- a/packages/cli/src/ui/utils/commandUtils.test.ts +++ b/packages/cli/src/ui/utils/commandUtils.test.ts @@ -33,6 +33,10 @@ vi.mock('child_process'); // fs (for /dev/tty) const mockFs = vi.hoisted(() => ({ createWriteStream: vi.fn(), + accessSync: vi.fn(), + constants: { + W_OK: 2, // Standard Node.js constant for write access + }, })); vi.mock('node:fs', () => ({ default: mockFs, @@ -123,7 +127,7 @@ describe('commandUtils', () => { mockClipboardyWrite = clipboardy.write as Mock; // default: no /dev/tty available - mockFs.createWriteStream.mockImplementation(() => { + mockFs.accessSync.mockImplementation(() => { throw new Error('ENOENT'); }); @@ -221,6 +225,7 @@ describe('commandUtils', () => { it('writes OSC-52 to /dev/tty when in SSH', async () => { const testText = 'abc'; const tty = makeWritable({ isTTY: true }); + mockFs.accessSync.mockReturnValue(undefined); // /dev/tty is accessible mockFs.createWriteStream.mockReturnValue(tty); process.env['SSH_CONNECTION'] = '1'; @@ -239,6 +244,7 @@ describe('commandUtils', () => { it('wraps OSC-52 for tmux', async () => { const testText = 'tmux-copy'; const tty = makeWritable({ isTTY: true }); + mockFs.accessSync.mockReturnValue(undefined); // /dev/tty is accessible mockFs.createWriteStream.mockReturnValue(tty); process.env['TMUX'] = '1'; @@ -258,6 +264,7 @@ describe('commandUtils', () => { // ensure payload > chunk size (240) so there are multiple chunks const testText = 'x'.repeat(1200); const tty = makeWritable({ isTTY: true }); + mockFs.accessSync.mockReturnValue(undefined); // /dev/tty is accessible mockFs.createWriteStream.mockReturnValue(tty); process.env['STY'] = 'screen-session'; @@ -308,6 +315,7 @@ describe('commandUtils', () => { it('resolves on drain when backpressure occurs', async () => { const tty = makeWritable({ isTTY: true, writeReturn: false }); + mockFs.accessSync.mockReturnValue(undefined); // /dev/tty is accessible mockFs.createWriteStream.mockReturnValue(tty); process.env['SSH_CONNECTION'] = '1'; @@ -320,6 +328,7 @@ describe('commandUtils', () => { it('propagates errors from OSC-52 write path', async () => { const tty = makeWritable({ isTTY: true, writeReturn: false }); + mockFs.accessSync.mockReturnValue(undefined); // /dev/tty is accessible mockFs.createWriteStream.mockReturnValue(tty); process.env['SSH_CONNECTION'] = '1'; @@ -344,6 +353,7 @@ describe('commandUtils', () => { it('uses clipboardy when not in eligible env even if /dev/tty exists', async () => { const tty = makeWritable({ isTTY: true }); + mockFs.accessSync.mockReturnValue(undefined); // /dev/tty is accessible mockFs.createWriteStream.mockReturnValue(tty); const text = 'local-terminal'; mockClipboardyWrite.mockResolvedValue(undefined); @@ -354,6 +364,46 @@ describe('commandUtils', () => { expect(tty.write).not.toHaveBeenCalled(); expect(tty.end).not.toHaveBeenCalled(); }); + + it('skips /dev/tty on Windows and falls back to stderr when in SSH', async () => { + const originalPlatform = mockProcess.platform; + mockProcess.platform = 'win32'; + + const stderrStream = makeWritable({ isTTY: true }); + Object.defineProperty(process, 'stderr', { + value: stderrStream, + configurable: true, + }); + + process.env['SSH_CONNECTION'] = '1'; + const testText = 'windows-ssh'; + + await copyToClipboard(testText); + + const b64 = Buffer.from(testText, 'utf8').toString('base64'); + const expected = `${ESC}]52;c;${b64}${BEL}`; + + expect(stderrStream.write).toHaveBeenCalledWith(expected); + expect(mockFs.accessSync).not.toHaveBeenCalled(); // Should not attempt /dev/tty on Windows + expect(mockClipboardyWrite).not.toHaveBeenCalled(); + + mockProcess.platform = originalPlatform; + }); + + it('skips /dev/tty on Windows and uses clipboardy when no TTY available', async () => { + const originalPlatform = mockProcess.platform; + mockProcess.platform = 'win32'; + + const testText = 'windows-no-tty'; + mockClipboardyWrite.mockResolvedValue(undefined); + + await copyToClipboard(testText); + + expect(mockFs.accessSync).not.toHaveBeenCalled(); // Should not attempt /dev/tty on Windows + expect(mockClipboardyWrite).toHaveBeenCalledWith(testText); + + mockProcess.platform = originalPlatform; + }); }); describe('getUrlOpenCommand', () => { diff --git a/packages/cli/src/ui/utils/commandUtils.ts b/packages/cli/src/ui/utils/commandUtils.ts index d16e1084234..23a22f0bc87 100644 --- a/packages/cli/src/ui/utils/commandUtils.ts +++ b/packages/cli/src/ui/utils/commandUtils.ts @@ -67,11 +67,16 @@ type TtyTarget = { stream: Writable; closeAfter: boolean } | null; const pickTty = (): TtyTarget => { // Prefer the controlling TTY to avoid interleaving escape sequences with piped stdout. - try { - const devTty = fs.createWriteStream('/dev/tty'); - return { stream: devTty, closeAfter: true }; - } catch { - // fall through + // Skip /dev/tty on Windows as it doesn't exist + if (process.platform !== 'win32') { + try { + // Check if /dev/tty exists and is writable before creating the stream + fs.accessSync('/dev/tty', fs.constants.W_OK); + const devTty = fs.createWriteStream('/dev/tty'); + return { stream: devTty, closeAfter: true }; + } catch { + // fall through + } } if (process.stderr?.isTTY) return { stream: process.stderr, closeAfter: false };