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()