diff --git a/src/drd/prompts/instructions.py b/src/drd/prompts/instructions.py index eb10998..e980232 100644 --- a/src/drd/prompts/instructions.py +++ b/src/drd/prompts/instructions.py @@ -80,9 +80,9 @@ def example(): Important guidelines: -1. No files in current directory +1. No files in current directory or if user explicitly tells you to create something in current directory: - During project initialisation: Use `npx create-next-app@latest .` like cmds to create project in the same directory. - - In current directory, do not use 'cd' commands. All operations should be relative to the current directory. + - In such scenario no need to use 'cd' commands. All operations should be relative to the current directory. - Use relative paths for all file operations. 2. When there are files in current directory - You have to initialise a project, you can create a new directory `npx create-docusaurus@latest new-drd-docs`, as soon @@ -118,4 +118,5 @@ def example(): - If the fix involves changes to configuration files, environment variables, or package installations, a restart is likely needed. - If the fix is a simple code change that doesn't affect the server's core functionality or loaded modules, a restart may not be necessary. - When in doubt, err on the side of caution and suggest a restart. +19. When you create new project or new files that are non-existent, never give UPDATE step. """ diff --git a/src/drd/utils/step_executor.py b/src/drd/utils/step_executor.py index d11b89b..1b0b3bf 100644 --- a/src/drd/utils/step_executor.py +++ b/src/drd/utils/step_executor.py @@ -176,9 +176,14 @@ def execute_shell_command(self, command, timeout=300): # 5 minutes timeout click.echo( f"{Fore.YELLOW}Executing shell command: {command}{Style.RESET_ALL}") - if command.strip().startswith(('source ', '.')): + if command.strip().startswith(('cd', 'chdir')): + return self._handle_cd_command(command) + elif command.strip().startswith(('source', '.')): return self._handle_source_command(command) + else: + return self._execute_single_command(command, timeout) + def _execute_single_command(self, command, timeout): try: process = subprocess.Popen( command, @@ -187,6 +192,7 @@ def execute_shell_command(self, command, timeout=300): # 5 minutes timeout stderr=subprocess.PIPE, text=True, env=self.env, + cwd=self.current_dir ) start_time = time.time() @@ -216,7 +222,6 @@ def execute_shell_command(self, command, timeout=300): # 5 minutes timeout print_error(error_message) raise Exception(error_message) - # Update environment variables if the command was successful self._update_env_from_command(command) print_success("Command executed successfully.") @@ -277,3 +282,20 @@ def _update_env_from_command(self, command): # Handle simple assignment key, value = command.split('=', 1) self.env[key.strip()] = value.strip().strip('"\'') + + def _handle_cd_command(self, command): + _, path = command.split(None, 1) + new_dir = os.path.abspath(os.path.join(self.current_dir, path)) + if self.is_safe_path(new_dir): + os.chdir(new_dir) + self.current_dir = new_dir + print_info(f"Changed directory to: {self.current_dir}") + return f"Changed directory to: {self.current_dir}" + else: + print_error(f"Cannot change to directory: {new_dir}") + return f"Failed to change directory to: {new_dir}" + + def reset_directory(self): + os.chdir(self.initial_dir) + self.current_dir = self.initial_dir + print_info(f"Reset directory to: {self.current_dir}") diff --git a/tests/cli/query/test_dynamic_command_handler.py b/tests/cli/query/test_dynamic_command_handler.py index 3707951..6c1f2be 100644 --- a/tests/cli/query/test_dynamic_command_handler.py +++ b/tests/cli/query/test_dynamic_command_handler.py @@ -1,5 +1,5 @@ import unittest -from unittest.mock import patch, MagicMock, call +from unittest.mock import patch, MagicMock, call, mock_open import xml.etree.ElementTree as ET from drd.cli.query.dynamic_command_handler import ( @@ -195,3 +195,76 @@ def test_execute_commands_with_skipped_steps(self, mock_print_debug, mock_print_ call("Completed step 2/3"), call("Completed step 3/3") ]) + + @patch('os.chdir') + @patch('os.path.abspath') + def test_handle_cd_command(self, mock_abspath, mock_chdir): + mock_abspath.return_value = '/fake/path/app' + result = self.executor._handle_cd_command('cd app') + self.assertEqual(result, "Changed directory to: /fake/path/app") + mock_chdir.assert_called_once_with('/fake/path/app') + self.assertEqual(self.executor.current_dir, '/fake/path/app') + + @patch('subprocess.Popen') + def test_execute_single_command(self, mock_popen): + mock_process = MagicMock() + mock_process.poll.side_effect = [None, 0] + mock_process.stdout.readline.return_value = 'output line' + mock_process.communicate.return_value = ('', '') + mock_popen.return_value = mock_process + + result = self.executor._execute_single_command('echo "Hello"', 300) + self.assertEqual(result, 'output line') + mock_popen.assert_called_once_with( + 'echo "Hello"', + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + env=self.executor.env, + cwd=self.executor.current_dir + ) + + @patch('click.confirm') + @patch('os.chdir') + @patch('os.path.abspath') + def test_execute_shell_command_cd(self, mock_abspath, mock_chdir, mock_confirm): + mock_confirm.return_value = True + mock_abspath.return_value = '/fake/path/app' + result = self.executor.execute_shell_command('cd app') + self.assertEqual(result, "Changed directory to: /fake/path/app") + mock_chdir.assert_called_once_with('/fake/path/app') + self.assertEqual(self.executor.current_dir, '/fake/path/app') + + @patch('click.confirm') + @patch('subprocess.Popen') + def test_execute_shell_command_echo(self, mock_popen, mock_confirm): + mock_confirm.return_value = True + mock_process = MagicMock() + mock_process.poll.side_effect = [None, 0] + mock_process.stdout.readline.return_value = 'Hello, World!' + mock_process.communicate.return_value = ('', '') + mock_popen.return_value = mock_process + + result = self.executor.execute_shell_command('echo "Hello, World!"') + self.assertEqual(result, 'Hello, World!') + + @patch('os.path.exists') + @patch('builtins.open', new_callable=mock_open) + @patch('click.confirm') + def test_perform_file_operation_create(self, mock_confirm, mock_file, mock_exists): + mock_exists.return_value = False + mock_confirm.return_value = True + result = self.executor.perform_file_operation( + 'CREATE', 'test.txt', 'content') + self.assertTrue(result) + mock_file.assert_called_with(os.path.join( + self.executor.current_dir, 'test.txt'), 'w') + mock_file().write.assert_called_with('content') + + @patch('os.chdir') + def test_reset_directory(self, mock_chdir): + self.executor.current_dir = '/fake/path/app' + self.executor.reset_directory() + mock_chdir.assert_called_once_with(self.executor.initial_dir) + self.assertEqual(self.executor.current_dir, self.executor.initial_dir)