release: 0.2.2 fix post-install packaging and robustness

This commit is contained in:
2026-04-02 21:35:45 +02:00
parent f7f09b895d
commit 71daf35cbf
8 changed files with 216 additions and 27 deletions

View File

@@ -4,21 +4,33 @@ All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project follows Semantic Versioning.
## [0.1.1] - 2026-03-16
## [0.2.2] - 2026-04-02
### Fixed
- Include `cd_browser_post_install.py` in wheel/sdist so `cd_browser_post_install` works after `pip install`
### Added
- AI logging system
- AI agent workflow framework
- ai-task tooling
## [0.2.1] - 2026-04-02
### Fixed
- Prevent crashes when scanning directories with restricted permissions
- Handle non-interactive post-install runs without failing on EOF
- Fix shell reload message interpolation in post-install output
### Improved
- Terminal navigation
- History mode behavior
- UI scrolling
- Toggle hidden directories with "." key in tree view
- Make post-install shell integration idempotent (avoid duplicate `cd_()` entries)
- Add test coverage for permission errors and post-install flows
- Update default application name to `cd-browser`
## [0.1.0] - 2026-03-14
## [0.2.0] - 2026-04-02
### Added
- Interactive post-install script (`cd_browser_post_install`) for automatic shell integration
- Option to automatically add `cd_()` function to ~/.bashrc or ~/.zshrc during installation
- Improved uninstall instructions in README
### Improved
- Installation UX: clearer setup process with user prompts
- Shell integration: better guidance for enabling `cd_` command
### Added
- Initial release of **cd-browser**

View File

