v0.163.0
  1import os
  2import subprocess
  3import sys
  4from functools import cached_property
  5from pathlib import Path
  6
  7import click
  8from plain.runtime import PLAIN_CACHE_PATH
  9
 10
 11class AliasManager:
 12    """Manages the 'p' alias for 'uv run plain'."""
 13
 14    MARKER_FILE = PLAIN_CACHE_PATH / ".alias_prompted"
 15    ALIAS_COMMAND = "uv run plain"
 16    ALIAS_NAME = "p"
 17
 18    @cached_property
 19    def shell(self) -> str | None:
 20        """Detect the current shell."""
 21        shell = os.environ.get("SHELL", "")
 22        if "zsh" in shell:
 23            return "zsh"
 24        elif "bash" in shell:
 25            return "bash"
 26        elif "fish" in shell:
 27            return "fish"
 28        return None
 29
 30    @cached_property
 31    def shell_config_file(self) -> Path | None:
 32        """Get the appropriate shell configuration file."""
 33        home = Path.home()
 34
 35        if self.shell == "zsh":
 36            return home / ".zshrc"
 37        elif self.shell == "bash":
 38            # Check for .bash_aliases first (Ubuntu/Debian convention)
 39            if (home / ".bash_aliases").exists():
 40                return home / ".bash_aliases"
 41            return home / ".bashrc"
 42        elif self.shell == "fish":
 43            return home / ".config" / "fish" / "config.fish"
 44
 45        return None
 46
 47    def _command_exists(self, command: str) -> bool:
 48        """Check if a command exists in the system."""
 49        try:
 50            result = subprocess.run(
 51                ["which", command], capture_output=True, text=True, check=False
 52            )
 53            return result.returncode == 0
 54        except Exception:
 55            return False
 56
 57    def _alias_exists(self) -> bool:
 58        """Check if the 'p' alias already exists."""
 59        # First check if 'p' is already a command
 60        if self._command_exists(self.ALIAS_NAME):
 61            return True
 62
 63        # Check if alias is defined in shell
 64        try:
 65            if not self.shell:
 66                return False
 67            # Try to run the alias to see if it exists
 68            result = subprocess.run(
 69                [self.shell, "-i", "-c", f"alias {self.ALIAS_NAME}"],
 70                capture_output=True,
 71                text=True,
 72                check=False,
 73                timeout=2,
 74            )
 75            return result.returncode == 0
 76        except Exception:
 77            return False
 78
 79    def _add_alias_to_shell(self) -> bool:
 80        """Add the alias to the shell configuration file."""
 81        if not self.shell_config_file or not self.shell_config_file.exists():
 82            return False
 83
 84        alias_line = f'alias {self.ALIAS_NAME}="{self.ALIAS_COMMAND}"'
 85        comment = "# Added by Plain"
 86
 87        # Check if alias already in file
 88        try:
 89            with open(self.shell_config_file) as f:
 90                content = f.read()
 91                if alias_line in content:
 92                    return True
 93        except Exception:
 94            return False
 95
 96        # Add alias to file
 97        try:
 98            with open(self.shell_config_file, "a") as f:
 99                f.write(f"\n{comment}\n{alias_line}\n")
100
101            click.secho(
102                f"✓ Added '{self.ALIAS_NAME}' alias to {self.shell_config_file.name}. Restart your shell!",
103                fg="green",
104            )
105            return True
106        except Exception as e:
107            click.secho(
108                f"Failed to add alias to {self.shell_config_file.name}: {e}", fg="red"
109            )
110            return False
111
112    def check_and_prompt(self) -> None:
113        """Check if alias exists and prompt user to set it up if needed."""
114        # Only suggest if project uses uv (has uv.lock file)
115        if not Path("uv.lock").exists():
116            return
117
118        # Don't prompt if already configured
119        if self._alias_exists():
120            return
121
122        # Don't prompt if we've asked before
123        if self.MARKER_FILE.exists():
124            return
125
126        # Don't prompt for certain commands
127        if "--help" in sys.argv or "-h" in sys.argv:
128            return
129
130        # Mark that we've asked (do this first so we don't ask again even if they Ctrl+C)
131        self.MARKER_FILE.parent.mkdir(parents=True, exist_ok=True)
132        self.MARKER_FILE.touch()
133
134        click.echo()
135        click.secho("💡 Tip: ", fg="yellow", bold=True, nl=False)
136        click.echo(
137            f"Set up `{self.ALIAS_NAME}` as an alias to run commands faster (e.g., `{self.ALIAS_NAME} dev` instead of `uv run plain dev`)."
138        )
139        click.echo()
140
141        # Check if shell is supported
142        if not self.shell or not self.shell_config_file:
143            click.echo("To set this up manually, add to your shell config:")
144            click.echo(f'  alias {self.ALIAS_NAME}="{self.ALIAS_COMMAND}"')
145            click.echo()
146            return
147
148        # Offer to set it up
149        prompt_text = f"Would you like to add this to {self.shell_config_file.name}?"
150        if click.confirm(prompt_text, default=False):
151            click.echo()
152            if self._add_alias_to_shell():
153                sys.exit(0)  # Completely exit
154
155        click.echo()