Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions tests/test_cli_cd.py
Original file line number Diff line number Diff line change
@@ -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
34 changes: 34 additions & 0 deletions tests/test_trukernel.py
Original file line number Diff line number Diff line change
@@ -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)
9 changes: 4 additions & 5 deletions trushell/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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()

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Expand Down
25 changes: 22 additions & 3 deletions trushell/core/trukernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
Loading