From 9766bc570c40744f7fa9eaadea497de6906831bf Mon Sep 17 00:00:00 2001 From: David Vigo Date: Tue, 7 Jul 2026 22:08:11 +0200 Subject: [PATCH] feat(plugin): Add project-aliases plugin --- plugins/project-aliases/README.md | 127 +++++++++ .../project-aliases.plugin.zsh | 258 ++++++++++++++++++ 2 files changed, 385 insertions(+) create mode 100644 plugins/project-aliases/README.md create mode 100644 plugins/project-aliases/project-aliases.plugin.zsh diff --git a/plugins/project-aliases/README.md b/plugins/project-aliases/README.md new file mode 100644 index 000000000..734a1aa42 --- /dev/null +++ b/plugins/project-aliases/README.md @@ -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. + +--- \ No newline at end of file diff --git a/plugins/project-aliases/project-aliases.plugin.zsh b/plugins/project-aliases/project-aliases.plugin.zsh new file mode 100644 index 000000000..f17ea37a4 --- /dev/null +++ b/plugins/project-aliases/project-aliases.plugin.zsh @@ -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 . + +# =========================== +# 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