summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorhritan <[email protected]>2025-08-20 18:42:42 +0000
committerGitHub <[email protected]>2025-08-20 18:42:42 +0000
commitfd64d89da03840802c4d39b01a5915207514d29c (patch)
tree967e9ec8b0d089877dec4cdaf3cc2befb2459679 /packages
parent716297fb320718a233527316783f2338f39028c9 (diff)
fix: copy command gets stuck (#6482)
Co-authored-by: Hriday Taneja <[email protected]>
Diffstat (limited to 'packages')
-rw-r--r--packages/cli/src/ui/utils/commandUtils.test.ts37
-rw-r--r--packages/cli/src/ui/utils/commandUtils.ts30
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<void> => {
- const run = (cmd: string, args: string[]) =>
+ const run = (cmd: string, args: string[], options?: SpawnOptions) =>
new Promise<void>((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<void> => {
),
);
});
- 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<void> => {
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