diff --git a/README.md b/README.md index ca0519d..ebd8784 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A lightweight, context‑aware shell for developers [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![Python Version](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/) -=========================================================== +--- TruShell is not a full replacement for bash or zsh. It is a small utility shell that sits next to your normal terminal and helps you diff --git a/tests/test_cli_cd.py b/tests/test_cli_cd.py new file mode 100644 index 0000000..4b39cab --- /dev/null +++ b/tests/test_cli_cd.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from pathlib import Path + +from trushell import cli + + +def test_handle_cd_command_changes_directory(tmp_path, monkeypatch, capsys): + monkeypatch.chdir(tmp_path) + target = tmp_path / "dir" + target.mkdir() + + called = False + + def fake_run_external_command(command: str, shell: bool = True, check: bool = False, cwd: str | None = None): + nonlocal called + called = True + return None + + monkeypatch.setattr(cli, "_run_external_command", fake_run_external_command) + + result = cli._handle_cd_command("cd dir") + + assert result is True + assert Path.cwd() == target + assert not called + assert str(target) in capsys.readouterr().out diff --git a/tests/test_trukernel.py b/tests/test_trukernel.py new file mode 100644 index 0000000..7b02e03 --- /dev/null +++ b/tests/test_trukernel.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +import os +from pathlib import Path + +from trushell.core.trukernel import TruKernel + + +def test_execute_entry_handles_trushell_cd_sentinel(tmp_path, monkeypatch): + target = tmp_path / "target" + target.mkdir() + monkeypatch.chdir(tmp_path) + + module_path = tmp_path / "jump_module.py" + module_path.write_text( + "def jump(args):\n" + " return '__TRUSHELL_CD__: ' + args\n", + encoding="utf-8", + ) + + entry = { + "command": "j", + "path": str(module_path), + "function": "jump", + } + + kernel = TruKernel() + try: + result = kernel._execute_entry(entry, args=str(target)) + assert result is True + assert Path(os.getcwd()) == target + finally: + # Reset cwd for the rest of the test session if needed + monkeypatch.chdir(tmp_path) diff --git a/trushell/cli.py b/trushell/cli.py index d0749bf..79beb6a 100644 --- a/trushell/cli.py +++ b/trushell/cli.py @@ -18,10 +18,9 @@ psutil = None from . import __version__ -from .core.trukernel import EXIT_SENTINEL, TruKernel +from .core.trukernel import EXIT_SENTINEL, get_kernel app = typer.Typer(name="trushell", help="TruShell manifest-driven launcher.") -kernel = TruKernel() def app_with_lower() -> None: @@ -30,7 +29,7 @@ def app_with_lower() -> None: sys.argv[1] = sys.argv[1].lower() if sys.argv[1] not in {"--help", "-h", "version"}: raw = " ".join(sys.argv[1:]) - kernel.execute_command(raw) + get_kernel().execute_command(raw) return app() @@ -271,7 +270,7 @@ def _handle_cd_command(raw_command: str) -> bool: try: os.chdir(target) - _run_external_command("ls", shell=True, check=False, cwd=os.getcwd()) + typer.echo(os.getcwd()) except (FileNotFoundError, NotADirectoryError, PermissionError) as error: typer.secho(f"❌ Cannot navigate: {error}", fg=typer.colors.RED) except OSError as error: @@ -299,7 +298,7 @@ def _handle_os_fallback(raw_command: str) -> bool: def run_interactive_shell() -> None: """Persistent REPL loop for the TruShell core.""" - kernel = TruKernel() + kernel = get_kernel() typer.secho("Entering TruShell. Type 'exit' to quit.", fg=typer.colors.CYAN) diff --git a/trushell/core/trukernel.py b/trushell/core/trukernel.py index cd57b6b..9a1929b 100644 --- a/trushell/core/trukernel.py +++ b/trushell/core/trukernel.py @@ -49,9 +49,16 @@ def _load_manifests(self) -> None: plugins = parse_manifest( self._manifest_path("plugins.md"), source="plugin" ) - aliases = parse_manifest( - self._manifest_path("my_command_config.md"), source="alias" - ) + + alias_path = self._manifest_path("my_command_config.md") + if alias_path.exists(): + aliases = parse_manifest(alias_path, source="alias") + else: + self.logger.debug( + "Alias manifest not found, skipping optional manifest: %s", + alias_path, + ) + aliases = [] for entry in builtins: self._register(entry) @@ -141,6 +148,18 @@ def _execute_entry( self.logger.info("Exit command received") return EXIT_SENTINEL + # Special-case TruShell directory jump helper + if isinstance(result, str) and result.startswith("__TRUSHELL_CD__: "): + target_path = result.split(": ", 1)[1].strip() + try: + os.chdir(target_path) + print(os.getcwd()) + return True + except OSError as error: + print(f"cd: {error}") + self.logger.warning("Failed to change directory to %s: %s", target_path, error) + return False + # Only print if the function explicitly returned a value # This prevents the kernel from printing 'None' when functions # perform their own prints and return None.