Coverage for src/workstack/core/shell_ops.py: 44%
27 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-19 09:31 -0400
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-19 09:31 -0400
1"""Shell detection and tool availability operations.
3This module provides abstraction over shell-specific operations like detecting
4the current shell and checking if command-line tools are installed. This abstraction
5enables dependency injection for testing without mock.patch.
6"""
8import os
9import shutil
10from abc import ABC, abstractmethod
11from pathlib import Path
14class ShellOps(ABC):
15 """Abstract interface for shell detection and tool availability checks.
17 This abstraction enables testing without mock.patch by making shell
18 operations injectable dependencies.
19 """
21 @abstractmethod
22 def detect_shell(self) -> tuple[str, Path] | None:
23 """Detect current shell and return configuration file path.
25 Returns:
26 Tuple of (shell_name, rc_file_path) or None if shell cannot be detected.
28 Supported shells:
29 - bash: returns ("bash", ~/.bashrc)
30 - zsh: returns ("zsh", ~/.zshrc)
31 - fish: returns ("fish", ~/.config/fish/config.fish)
33 Example:
34 >>> shell_ops = RealShellOps()
35 >>> result = shell_ops.detect_shell()
36 >>> if result:
37 ... shell_name, rc_file = result
38 ... print(f"Detected {shell_name} with rc file at {rc_file}")
39 """
40 ...
42 @abstractmethod
43 def check_tool_installed(self, tool_name: str) -> str | None:
44 """Check if a command-line tool is installed and available in PATH.
46 Args:
47 tool_name: Name of the tool to check (e.g., "gt", "git", "python")
49 Returns:
50 Absolute path to the tool executable if found, None otherwise.
52 Example:
53 >>> shell_ops = RealShellOps()
54 >>> gt_path = shell_ops.check_tool_installed("gt")
55 >>> if gt_path:
56 ... print(f"Graphite found at {gt_path}")
57 """
58 ...
61class RealShellOps(ShellOps):
62 """Production implementation using system environment and PATH."""
64 def detect_shell(self) -> tuple[str, Path] | None:
65 """Detect current shell from SHELL environment variable.
67 Implementation details:
68 - Reads $SHELL environment variable
69 - Extracts shell name from path (e.g., /bin/bash -> bash)
70 - Maps to appropriate RC file location
71 """
72 shell_path = os.environ.get("SHELL", "")
73 if not shell_path:
74 return None
76 shell_name = Path(shell_path).name
78 if shell_name == "bash":
79 rc_file = Path.home() / ".bashrc"
80 return ("bash", rc_file)
81 if shell_name == "zsh":
82 rc_file = Path.home() / ".zshrc"
83 return ("zsh", rc_file)
84 if shell_name == "fish":
85 rc_file = Path.home() / ".config" / "fish" / "config.fish"
86 return ("fish", rc_file)
88 return None
90 def check_tool_installed(self, tool_name: str) -> str | None:
91 """Check if tool is in PATH using shutil.which."""
92 return shutil.which(tool_name)