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