@@ -51,22 +51,16 @@ docs/index.md
Install the project in user mode:
```bash
pip install .
pip install cd-browser
```
For editable local development:
**Important**: After installation, run this to set up the `cd_` command:
```bash
pip install -e '.[dev]'
cd_browser_post_install
```
After installation, the application command is:
```bash
cd_browser
```
When the interactive session exits, the program prints the final selected directory path.
This interactive script will guide you through enabling `cd_` in your shell.
## Shell Integration For `cd_`
@@ -113,7 +107,7 @@ If the project was installed with `pip`, remove it with:
pip uninstall cd-browser
```
If you also added the `cd_` shell wrapper, remove that function from your shell profile and reload the shell configuration.
**Important**: After uninstalling, remove the `cd_()` function from your shell profile (~/.bashrc or ~/.zshrc) to clean up completely.
## Developer Setup

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "cd-browser"
version = "0.1.1"
version = "0.2.2"
description = "A fast keyboard-driven directory navigator for the terminal."
readme = "README.md"
requires-python = ">=3.8"
@@ -28,6 +28,7 @@ dependencies = []
[project.scripts]
cd_browser = "app.main:main"
cd_browser_post_install = "cd_browser_post_install:main"
[project.optional-dependencies]
dev = [
@@ -40,6 +41,7 @@ dev = [
[tool.setuptools]
package-dir = {"" = "src"}
py-modules = ["cd_browser_post_install"]
[tool.setuptools.packages.find]
where = ["src"]

View File

@@ -5,5 +5,5 @@ from dataclasses import dataclass
class AppConfig:
"""Application configuration."""
app_name: str = "Python AI Dev Template"
app_name: str = "cd-browser"
debug: bool = False

View File

@@ -19,8 +19,18 @@ def list_directories(path: Path, show_hidden: bool = False) -> list[DirectoryEnt
directory_path = path.expanduser().resolve()
entries: list[DirectoryEntry] = []
for child in directory_path.iterdir():
if not child.is_dir():
try:
children = list(directory_path.iterdir())
except PermissionError:
return []
for child in children:
try:
is_directory = child.is_dir()
except OSError:
continue
if not is_directory:
continue
if not show_hidden and child.name.startswith("."):
continue
@@ -41,8 +51,18 @@ def has_subdirectories(path: Path, show_hidden: bool = False) -> bool:
directory_path = path.expanduser().resolve()
for child in directory_path.iterdir():
if not child.is_dir():
try:
children = list(directory_path.iterdir())
except PermissionError:
return False
for child in children:
try:
is_directory = child.is_dir()
except OSError:
continue
if not is_directory:
continue
if not show_hidden and child.name.startswith("."):

View File

@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""Post-install script for cd-browser to guide users on shell integration."""
import os
import sys
def _profile_for_shell(shell: str) -> str:
if shell == "zsh":
return "~/.zshrc"
return "~/.bashrc"
def _is_cd_function_present(profile_path: str) -> bool:
if not os.path.exists(profile_path):
return False
with open(profile_path, encoding="utf-8") as profile_file:
return "cd_() {" in profile_file.read()
def main() -> None:
"""Print shell integration instructions after installation."""
print("\n" + "=" * 60)
print("🎉 cd-browser installed successfully!")
print("=" * 60)
print()
print(
"To enable the 'cd_' command, you need to add a function to your shell profile."
)
print()
shell = os.environ.get("SHELL", "").split("/")[-1]
profile = _profile_for_shell(shell)
cd_function = """cd_() {
local target
target="$(cd_browser)" || return
if [ -n "$target" ] && [ -d "$target" ]; then
cd "$target"
fi
}"""
print(f"Copy this to the end of {profile}:")
print()
print(cd_function)
print()
profile_path = os.path.expanduser(profile)
try:
response = (
input("Do you want me to add it automatically? (y/n): ").strip().lower()
)
if response == "y":
if _is_cd_function_present(profile_path):
print(f"\n cd_() already exists in {profile}, no changes made.")
else:
with open(profile_path, "a", encoding="utf-8") as f:
f.write("\n" + cd_function + "\n")
print(f"\n✅ Added to {profile}")
print(f"Run 'source {profile}' or restart your terminal to activate cd_.")
else:
print(f"\nAdd the function to {profile} manually.")
print(f"Then run 'source {profile}' or restart terminal.")
except EOFError:
print(
f"\nNo interactive input detected. Add the function to {profile} manually."
)
print(f"Then run 'source {profile}' or restart terminal.")
except KeyboardInterrupt:
print("\n\nSetup cancelled. You can add the function manually later.")
sys.exit(0)
print("\n" + "=" * 60)
print(
f"For uninstall: after 'pip uninstall cd-browser', remove cd_() from {profile}."
)
print("=" * 60 + "\n")
if __name__ == "__main__":
main()

View File

@@ -44,3 +44,33 @@ def test_list_directories_raises_for_missing_directory(tmp_path: Path) -> None:
with pytest.raises(FileNotFoundError):
list_directories(missing_path)
def test_list_directories_skips_permission_errors_for_children(tmp_path: Path) -> None:
root = tmp_path / "root"
root.mkdir()
allowed = root / "allowed"
blocked = root / "blocked"
allowed.mkdir()
blocked.mkdir()
blocked.chmod(0)
try:
entries = list_directories(root)
finally:
blocked.chmod(0o700)
assert any(entry.name == "allowed" for entry in entries)
def test_has_subdirectories_returns_false_on_permission_error(tmp_path: Path) -> None:
blocked = tmp_path / "blocked"
blocked.mkdir()
blocked.chmod(0)
try:
result = has_subdirectories(blocked)
finally:
blocked.chmod(0o700)
assert result is False

View File

@@ -0,0 +1,47 @@
from pathlib import Path
import pytest
from cd_browser_post_install import main
CD_FUNCTION_SNIPPET = "cd_() {"
def test_post_install_handles_eof_without_crashing(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
monkeypatch.setenv("SHELL", "/bin/zsh")
monkeypatch.setattr(
"builtins.input", lambda _prompt: (_ for _ in ()).throw(EOFError)
)
main()
output = capsys.readouterr().out
assert "No interactive input detected" in output
assert "source ~/.zshrc" in output
def test_post_install_adds_cd_function_once(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
) -> None:
monkeypatch.setenv("SHELL", "/bin/zsh")
monkeypatch.setenv("HOME", str(tmp_path))
responses = iter(["y", "y"])
monkeypatch.setattr("builtins.input", lambda _prompt: next(responses))
main()
main()
profile_path = tmp_path / ".zshrc"
content = profile_path.read_text(encoding="utf-8")
assert content.count(CD_FUNCTION_SNIPPET) == 1
output = capsys.readouterr().out
assert "already exists" in output
assert "source ~/.zshrc" in output