From 9d632c16edd11c559f2f7a4e0440ee7be2b49212 Mon Sep 17 00:00:00 2001 From: "Aaron D. Lee" Date: Mon, 15 Dec 2025 21:11:15 -0500 Subject: [PATCH] Setup wizard improvments. --- CHANGELOG.md | 8 + README.md | 41 ++++ bin/setup-wizard.sh | 125 ++++++++-- docs/SETUP_GUIDE.md | 83 +++++++ dotfiles.conf | 10 + zsh/.zshrc | 51 ++-- zsh/functions/password-manager.zsh | 363 +++++++++++++++++++++++++++++ 7 files changed, 641 insertions(+), 40 deletions(-) create mode 100644 zsh/functions/password-manager.zsh diff --git a/CHANGELOG.md b/CHANGELOG.md index 50390f0..8ec5f5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,14 +11,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Core Features - **Interactive Setup Wizard** (`setup-wizard.sh`) - Beautiful TUI installer using `gum` with fallback to basic prompts +- **Dynamic MOTD** (`motd.zsh`) - Compact system info on shell start (uptime, CPU, memory, docker, git status) - **Command Palette** (`command-palette.zsh`) - Raycast-style fuzzy launcher triggered by Ctrl+Space or Ctrl+P - **Smart Suggestions** (`smart-suggest.zsh`) - Typo correction for 100+ common mistakes + alias recommendations - **Shell Analytics** (`shell-stats.sh`) - Dashboard showing command usage, suggestions, and activity heatmap - **Secrets Vault** (`vault.sh`) - Encrypted storage for API keys using age/gpg +- **Password Manager Integration** (`password-manager.zsh`) - Unified CLI for 1Password, LastPass, Bitwarden - **Dotfiles Sync** (`dotfiles-sync.sh`) - Multi-machine synchronization with watch mode - **Dotfiles Doctor** (`dotfiles-doctor.sh`) - Health checker with auto-fix capability - **Version Tracking** (`dotfiles-version.sh`) - Compare local vs remote versions +#### Password Manager Support +- 1Password CLI (`op`) installation and integration +- LastPass CLI (`lpass`) installation and integration +- Bitwarden CLI (`bw`) installation and integration +- Unified `pw` command with fuzzy search support + #### Configuration - Centralized `dotfiles.conf` for all settings - Git identity configuration (generated, not hardcoded) diff --git a/README.md b/README.md index 83536ec..3d976e5 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,13 @@ Personal configuration files for a fast, consistent dev environment across Linux | Feature | Description | |---------|-------------| | **Setup Wizard** | Beautiful TUI installer with feature selection | +| **Dynamic MOTD** | System info on shell start (uptime, CPU, memory, updates) | | **Zsh Theme** | Git status, command timer, root detection | | **Command Palette** | Raycast-style fuzzy launcher (Ctrl+Space) | | **Smart Suggestions** | Typo correction + alias recommendations | | **Shell Analytics** | Track command usage, get insights | | **Secrets Vault** | Encrypted storage for API keys | +| **Password Managers** | Unified CLI for 1Password, LastPass, Bitwarden | | **Dotfiles Sync** | Auto-sync across machines | | **Espanso** | 100+ text expansion snippets | | **Snapper** | Btrfs snapshot helpers (Arch/CachyOS) | @@ -184,6 +186,45 @@ eval $(vault shell) # Load all secrets Uses `age` or `gpg` encryption. Secrets auto-load on shell start. +## πŸ”‘ Password Manager Integration + +Unified CLI for 1Password, LastPass, and Bitwarden: + +```bash +pw list # List all items +pw get github # Get password +pw get github username # Get specific field +pw otp github # Get TOTP code +pw copy aws # Copy password to clipboard +pw search mail # Search items +pwf # Fuzzy search + copy (requires fzf) +``` + +Supports: +- **1Password** (`op`) - `INSTALL_1PASSWORD="true"` +- **LastPass** (`lpass`) - `INSTALL_LASTPASS="true"` +- **Bitwarden** (`bw`) - `INSTALL_BITWARDEN="true"` + +## πŸ–₯️ Dynamic MOTD + +System info displayed on shell start: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ ✦ alee@battlestation Mon Dec 15 14:30β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β–² up:4d 7h β—† cpu:12% β—‡ mem:8.2/32G ⊑ 234G free β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β—‰4 containers βŽ‡2 dirty ↑3 updates ●dotfiles:βœ“ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +Configure in `dotfiles.conf`: +```bash +ENABLE_MOTD="true" +MOTD_STYLE="compact" # compact, mini, or off +``` + ## πŸ”„ Dotfiles Sync Keep dotfiles synchronized across machines: diff --git a/bin/setup-wizard.sh b/bin/setup-wizard.sh index 78b3f2b..0c8785d 100755 --- a/bin/setup-wizard.sh +++ b/bin/setup-wizard.sh @@ -312,19 +312,70 @@ step_features() { wizard_header "πŸ“¦ Feature Selection" echo "Select which features to install." + echo "(Use space to select, enter to confirm)" echo local features=( - "zsh-plugins: Autosuggestions + Syntax Highlighting" - "fzf: Fuzzy finder for files and history" - "bat: Better cat with syntax highlighting" - "eza: Modern ls replacement with icons" - "espanso: Text expander (100+ snippets)" - "vault: Encrypted secrets storage" - "sync: Auto-sync dotfiles across machines" + "zsh-plugins" + "fzf" + "bat" + "eza" + "espanso" + "vault" + "motd" + "1password" + "lastpass" + "bitwarden" ) - WIZARD_FEATURES=$(wizard_multichoose "Select features to install:" "${features[@]}") + local descriptions=( + "Autosuggestions + Syntax Highlighting" + "Fuzzy finder for files and history" + "Better cat with syntax highlighting" + "Modern ls replacement" + "Text expander (100+ snippets)" + "Encrypted secrets storage" + "Dynamic system info on startup" + "1Password CLI integration" + "LastPass CLI integration" + "Bitwarden CLI integration" + ) + + if [[ "$HAS_GUM" == true ]]; then + # Build display options + local display_opts=() + for i in "${!features[@]}"; do + display_opts+=("${features[$i]}: ${descriptions[$i]}") + done + + WIZARD_FEATURES=$(printf '%s\n' "${display_opts[@]}" | gum choose --no-limit --height=15 \ + --selected="zsh-plugins: Autosuggestions + Syntax Highlighting,fzf: Fuzzy finder for files and history,vault: Encrypted secrets storage,motd: Dynamic system info on startup") + else + echo "Available features:" + for i in "${!features[@]}"; do + echo " $((i+1)). ${features[$i]}: ${descriptions[$i]}" + done + echo + echo "Enter numbers separated by commas (e.g., 1,2,3) or 'all':" + read -p "> " choices + choices=${choices:-"1,2,5,7"} + + WIZARD_FEATURES="" + if [[ "$choices" == "all" ]]; then + for i in "${!features[@]}"; do + WIZARD_FEATURES+="${features[$i]}: ${descriptions[$i]}"$'\n' + done + else + IFS=',' read -ra selected <<< "$choices" + for idx in "${selected[@]}"; do + idx=$(echo "$idx" | tr -d ' ') + local i=$((idx - 1)) + if [[ $i -ge 0 && $i -lt ${#features[@]} ]]; then + WIZARD_FEATURES+="${features[$i]}: ${descriptions[$i]}"$'\n' + fi + done + fi + fi echo echo -e "${GREEN}βœ“${NC} Features selected" @@ -405,12 +456,27 @@ step_review() { echo "Please review your configuration:" echo + # Format features list - extract just the short names + local features_short="" + local features_list="" + while IFS= read -r feature; do + [[ -z "$feature" ]] && continue + local short_name="${feature%%:*}" + if [[ -z "$features_short" ]]; then + features_short="$short_name" + else + features_short="$features_short, $short_name" + fi + features_list="${features_list} β€’ ${feature}\n" + done <<< "$WIZARD_FEATURES" + if [[ "$HAS_GUM" == true ]]; then gum style \ --border normal \ --border-foreground 240 \ --padding "1 2" \ --margin "0 2" \ + --width 60 \ "Identity:" \ " Name: $WIZARD_NAME" \ " Email: $WIZARD_EMAIL" \ @@ -420,9 +486,13 @@ step_review() { " Branch: $WIZARD_GIT_BRANCH" \ " Editor: $WIZARD_GIT_EDITOR" \ "" \ - "Theme: $WIZARD_THEME" \ - "" \ - "Features: $(echo "$WIZARD_FEATURES" | tr '\n' ', ' | sed 's/,$//')" + "Theme: $WIZARD_THEME" + + echo + echo -e "${CYAN}Selected Features:${NC}" + echo "$WIZARD_FEATURES" | while IFS= read -r feature; do + [[ -n "$feature" ]] && echo -e " ${GREEN}βœ“${NC} ${feature%%:*}" + done else echo " Identity:" echo " Name: $WIZARD_NAME" @@ -435,7 +505,10 @@ step_review() { echo echo " Theme: $WIZARD_THEME" echo - echo " Features: $(echo "$WIZARD_FEATURES" | tr '\n' ', ' | sed 's/,$//')" + echo " Features:" + echo "$WIZARD_FEATURES" | while IFS= read -r feature; do + [[ -n "$feature" ]] && echo " βœ“ ${feature%%:*}" + done fi echo @@ -510,6 +583,11 @@ step_complete() { } generate_config() { + # Helper to check if feature is selected + _has_feature() { + echo "$WIZARD_FEATURES" | grep -qi "^$1:" || echo "$WIZARD_FEATURES" | grep -qi "^$1$" + } + # Generate dotfiles.conf with wizard selections cat > "$DOTFILES_DIR/dotfiles.conf.wizard" << EOF # ============================================================================ @@ -532,21 +610,30 @@ GIT_CREDENTIAL_HELPER="${WIZARD_GIT_CRED}" # --- Feature Toggles --- INSTALL_DEPS="${WIZARD_INSTALL_DEPS}" -INSTALL_ZSH_PLUGINS="$(echo "$WIZARD_FEATURES" | grep -q "zsh-plugins" && echo "true" || echo "false")" -INSTALL_FZF="$(echo "$WIZARD_FEATURES" | grep -q "fzf" && echo "true" || echo "false")" -INSTALL_BAT="$(echo "$WIZARD_FEATURES" | grep -q "bat" && echo "true" || echo "false")" -INSTALL_EZA="$(echo "$WIZARD_FEATURES" | grep -q "eza" && echo "true" || echo "false")" -INSTALL_ESPANSO="$(echo "$WIZARD_FEATURES" | grep -q "espanso" && echo "true" || echo "false")" +INSTALL_ZSH_PLUGINS="$(echo "$WIZARD_FEATURES" | grep -qi "zsh-plugins" && echo "true" || echo "false")" +INSTALL_FZF="$(echo "$WIZARD_FEATURES" | grep -qi "fzf" && echo "true" || echo "false")" +INSTALL_BAT="$(echo "$WIZARD_FEATURES" | grep -qi "bat" && echo "true" || echo "false")" +INSTALL_EZA="$(echo "$WIZARD_FEATURES" | grep -qi "eza" && echo "true" || echo "false")" +INSTALL_ESPANSO="$(echo "$WIZARD_FEATURES" | grep -qi "espanso" && echo "true" || echo "false")" SET_ZSH_DEFAULT="${WIZARD_SET_DEFAULT_SHELL}" +# --- Password Manager Integration --- +INSTALL_1PASSWORD="$(echo "$WIZARD_FEATURES" | grep -qi "1password" && echo "true" || echo "false")" +INSTALL_LASTPASS="$(echo "$WIZARD_FEATURES" | grep -qi "lastpass" && echo "true" || echo "false")" +INSTALL_BITWARDEN="$(echo "$WIZARD_FEATURES" | grep -qi "bitwarden" && echo "true" || echo "false")" + +# --- MOTD --- +ENABLE_MOTD="$(echo "$WIZARD_FEATURES" | grep -qi "motd" && echo "true" || echo "false")" +MOTD_STYLE="compact" + # --- Theme --- ZSH_THEME_NAME="${WIZARD_THEME}" # --- Advanced Features --- ENABLE_SHELL_ANALYTICS="${WIZARD_ANALYTICS}" ENABLE_SMART_SUGGESTIONS="${WIZARD_SUGGESTIONS}" -ENABLE_VAULT="$(echo "$WIZARD_FEATURES" | grep -q "vault" && echo "true" || echo "false")" -ENABLE_SYNC="$(echo "$WIZARD_FEATURES" | grep -q "sync" && echo "true" || echo "false")" +ENABLE_VAULT="$(echo "$WIZARD_FEATURES" | grep -qi "vault" && echo "true" || echo "false")" +ENABLE_COMMAND_PALETTE="true" EOF } diff --git a/docs/SETUP_GUIDE.md b/docs/SETUP_GUIDE.md index 1f6cb5e..11b2327 100644 --- a/docs/SETUP_GUIDE.md +++ b/docs/SETUP_GUIDE.md @@ -295,6 +295,89 @@ dotfiles-sync.sh --log # Show sync history **Auto-check:** On shell start, you'll be notified of available updates. +### Password Manager Integration + +Unified interface for 1Password, LastPass, and Bitwarden: + +```bash +pw list # List all items +pw get # Get password +pw get username # Get specific field +pw otp # Get TOTP/2FA code +pw copy # Copy password to clipboard +pw search # Search items +pw provider # Show which CLI is being used +pw lock # Lock session +``` + +**Interactive selection (requires fzf):** + +```bash +pwf # Fuzzy search items, copy password +pwof # Fuzzy search items, copy OTP +``` + +**Configuration in `dotfiles.conf`:** + +```bash +INSTALL_1PASSWORD="ask" # Install 1Password CLI (op) +INSTALL_LASTPASS="ask" # Install LastPass CLI (lpass) +INSTALL_BITWARDEN="ask" # Install Bitwarden CLI (bw) +PASSWORD_MANAGER="auto" # auto, 1password, lastpass, or bitwarden +``` + +**Manual CLI installation:** + +```bash +# 1Password +brew install --cask 1password-cli # macOS +# See: https://1password.com/downloads/command-line/ + +# LastPass +brew install lastpass-cli # macOS +sudo apt install lastpass-cli # Ubuntu + +# Bitwarden +brew install bitwarden-cli # macOS +npm install -g @bitwarden/cli # Any platform +``` + +### Dynamic MOTD + +System information displayed on shell start: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ ✦ user@hostname Mon Dec 15 14:30β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β–² up:4d 7h β—† cpu:12% β—‡ mem:8.2/32G ⊑ 234G free β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β—‰4 containers βŽ‡2 dirty ↑3 updates ●dotfiles:βœ“ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Shows:** +- Uptime, CPU usage, memory, disk space +- Docker containers running +- Git repos with uncommitted changes +- Available system updates +- Dotfiles sync status + +**Configuration:** + +```bash +ENABLE_MOTD="true" # Enable MOTD +MOTD_STYLE="compact" # compact (box), mini (single line), or off +``` + +**Manual commands:** + +```bash +show_motd # Show compact MOTD +show_motd_mini # Show single-line MOTD +motd # Alias for show_motd +``` + --- ## Customization diff --git a/dotfiles.conf b/dotfiles.conf index 6b712f8..0a34111 100644 --- a/dotfiles.conf +++ b/dotfiles.conf @@ -44,6 +44,16 @@ INSTALL_BAT="ask" INSTALL_EZA="ask" SET_ZSH_DEFAULT="ask" +# --- Password Manager Integration --- +# Enables CLI plugins for password managers (requires respective CLI tools) +INSTALL_1PASSWORD="ask" # 1Password CLI (op) - https://1password.com/downloads/command-line/ +INSTALL_LASTPASS="ask" # LastPass CLI (lpass) - https://github.com/lastpass/lastpass-cli +INSTALL_BITWARDEN="ask" # Bitwarden CLI (bw) - https://bitwarden.com/help/cli/ + +# --- MOTD (Message of the Day) --- +ENABLE_MOTD="true" # Show system info on shell start +MOTD_STYLE="compact" # "compact" (box), "mini" (single line), or "off" + # --- Theme Settings --- ZSH_THEME_NAME="adlee" THEME_TIMER_THRESHOLD=10 # Show elapsed time for commands longer than N seconds diff --git a/zsh/.zshrc b/zsh/.zshrc index f892149..bcb1155 100644 --- a/zsh/.zshrc +++ b/zsh/.zshrc @@ -60,7 +60,7 @@ export EDITOR='vim' export VISUAL='vim' export LANG=en_US.UTF-8 export LC_ALL=en_US.UTF-8 -export PATH="$HOME/.local/bin:$HOME/.dotfiles/bin:$PATH" +export PATH="$HOME/.local/bin:$PATH" # --- Aliases --- @@ -210,25 +210,14 @@ bindkey "^[[3~" delete-char # Delete # --- Custom Widgets --- - reload-zshrc() { - echo -n "Re-sourcing ~/.zshrc ... " - source ~/.zshrc - echo "Done." - _adlee_build_prompt - zle reset-prompt - } - zle -N reload-zshrc - bindkey "^X@s^[^R" reload-zshrc # Ctrl+Super+Alt+R - - grab-fastfetch() { - echo "fastfetch" - fastfetch - _adlee_build_prompt - zle reset-prompt - } - zle -N grab-fastfetch - bindkey "^X@s^[^F" grab-fastfetch # Ctrl+Super+Alt+F - +# Alt+R to reload zsh config +reload-zsh() { + source ~/.zshrc + echo "βœ“ zsh configuration reloaded" + zle reset-prompt +} +zle -N reload-zsh +bindkey "^[r" reload-zsh # Alt+G to show git status git-status-widget() { @@ -330,7 +319,7 @@ fi if [[ "${DOTFILES_AUTO_SYNC_CHECK:-true}" == "true" ]]; then # Quick async check for dotfiles updates - ($HOME/.dotfiles/bin/dotfiles-sync.sh --auto 2>/dev/null &) + (dotfiles-sync.sh --auto 2>/dev/null &) fi # --- Vault Integration --- @@ -340,6 +329,26 @@ if command -v vault.sh &>/dev/null && [[ -f "$HOME/.dotfiles/vault/secrets.enc" eval "$(vault.sh shell 2>/dev/null)" || true fi +# --- Password Manager Integration --- + +if [[ -f "$HOME/.dotfiles/zsh/functions/password-manager.zsh" ]]; then + source "$HOME/.dotfiles/zsh/functions/password-manager.zsh" +fi + +# --- MOTD (Message of the Day) --- + +if [[ -f "$HOME/.dotfiles/zsh/functions/motd.zsh" ]]; then + source "$HOME/.dotfiles/zsh/functions/motd.zsh" + + # Show MOTD based on style setting + case "${MOTD_STYLE:-compact}" in + compact) show_motd ;; + mini) show_motd_mini ;; + off|false|no) ;; + *) show_motd ;; + esac +fi + # --- Local Configuration --- [ -f ~/.zshrc.local ] && source ~/.zshrc.local diff --git a/zsh/functions/password-manager.zsh b/zsh/functions/password-manager.zsh new file mode 100644 index 0000000..d1746c3 --- /dev/null +++ b/zsh/functions/password-manager.zsh @@ -0,0 +1,363 @@ +# ============================================================================ +# Password Manager Integration for Zsh +# ============================================================================ +# Unified interface for 1Password, LastPass, and Bitwarden CLIs +# +# Usage: +# pw list # List items (auto-detects provider) +# pw get # Get password +# pw otp # Get OTP/TOTP code +# pw search # Search items +# pw copy # Copy password to clipboard +# +# Supported: 1Password (op), LastPass (lpass), Bitwarden (bw) +# +# Add to .zshrc: +# source ~/.dotfiles/zsh/functions/password-manager.zsh +# ============================================================================ + +# ============================================================================ +# Configuration +# ============================================================================ + +# Auto-detect preferred password manager (override in dotfiles.conf) +typeset -g PASSWORD_MANAGER="${PASSWORD_MANAGER:-auto}" + +# Session timeout (seconds) - for managers that support it +typeset -g PW_SESSION_TIMEOUT=1800 + +# ============================================================================ +# Provider Detection +# ============================================================================ + +_pw_detect_provider() { + if [[ "$PASSWORD_MANAGER" != "auto" ]]; then + echo "$PASSWORD_MANAGER" + return + fi + + # Auto-detect based on installed CLI + if command -v op &>/dev/null; then + echo "1password" + elif command -v lpass &>/dev/null; then + echo "lastpass" + elif command -v bw &>/dev/null; then + echo "bitwarden" + else + echo "none" + fi +} + +_pw_check_provider() { + local provider=$(_pw_detect_provider) + + if [[ "$provider" == "none" ]]; then + echo "Error: No password manager CLI found" >&2 + echo "Install one of: op (1Password), lpass (LastPass), bw (Bitwarden)" >&2 + return 1 + fi + + echo "$provider" +} + +# ============================================================================ +# 1Password Functions +# ============================================================================ + +_1p_ensure_session() { + # Check if signed in + if ! op account list &>/dev/null 2>&1; then + echo "Signing into 1Password..." >&2 + eval $(op signin) + fi +} + +_1p_list() { + _1p_ensure_session + op item list --format=json | jq -r '.[] | "\(.title)\t\(.category)"' 2>/dev/null || \ + op item list 2>/dev/null +} + +_1p_get() { + local item="$1" + local field="${2:-password}" + _1p_ensure_session + op item get "$item" --fields "$field" 2>/dev/null +} + +_1p_otp() { + local item="$1" + _1p_ensure_session + op item get "$item" --otp 2>/dev/null +} + +_1p_search() { + local query="$1" + _1p_ensure_session + op item list --format=json | jq -r ".[] | select(.title | test(\"$query\"; \"i\")) | .title" 2>/dev/null +} + +# ============================================================================ +# LastPass Functions +# ============================================================================ + +_lp_ensure_session() { + if ! lpass status -q 2>/dev/null; then + echo "Signing into LastPass..." >&2 + lpass login "${LASTPASS_EMAIL:-}" + fi +} + +_lp_list() { + _lp_ensure_session + lpass ls --format="%an\t%ag" 2>/dev/null +} + +_lp_get() { + local item="$1" + local field="${2:-password}" + _lp_ensure_session + + case "$field" in + password) lpass show --password "$item" 2>/dev/null ;; + username) lpass show --username "$item" 2>/dev/null ;; + url) lpass show --url "$item" 2>/dev/null ;; + notes) lpass show --notes "$item" 2>/dev/null ;; + *) lpass show --field="$field" "$item" 2>/dev/null ;; + esac +} + +_lp_otp() { + local item="$1" + _lp_ensure_session + lpass show --otp "$item" 2>/dev/null +} + +_lp_search() { + local query="$1" + _lp_ensure_session + lpass ls 2>/dev/null | grep -i "$query" +} + +# ============================================================================ +# Bitwarden Functions +# ============================================================================ + +_bw_ensure_session() { + # Check if locked + if [[ -z "$BW_SESSION" ]]; then + if bw status 2>/dev/null | grep -q '"status":"locked"'; then + echo "Unlocking Bitwarden..." >&2 + export BW_SESSION=$(bw unlock --raw) + elif bw status 2>/dev/null | grep -q '"status":"unauthenticated"'; then + echo "Signing into Bitwarden..." >&2 + bw login + export BW_SESSION=$(bw unlock --raw) + fi + fi +} + +_bw_list() { + _bw_ensure_session + bw list items --session "$BW_SESSION" 2>/dev/null | jq -r '.[] | "\(.name)\t\(.type)"' +} + +_bw_get() { + local item="$1" + local field="${2:-password}" + _bw_ensure_session + + local item_json=$(bw get item "$item" --session "$BW_SESSION" 2>/dev/null) + + case "$field" in + password) echo "$item_json" | jq -r '.login.password // empty' ;; + username) echo "$item_json" | jq -r '.login.username // empty' ;; + url) echo "$item_json" | jq -r '.login.uris[0].uri // empty' ;; + notes) echo "$item_json" | jq -r '.notes // empty' ;; + *) echo "$item_json" | jq -r ".fields[]? | select(.name==\"$field\") | .value" ;; + esac +} + +_bw_otp() { + local item="$1" + _bw_ensure_session + bw get totp "$item" --session "$BW_SESSION" 2>/dev/null +} + +_bw_search() { + local query="$1" + _bw_ensure_session + bw list items --search "$query" --session "$BW_SESSION" 2>/dev/null | jq -r '.[].name' +} + +# ============================================================================ +# Unified Interface +# ============================================================================ + +pw() { + local cmd="${1:-help}" + shift + + local provider=$(_pw_check_provider) || return 1 + + case "$cmd" in + list|ls|l) + case "$provider" in + 1password) _1p_list ;; + lastpass) _lp_list ;; + bitwarden) _bw_list ;; + esac + ;; + + get|g|show) + local item="$1" + local field="${2:-password}" + [[ -z "$item" ]] && { echo "Usage: pw get [field]"; return 1; } + + case "$provider" in + 1password) _1p_get "$item" "$field" ;; + lastpass) _lp_get "$item" "$field" ;; + bitwarden) _bw_get "$item" "$field" ;; + esac + ;; + + otp|totp|2fa) + local item="$1" + [[ -z "$item" ]] && { echo "Usage: pw otp "; return 1; } + + case "$provider" in + 1password) _1p_otp "$item" ;; + lastpass) _lp_otp "$item" ;; + bitwarden) _bw_otp "$item" ;; + esac + ;; + + search|find|s) + local query="$1" + [[ -z "$query" ]] && { echo "Usage: pw search "; return 1; } + + case "$provider" in + 1password) _1p_search "$query" ;; + lastpass) _lp_search "$query" ;; + bitwarden) _bw_search "$query" ;; + esac + ;; + + copy|cp|c) + local item="$1" + local field="${2:-password}" + [[ -z "$item" ]] && { echo "Usage: pw copy [field]"; return 1; } + + local value + case "$provider" in + 1password) value=$(_1p_get "$item" "$field") ;; + lastpass) value=$(_lp_get "$item" "$field") ;; + bitwarden) value=$(_bw_get "$item" "$field") ;; + esac + + if [[ -n "$value" ]]; then + echo -n "$value" | pbcopy 2>/dev/null || \ + echo -n "$value" | xclip -selection clipboard 2>/dev/null || \ + echo -n "$value" | xsel --clipboard 2>/dev/null || \ + { echo "Could not copy to clipboard"; return 1; } + echo "Copied to clipboard" + else + echo "Item not found or empty" + return 1 + fi + ;; + + provider|which) + echo "Using: $provider" + case "$provider" in + 1password) op --version 2>/dev/null ;; + lastpass) lpass --version 2>/dev/null ;; + bitwarden) bw --version 2>/dev/null ;; + esac + ;; + + lock) + case "$provider" in + 1password) op signout 2>/dev/null ;; + lastpass) lpass logout -f 2>/dev/null ;; + bitwarden) bw lock 2>/dev/null; unset BW_SESSION ;; + esac + echo "Session locked" + ;; + + help|--help|-h|*) + echo "Password Manager CLI (using: $provider)" + echo + echo "Usage: pw [args]" + echo + echo "Commands:" + echo " list List all items" + echo " get [field] Get field (default: password)" + echo " otp Get OTP/TOTP code" + echo " search Search items" + echo " copy [field] Copy to clipboard" + echo " provider Show current provider" + echo " lock Lock/sign out" + echo " help Show this help" + echo + echo "Fields: password, username, url, notes, or custom field name" + echo + echo "Examples:" + echo " pw get github" + echo " pw get github username" + echo " pw otp github" + echo " pw copy aws" + echo " pw search mail" + ;; + esac +} + +# ============================================================================ +# Aliases +# ============================================================================ + +alias pwl='pw list' +alias pwg='pw get' +alias pwc='pw copy' +alias pws='pw search' + +# ============================================================================ +# FZF Integration (if available) +# ============================================================================ + +if command -v fzf &>/dev/null; then + # Interactive password selection + pwf() { + local provider=$(_pw_check_provider) || return 1 + + local item + case "$provider" in + 1password) item=$(_1p_list | fzf --height=40% --reverse | cut -f1) ;; + lastpass) item=$(_lp_list | fzf --height=40% --reverse | cut -f1) ;; + bitwarden) item=$(_bw_list | fzf --height=40% --reverse | cut -f1) ;; + esac + + [[ -n "$item" ]] && pw copy "$item" + } + + # Interactive OTP selection + pwof() { + local provider=$(_pw_check_provider) || return 1 + + local item + case "$provider" in + 1password) item=$(_1p_list | fzf --height=40% --reverse | cut -f1) ;; + lastpass) item=$(_lp_list | fzf --height=40% --reverse | cut -f1) ;; + bitwarden) item=$(_bw_list | fzf --height=40% --reverse | cut -f1) ;; + esac + + if [[ -n "$item" ]]; then + local otp=$(pw otp "$item") + if [[ -n "$otp" ]]; then + echo -n "$otp" | pbcopy 2>/dev/null || \ + echo -n "$otp" | xclip -selection clipboard 2>/dev/null + echo "OTP copied: $otp" + fi + fi + } +fi