From fd64d89da03840802c4d39b01a5915207514d29c Mon Sep 17 00:00:00 2001 From: hritan <48129645+hritan@users.noreply.github.com> Date: Wed, 20 Aug 2025 18:42:42 +0000 Subject: [PATCH] fix: copy command gets stuck (#6482) Co-authored-by: Hriday Taneja --- .../cli/src/ui/utils/commandUtils.test.ts | 37 ++++++++++++------- packages/cli/src/ui/utils/commandUtils.ts | 30 ++++++++++----- 2 files changed, 45 insertions(+), 22 deletions(-) diff --git a/packages/cli/src/ui/utils/commandUtils.test.ts b/packages/cli/src/ui/utils/commandUtils.test.ts index db333e72..c1920599 100644 --- a/packages/cli/src/ui/utils/commandUtils.test.ts +++ b/packages/cli/src/ui/utils/commandUtils.test.ts @@ -5,7 +5,7 @@ */ import { vi, describe, it, expect, beforeEach, Mock } from 'vitest'; -import { spawn } from 'child_process'; +import { spawn, SpawnOptions } from 'child_process'; import { EventEmitter } from 'events'; import { isAtCommand, @@ -186,6 +186,9 @@ describe('commandUtils', () => { it('should successfully copy text to clipboard using xclip', async () => { const testText = 'Hello, world!'; + const linuxOptions: SpawnOptions = { + stdio: ['pipe', 'inherit', 'pipe'], + }; setTimeout(() => { mockChild.emit('close', 0); @@ -193,10 +196,11 @@ describe('commandUtils', () => { await copyToClipboard(testText); - expect(mockSpawn).toHaveBeenCalledWith('xclip', [ - '-selection', - 'clipboard', - ]); + expect(mockSpawn).toHaveBeenCalledWith( + 'xclip', + ['-selection', 'clipboard'], + linuxOptions, + ); expect(mockChild.stdin.write).toHaveBeenCalledWith(testText); expect(mockChild.stdin.end).toHaveBeenCalled(); }); @@ -204,6 +208,9 @@ describe('commandUtils', () => { it('should fall back to xsel when xclip fails', async () => { const testText = 'Hello, world!'; let callCount = 0; + const linuxOptions: SpawnOptions = { + stdio: ['pipe', 'inherit', 'pipe'], + }; mockSpawn.mockImplementation(() => { const child = Object.assign(new EventEmitter(), { @@ -232,14 +239,18 @@ describe('commandUtils', () => { await copyToClipboard(testText); expect(mockSpawn).toHaveBeenCalledTimes(2); - expect(mockSpawn).toHaveBeenNthCalledWith(1, 'xclip', [ - '-selection', - 'clipboard', - ]); - expect(mockSpawn).toHaveBeenNthCalledWith(2, 'xsel', [ - '--clipboard', - '--input', - ]); + expect(mockSpawn).toHaveBeenNthCalledWith( + 1, + 'xclip', + ['-selection', 'clipboard'], + linuxOptions, + ); + expect(mockSpawn).toHaveBeenNthCalledWith( + 2, + 'xsel', + ['--clipboard', '--input'], + linuxOptions, + ); }); it('should throw error when both xclip and xsel fail', async () => { diff --git a/packages/cli/src/ui/utils/commandUtils.ts b/packages/cli/src/ui/utils/commandUtils.ts index 80ed51ae..089ec339 100644 --- a/packages/cli/src/ui/utils/commandUtils.ts +++ b/packages/cli/src/ui/utils/commandUtils.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { spawn } from 'child_process'; +import { spawn, SpawnOptions } from 'child_process'; /** * Checks if a query string potentially represents an '@' command. @@ -29,11 +29,13 @@ export const isSlashCommand = (query: string): boolean => query.startsWith('/'); // Copies a string snippet to the clipboard for different platforms export const copyToClipboard = async (text: string): Promise => { - const run = (cmd: string, args: string[]) => + const run = (cmd: string, args: string[], options?: SpawnOptions) => new Promise((resolve, reject) => { - const child = spawn(cmd, args); + const child = options ? spawn(cmd, args, options) : spawn(cmd, args); let stderr = ''; - child.stderr.on('data', (chunk) => (stderr += chunk.toString())); + if (child.stderr) { + child.stderr.on('data', (chunk) => (stderr += chunk.toString())); + } child.on('error', reject); child.on('close', (code) => { if (code === 0) return resolve(); @@ -44,11 +46,21 @@ export const copyToClipboard = async (text: string): Promise => { ), ); }); - child.stdin.on('error', reject); - child.stdin.write(text); - child.stdin.end(); + if (child.stdin) { + child.stdin.on('error', reject); + child.stdin.write(text); + child.stdin.end(); + } else { + reject(new Error('Child process has no stdin stream to write to.')); + } }); + // Configure stdio for Linux clipboard commands. + // - stdin: 'pipe' to write the text that needs to be copied. + // - stdout: 'inherit' since we don't need to capture the command's output on success. + // - stderr: 'pipe' to capture error messages (e.g., "command not found") for better error handling. + const linuxOptions: SpawnOptions = { stdio: ['pipe', 'inherit', 'pipe'] }; + switch (process.platform) { case 'win32': return run('clip', []); @@ -56,11 +68,11 @@ export const copyToClipboard = async (text: string): Promise => { return run('pbcopy', []); case 'linux': try { - await run('xclip', ['-selection', 'clipboard']); + await run('xclip', ['-selection', 'clipboard'], linuxOptions); } catch (primaryError) { try { // If xclip fails for any reason, try xsel as a fallback. - await run('xsel', ['--clipboard', '--input']); + await run('xsel', ['--clipboard', '--input'], linuxOptions); } catch (fallbackError) { const primaryMsg = primaryError instanceof Error