feat(plugin): Add project-aliases plugin

This commit is contained in:
David Vigo 2026-07-07 22:08:11 +02:00
commit 9766bc570c
2 changed files with 385 additions and 0 deletions

View file

@ -0,0 +1,127 @@
# Project Aliases (Zsh Plugin)
A lightweight **Zsh plugin** to manage **per-project shell aliases**.
Aliases are **loaded automatically** when you enter a project folder and **removed automatically** when you leave.
![Project Aliases demo](/assets/project-aliases.svg)
---
## ⚡ Quick Start
```bash
# Inside your project folder
echo "alias run='npm run dev'" > .proj_aliases
echo "alias test='npm test'" >> .proj_aliases
cd ~/my-project
# [project-aliases] Warning: Untrusted .proj_aliases found. Run 'palias allow' to trust and load it.
palias allow
# [project-aliases] Loaded and trusted aliases for project: /Users/username/my-project
run
# Executes: npm run dev
cd ..
# [project-aliases] Project aliases removed
```
---
## ✨ Features
- Define aliases per project in a `.proj_aliases` file.
- **🔒 Security-First**: Requires explicit trust (`palias allow`) before loading a `.proj_aliases` file. If the file is modified externally, the trust is invalidated, preventing silent execution of untrusted code.
- Automatically **load aliases** when entering a trusted project folder.
- Automatically **unload them** when leaving.
- Keep a clean shell environment — no alias pollution between projects.
- Helper commands:
- `palias list` → view currently active project aliases
- `palias edit` → edit currently project aliases
- `palias reload` → reload aliases for the current project without leaving the folder
- `palias allow` → trust and load aliases for the current project
- `palias deny` → untrust and unload aliases for the current project
---
## 📦 Installation
### **Oh My Zsh**
1. Clone the plugin into your Oh My Zsh custom plugins folder:
```bash
git clone https://github.com/dvigo/project-aliases.git ~/.oh-my-zsh/custom/plugins/project-aliases
```
2. Enable it in your `~/.zshrc`:
```bash
plugins=(... project-aliases)
```
3. Reload Zsh:
```bash
source ~/.zshrc
```
---
## 🎥 Demo
TBD
---
## ⚙️ Configuration
No extra configuration is needed.
Simply create a `.proj_aliases` file in the root of any project you want to use aliases in.
**Example `.proj_aliases`**
```bash
alias run="npm run dev"
alias lint="eslint src/"
alias up="docker-compose up -d"
alias down="docker-compose down"
```
---
## 🖊️ Usage
### Available commands:
```bash
palias list # Shows active project aliases
palias edit # Opens the current project's .proj_aliases in your editor
palias reload # Reload aliases from the current project's .proj_aliases
palias allow # Trust and load the current project's .proj_aliases
palias deny # Untrust and unload the current project's .proj_aliases
```
### Example session:
```bash
cd ~/dev/my-api
# [project-aliases] Warning: Untrusted .proj_aliases found. Run 'palias allow' to trust and load it.
palias allow
# [project-aliases] Loaded and trusted aliases for project: /Users/username/dev/my-api
up # runs docker-compose up -d
down # runs docker-compose down
# If you modify .proj_aliases, it becomes untrusted again. Re-run allow to load it:
palias allow
cd ..
# [project-aliases] Project aliases removed
```
---
## 🔧 Roadmap
- [ ] Add support for `.proj_aliases.d/` folder with multiple alias files.
- [ ] Add an option to persist aliases across shells until manually cleared.
- [ ] Add support for project-specific environment variables.
---
## 📜 License
GNU General Public License v3.0 — See [LICENSE](LICENSE) for details.
---

View file

