Plain is headed towards 1.0! Subscribe for development updates โ†’

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