@ -0,0 +1,258 @@
# project-aliases - Zsh plugin to auto-load and clean project-specific aliases from .proj_aliases, keeping your shell organized and project-ready.
# Copyright (C) 2025 David Vigo
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# ===========================
# Project Aliases Plugin
# ===========================
# Loads aliases defined in .proj_aliases of each project when entering its folder.
# Cleans previous aliases when leaving the project.
# ===========================
CURRENT_PROJECT=""
PROJECT_ALIASES_LIST=()
TRUST_DB="${XDG_CONFIG_HOME:-$HOME/.config}/project-aliases/trusted"
# Find project root (contains .proj_aliases or .git)
function find_project_root() {
local dir="$1"
while [[ "$dir" != "/" ]]; do
if [[ -f "$dir/.proj_aliases" || -d "$dir/.git" ]]; then
echo "$dir"
return
fi
dir=$(dirname "$dir")
done
echo ""
}
# Remove aliases from previous project
function unset_project_aliases() {
for alias_name in "${PROJECT_ALIASES_LIST[@]}"; do
unalias "$alias_name" 2>/dev/null
done
PROJECT_ALIASES_LIST=()
}
# Calculate hash of a file portably
function _project_aliases_hash() {
local file="$1"
if (( $+commands[shasum] )); then
shasum -a 256 "$file" 2>/dev/null | awk '{print $1}'
elif (( $+commands[sha256sum] )); then
sha256sum "$file" 2>/dev/null | awk '{print $1}'
elif (( $+commands[md5sum] )); then
md5sum "$file" 2>/dev/null | awk '{print $1}'
elif (( $+commands[md5] )); then
md5 -q "$file" 2>/dev/null
else
# Safe fallback if no hashing command exists (uses file modification time and size)
local stats
stats=$(stat -f "%m%z" "$file" 2>/dev/null || stat -c "%Y%s" "$file" 2>/dev/null || echo "00")
echo "legacy-$stats"
fi
}
# Check if a .proj_aliases file is trusted
function check_project_aliases_trusted() {
local file="$1"
local abs_path
abs_path=$(realpath "$file" 2>/dev/null || readlink -f "$file" 2>/dev/null || echo "$file")
if [[ ! -f "$TRUST_DB" ]]; then
return 1
fi
local current_hash
current_hash=$(_project_aliases_hash "$file")
local line
local found=1
while IFS= read -r line; do
[[ -z "$line" ]] && continue
local entry_hash="${line%% *}"
local entry_path="${line#* }"
# Strip leading/trailing space
entry_path="${entry_path#${entry_path%%[![:space:]]*}}"
entry_path="${entry_path%${entry_path##*[![:space:]]}}"
if [[ "$entry_hash" == "$current_hash" && "$entry_path" == "$abs_path" ]]; then
found=0
break
fi
done < "$TRUST_DB"
return $found
}
# Add current .proj_aliases to trusted database
function allow_project_aliases() {
local file="$CURRENT_PROJECT/.proj_aliases"
local abs_path
abs_path=$(realpath "$file" 2>/dev/null || readlink -f "$file" 2>/dev/null || echo "$file")
local current_hash
current_hash=$(_project_aliases_hash "$file")
mkdir -p "$(dirname "$TRUST_DB")"
if [[ -f "$TRUST_DB" ]]; then
local temp_db="${TRUST_DB}.tmp"
local line
while IFS= read -r line; do
[[ -z "$line" ]] && continue
local entry_path="${line#* }"
entry_path="${entry_path#${entry_path%%[![:space:]]*}}"
entry_path="${entry_path%${entry_path##*[![:space:]]}}"
if [[ "$entry_path" != "$abs_path" ]]; then
echo "$line" >> "$temp_db"
fi
done < "$TRUST_DB"
mv "$temp_db" "$TRUST_DB" 2>/dev/null
fi
echo "${current_hash} ${abs_path}" >> "$TRUST_DB"
}
# Remove current .proj_aliases from trusted database
function deny_project_aliases() {
local file="$CURRENT_PROJECT/.proj_aliases"
local abs_path
abs_path=$(realpath "$file" 2>/dev/null || readlink -f "$file" 2>/dev/null || echo "$file")
if [[ -f "$TRUST_DB" ]]; then
local temp_db="${TRUST_DB}.tmp"
local line
while IFS= read -r line; do
[[ -z "$line" ]] && continue
local entry_path="${line#* }"
entry_path="${entry_path#${entry_path%%[![:space:]]*}}"
entry_path="${entry_path%${entry_path##*[![:space:]]}}"
if [[ "$entry_path" != "$abs_path" ]]; then
echo "$line" >> "$temp_db"
fi
done < "$TRUST_DB"
mv "$temp_db" "$TRUST_DB" 2>/dev/null
fi
}
# Load aliases from a file and store names
function load_project_aliases_file() {
local file="$1"
# Extract alias names from the file
local names=($(grep -oE '^alias[[:space:]]+[a-zA-Z0-9_-]+' "$file" | awk '{print $2}'))
# Source the file to define the aliases
source "$file"
# Store names in PROJECT_ALIASES_LIST
for name in "${names[@]}"; do
PROJECT_ALIASES_LIST+=("$name")
done
}
# Load aliases for the current project
function load_project_aliases() {
local dir=$(pwd)
local root=$(find_project_root "$dir")
# Not in any project -> clean aliases
if [[ -z "$root" ]]; then
if [[ -n "$CURRENT_PROJECT" ]]; then
unset_project_aliases
CURRENT_PROJECT=""
echo "[project-aliases] Project aliases removed."
fi
return
fi
# Changed project -> clean and reload
if [[ "$root" != "$CURRENT_PROJECT" ]]; then
unset_project_aliases
CURRENT_PROJECT="$root"
if [[ -f "$root/.proj_aliases" ]]; then
if check_project_aliases_trusted "$root/.proj_aliases"; then
load_project_aliases_file "$root/.proj_aliases"
echo "[project-aliases] Aliases loaded from $root/.proj_aliases"
else
echo "[project-aliases] Warning: Untrusted .proj_aliases found. Run 'palias allow' to trust and load it."
fi
fi
fi
}
# Command to list, edit, reload, allow, or deny aliases
function palias() {
case "$1" in
list)
if [[ ${#PROJECT_ALIASES_LIST[@]} -eq 0 ]]; then
echo "No active project aliases."
else
echo "Active project aliases:"
for name in "${PROJECT_ALIASES_LIST[@]}"; do
alias "$name"
done
fi
;;
edit)
if [[ -n "$CURRENT_PROJECT" && -f "$CURRENT_PROJECT/.proj_aliases" ]]; then
${EDITOR:-nano} "$CURRENT_PROJECT/.proj_aliases"
else
echo "No .proj_aliases file found in the current project."
fi
;;
reload)
if [[ -n "$CURRENT_PROJECT" && -f "$CURRENT_PROJECT/.proj_aliases" ]]; then
if check_project_aliases_trusted "$CURRENT_PROJECT/.proj_aliases"; then
unset_project_aliases
load_project_aliases_file "$CURRENT_PROJECT/.proj_aliases"
echo "[project-aliases] Aliases reloaded from $CURRENT_PROJECT/.proj_aliases"
else
echo "[project-aliases] Warning: .proj_aliases is not trusted. Run 'palias allow' to trust and load it."
fi
else
echo "[project-aliases] No active project or missing .proj_aliases file."
fi
;;
allow)
if [[ -n "$CURRENT_PROJECT" && -f "$CURRENT_PROJECT/.proj_aliases" ]]; then
allow_project_aliases
unset_project_aliases
load_project_aliases_file "$CURRENT_PROJECT/.proj_aliases"
echo "[project-aliases] Loaded and trusted aliases for project: $CURRENT_PROJECT"
else
echo "[project-aliases] No active project or missing .proj_aliases file."
fi
;;
deny|disallow)
if [[ -n "$CURRENT_PROJECT" && -f "$CURRENT_PROJECT/.proj_aliases" ]]; then
deny_project_aliases
unset_project_aliases
echo "[project-aliases] Untrusted and unloaded aliases for project: $CURRENT_PROJECT"
else
echo "[project-aliases] No active project or missing .proj_aliases file."
fi
;;
*)
echo "Usage: palias [list|edit|reload|allow|deny]"
;;
esac
}
# Hook when changing directory
autoload -U add-zsh-hook
add-zsh-hook chpwd load_project_aliases
# Initial load
load_project_aliases