#!/usr/bin/env bash # ============================================================================= # dotfiles_manager.sh — Centralized Dotfiles Management # # Manages shell configs, SSH config, and editor settings via a git repo # hosted on your local Gitea server. # # Remote: http://172.27.0.35:3000/kenjim/dotfiles # Strategy: Files live in ~/dotfiles/, HOME locations are symlinked to them. # # Usage: # ./dotfiles_manager.sh [args] # # Commands: # init Clone remote repo (or init + set remote if new machine) # add Track a file: move it into ~/dotfiles, create symlink back # remove Untrack: restore file to HOME, remove symlink record # install Reapply all symlinks (use on a new machine after cloning) # sync Pull latest from remote, reapply any new symlinks # push [message] Stage all changes, commit, and push to remote # status Show tracked files and their symlink health # list Alias for status # ssh-setup Interactively add SSH config and optionally keys # ssh-export GPG-encrypt private keys → dotfiles/.ssh/keys/*.gpg # ssh-import Decrypt GPG-encrypted keys from dotfiles to ~/.ssh/ # remote-bootstrap SSH into another machine and run full setup # help Show this message # # File layout inside ~/dotfiles/: # .bashrc → symlinked from ~/.bashrc # .bash_profile → symlinked from ~/.bash_profile # .bash_aliases → symlinked from ~/.bash_aliases # .gitconfig → symlinked from ~/.gitconfig # .ssh/config → symlinked from ~/.ssh/config # .ssh/keys/ → encrypted or public-only keys (see ssh-setup) # .vimrc → symlinked from ~/.vimrc # .tmux.conf → symlinked from ~/.tmux.conf # .inputrc → symlinked from ~/.inputrc # .dotfiles_manifest internal list of tracked HOME paths # install.sh portable restore script (run on new machines) # README.md documentation # ============================================================================= set -euo pipefail # ----------------------------------------------------------------------- # CONFIG — override with env vars if needed # ----------------------------------------------------------------------- DOTFILES_DIR="${DOTFILES_DIR:-$HOME/dotfiles}" DOTFILES_REMOTE="${DOTFILES_REMOTE:-http://172.27.0.35:3000/kenjim/dotfiles}" MANIFEST="$DOTFILES_DIR/.dotfiles_manifest" BACKUP_DIR="$HOME/.dotfiles_backup/$(date +%Y%m%d_%H%M%S)" # ----------------------------------------------------------------------- # COLORS # ----------------------------------------------------------------------- RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m' CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' info() { echo -e "${CYAN}[dotfiles]${RESET} $*"; } success() { echo -e "${GREEN}[dotfiles]${RESET} ✓ $*"; } warn() { echo -e "${YELLOW}[dotfiles]${RESET} ⚠ $*"; } error() { echo -e "${RED}[dotfiles]${RESET} ✗ $*" >&2; } bold() { echo -e "${BOLD}$*${RESET}"; } die() { error "$*"; exit 1; } # ----------------------------------------------------------------------- # HELPERS # ----------------------------------------------------------------------- # Resolve the relative path stored in manifest → absolute HOME path manifest_to_home() { # entries are stored relative to HOME, e.g. ".bashrc" or ".ssh/config" echo "$HOME/$1" } # Compute the path inside dotfiles/ for a given HOME path home_to_dotfiles() { local home_path="$1" local rel="${home_path#"$HOME/"}" echo "$DOTFILES_DIR/$rel" } # Add entry to manifest (deduplicates) manifest_add() { local rel="${1#"$HOME/"}" mkdir -p "$(dirname "$MANIFEST")" touch "$MANIFEST" grep -qxF "$rel" "$MANIFEST" || echo "$rel" >> "$MANIFEST" # Keep manifest sorted for clean diffs sort -o "$MANIFEST" "$MANIFEST" } # Remove entry from manifest manifest_remove() { local rel="${1#"$HOME/"}" [ -f "$MANIFEST" ] && sed -i.bak "/^${rel//\//\\/}$/d" "$MANIFEST" && rm -f "${MANIFEST}.bak" } # Check if dotfiles dir is a git repo is_git_repo() { git -C "$DOTFILES_DIR" rev-parse --git-dir &>/dev/null } # Git command scoped to dotfiles dir dgit() { git -C "$DOTFILES_DIR" "$@" } # ----------------------------------------------------------------------- # COMMAND: init # ----------------------------------------------------------------------- cmd_init() { bold "=== Initializing dotfiles repo ===" info "Local path : $DOTFILES_DIR" info "Remote : $DOTFILES_REMOTE" echo # Check if remote is reachable if curl --silent --max-time 5 --output /dev/null "$DOTFILES_REMOTE" 2>/dev/null; then info "Remote server is reachable." else warn "Remote server not reachable right now. Will init locally and set remote for later." fi if [ -d "$DOTFILES_DIR/.git" ]; then warn "Dotfiles repo already exists at $DOTFILES_DIR" # Ensure remote is set correctly if ! dgit remote get-url origin &>/dev/null; then dgit remote add origin "$DOTFILES_REMOTE" success "Remote 'origin' added: $DOTFILES_REMOTE" else local current_remote current_remote=$(dgit remote get-url origin) if [ "$current_remote" != "$DOTFILES_REMOTE" ]; then dgit remote set-url origin "$DOTFILES_REMOTE" success "Remote updated to: $DOTFILES_REMOTE" else info "Remote already set correctly." fi fi return fi # Try to clone first if curl --silent --max-time 5 --output /dev/null "$DOTFILES_REMOTE" 2>/dev/null; then info "Attempting to clone from remote..." if git clone "$DOTFILES_REMOTE" "$DOTFILES_DIR" 2>/dev/null; then success "Cloned from $DOTFILES_REMOTE" info "Run './dotfiles_manager.sh install' to apply symlinks." return else warn "Clone failed (repo may be empty). Initializing locally instead." fi fi # Init fresh repo mkdir -p "$DOTFILES_DIR" cd "$DOTFILES_DIR" git init dgit remote add origin "$DOTFILES_REMOTE" # Create .gitignore — protect private SSH keys by default cat > "$DOTFILES_DIR/.gitignore" <<'GITIGNORE' # macOS .DS_Store .DS_Store? ._* # Backup artifacts *.bak *.orig # SSH private keys — never commit unencrypted private keys # Remove a line below only if you store GPG-encrypted versions .ssh/id_rsa .ssh/id_ed25519 .ssh/id_ecdsa .ssh/id_dsa .ssh/keys/*_rsa .ssh/keys/*_ed25519 .ssh/keys/*_ecdsa .ssh/keys/*.pem # Public keys and config are fine !.ssh/*.pub !.ssh/keys/*.pub !.ssh/config !.ssh/known_hosts # GPG-encrypted private key backups are safe to commit !.ssh/keys/*.gpg # Secrets / tokens — never commit .env .env.* *.token *.secrets vault/ # Machine-local overrides — never commit (written by setup_enterprise_ai_bash.sh) .bashrc.local .bash_profile.local GITIGNORE # Seed README cat > "$DOTFILES_DIR/README.md" < MARKDOWN touch "$MANIFEST" dgit add . dgit commit -m "chore: initial dotfiles skeleton" success "Dotfiles repo initialized at $DOTFILES_DIR" info "Push when ready: ./dotfiles_manager.sh push 'initial commit'" } # ----------------------------------------------------------------------- # COMMAND: add # ----------------------------------------------------------------------- cmd_add() { [ $# -ge 1 ] || die "Usage: add [ ...]" for target in "$@"; do # Expand ~ target="${target/#\~/$HOME}" target="$(realpath -m "$target" 2>/dev/null || echo "$target")" if [ ! -e "$target" ] && [ ! -L "$target" ]; then warn "Skipping '$target': does not exist." continue fi local rel="${target#"$HOME/"}" local dest="$DOTFILES_DIR/$rel" if [ -L "$target" ] && [ "$(readlink "$target")" = "$dest" ]; then info "$rel is already tracked and symlinked." continue fi # Create destination parent directories mkdir -p "$(dirname "$dest")" # Back up existing dotfiles destination if present and not a symlink if [ -e "$dest" ] && [ ! -L "$dest" ]; then mkdir -p "$BACKUP_DIR/$(dirname "$rel")" cp -a "$dest" "$BACKUP_DIR/$rel" warn "Backed up existing $dest → $BACKUP_DIR/$rel" fi # Move file if [ -L "$target" ]; then # If it's already a symlink to somewhere else, copy the contents cp -a "$(readlink "$target")" "$dest" rm "$target" else mv "$target" "$dest" fi # Create symlink ln -sf "$dest" "$target" manifest_add "$target" success "Tracked: $rel\n $target → $dest" done } # ----------------------------------------------------------------------- # COMMAND: remove # ----------------------------------------------------------------------- cmd_remove() { [ $# -ge 1 ] || die "Usage: remove " local target="$1" target="${target/#\~/$HOME}" local rel="${target#"$HOME/"}" local src="$DOTFILES_DIR/$rel" if [ ! -e "$src" ]; then die "Not tracked: $rel" fi if [ -L "$target" ]; then rm "$target" fi cp -a "$src" "$target" manifest_remove "$target" success "Untracked $rel — file restored to $target" info "The copy in $src still exists. Remove it manually if desired." } # ----------------------------------------------------------------------- # COMMAND: install (idempotent — safe to re-run) # ----------------------------------------------------------------------- cmd_install() { bold "=== Applying dotfiles symlinks ===" [ -f "$MANIFEST" ] || { warn "Manifest empty — nothing to install."; return; } local count=0 skipped=0 errors=0 while IFS= read -r rel || [ -n "$rel" ]; do [ -z "$rel" ] && continue local src="$DOTFILES_DIR/$rel" local dest="$HOME/$rel" if [ ! -e "$src" ]; then warn "Missing in dotfiles: $src — skipping." (( errors++ )) || true continue fi mkdir -p "$(dirname "$dest")" if [ -L "$dest" ] && [ "$(readlink "$dest")" = "$src" ]; then (( skipped++ )) || true continue fi # Back up conflicting file if [ -e "$dest" ] && [ ! -L "$dest" ]; then mkdir -p "$BACKUP_DIR/$(dirname "$rel")" cp -a "$dest" "$BACKUP_DIR/$rel" warn "Backed up existing $dest" rm -f "$dest" elif [ -L "$dest" ]; then rm "$dest" fi # Set safe permissions for SSH files if [[ "$rel" == .ssh/* ]]; then if [ -d "$src" ]; then chmod 700 "$src" else chmod 600 "$src" fi fi ln -sf "$src" "$dest" success "Linked: ~/$rel → $src" (( count++ )) || true done < "$MANIFEST" echo bold "Install complete: $count linked, $skipped already up-to-date, $errors errors." # Offer ssh-import if GPG-encrypted keys are present but private key is missing local gpg_count=0 gpg_count=$(find "$DOTFILES_DIR/.ssh/keys" -maxdepth 1 -name '*.gpg' 2>/dev/null | wc -l | tr -d ' ') if [[ "$gpg_count" -gt 0 ]]; then local missing_keys=false for gpg_f in "$DOTFILES_DIR/.ssh/keys/"*.gpg; do local base_name; base_name="$(basename "${gpg_f%.gpg}")" [ ! -f "$HOME/.ssh/$base_name" ] && missing_keys=true && break done if $missing_keys; then echo warn "$gpg_count GPG-encrypted SSH key(s) found in dotfiles but not yet decrypted." read -r -p "Decrypt SSH keys now? (y/n): " _imp [[ "$_imp" == [yY] ]] && cmd_ssh_import fi fi } # ----------------------------------------------------------------------- # COMMAND: sync # ----------------------------------------------------------------------- cmd_sync() { bold "=== Syncing from remote ===" is_git_repo || die "Not a git repo: $DOTFILES_DIR. Run 'init' first." # Stash any local changes so pull is clean local stashed=false if ! dgit diff --quiet || ! dgit diff --cached --quiet; then warn "Local changes detected — stashing before pull." dgit stash push -m "dotfiles_manager auto-stash before sync" stashed=true fi dgit pull --rebase origin main 2>/dev/null || dgit pull --rebase origin master 2>/dev/null || { warn "Could not pull (remote unreachable or branch mismatch). Working offline." } if $stashed; then dgit stash pop || warn "Stash pop had conflicts. Resolve in $DOTFILES_DIR" fi cmd_install success "Sync complete." } # ----------------------------------------------------------------------- # COMMAND: push # ----------------------------------------------------------------------- cmd_push() { local msg="${1:-"chore: update dotfiles $(date '+%Y-%m-%d %H:%M')"}" bold "=== Pushing to remote ===" is_git_repo || die "Not a git repo. Run 'init' first." dgit add --all if dgit diff --cached --quiet; then info "Nothing to commit." else dgit commit -m "$msg" success "Committed: $msg" fi # Determine default branch local branch branch=$(dgit rev-parse --abbrev-ref HEAD) # Set upstream on first push if needed if ! dgit config "branch.$branch.remote" &>/dev/null; then dgit push --set-upstream origin "$branch" else dgit push origin "$branch" fi success "Pushed to $DOTFILES_REMOTE ($branch)." } # ----------------------------------------------------------------------- # COMMAND: status # ----------------------------------------------------------------------- cmd_status() { bold "=== Dotfiles Status ===" info "Repo : $DOTFILES_DIR" info "Remote : $DOTFILES_REMOTE" echo if [ ! -f "$MANIFEST" ] || [ ! -s "$MANIFEST" ]; then echo " (no files tracked yet — use 'add' to start)" return fi printf " %-42s %s\n" "HOME PATH" "STATUS" printf " %-42s %s\n" "-----------------------------------------" "-------" while IFS= read -r rel || [ -n "$rel" ]; do [ -z "$rel" ] && continue local home_path="$HOME/$rel" local src="$DOTFILES_DIR/$rel" local status if [ ! -e "$src" ]; then status="${RED}MISSING in dotfiles${RESET}" elif [ -L "$home_path" ] && [ "$(readlink "$home_path")" = "$src" ]; then status="${GREEN}OK (symlinked)${RESET}" elif [ -e "$home_path" ] && [ ! -L "$home_path" ]; then status="${YELLOW}CONFLICT (real file exists at HOME)${RESET}" elif [ ! -e "$home_path" ]; then status="${YELLOW}NOT LINKED (run install)${RESET}" else status="${YELLOW}STALE SYMLINK${RESET}" fi printf " %-42s " "~/$rel" echo -e "$status" done < "$MANIFEST" echo if is_git_repo; then bold "Git status:" dgit status --short echo local last_commit last_commit=$(dgit log -1 --format="%h %s (%ar)" 2>/dev/null || echo "no commits yet") info "Last commit: $last_commit" fi } # ----------------------------------------------------------------------- # COMMAND: ssh-setup # ----------------------------------------------------------------------- cmd_ssh_setup() { bold "=== SSH Config & Key Management ===" echo warn "SECURITY REMINDER: Private SSH key files are in .gitignore by default." warn "Only ~/.ssh/config, known_hosts, and .pub files will be committed." warn "To store encrypted private keys, use GPG encryption first." echo local ssh_config="$HOME/.ssh/config" local ssh_known="$HOME/.ssh/known_hosts" local dotfiles_ssh="$DOTFILES_DIR/.ssh" mkdir -p "$dotfiles_ssh/keys" chmod 700 "$dotfiles_ssh" # Track ssh/config if [ -f "$ssh_config" ]; then read -r -p "Track ~/.ssh/config in dotfiles? (y/n): " yn if [[ "$yn" == [yY] ]]; then cmd_add "$ssh_config" fi else info "No ~/.ssh/config found. Creating a template..." mkdir -p "$HOME/.ssh" cat > "$dotfiles_ssh/config" <<'SSH_CONFIG' # SSH Client Configuration # Managed by dotfiles_manager.sh # See: man ssh_config Host * ServerAliveInterval 60 ServerAliveCountMax 3 AddKeysToAgent yes IdentitiesOnly yes # Example host alias: # Host myserver # HostName 192.168.1.100 # User kenji # IdentityFile ~/.ssh/id_ed25519 # Port 22 # Local Gitea server Host gitea-local HostName 172.27.0.35 User git Port 22 IdentitiesOnly yes # IdentityFile ~/.ssh/id_ed25519_gitea SSH_CONFIG chmod 600 "$dotfiles_ssh/config" ln -sf "$dotfiles_ssh/config" "$HOME/.ssh/config" manifest_add "$HOME/.ssh/config" success "Created and tracked ~/.ssh/config" fi # Track known_hosts (useful for pre-seeding new machines) if [ -f "$ssh_known" ]; then read -r -p "Track ~/.ssh/known_hosts in dotfiles? (y/n): " yn [[ "$yn" == [yY] ]] && cmd_add "$ssh_known" fi # Track public keys local pub_keys=() while IFS= read -r -d '' f; do pub_keys+=("$f") done < <(find "$HOME/.ssh" -maxdepth 1 -name "*.pub" -print0 2>/dev/null) if [ ${#pub_keys[@]} -gt 0 ]; then echo info "Found public keys:" for k in "${pub_keys[@]}"; do echo " $k"; done read -r -p "Copy public keys to dotfiles/.ssh/keys/? (y/n): " yn if [[ "$yn" == [yY] ]]; then for pubkey in "${pub_keys[@]}"; do cp "$pubkey" "$dotfiles_ssh/keys/" success "Copied: $(basename "$pubkey") → dotfiles/.ssh/keys/" done fi fi # Offer to store encrypted private keys echo warn "Private key storage (advanced — optional):" info "If you want to store encrypted private key backups, use:" echo " gpg --symmetric --cipher-algo AES256 ~/.ssh/id_ed25519" echo " mv ~/.ssh/id_ed25519.gpg ~/dotfiles/.ssh/keys/" echo " Then amend .gitignore to allow the .gpg file." echo success "SSH setup complete." echo info "Next step: GPG-encrypt your private keys for cross-machine sharing:" info " dotfiles ssh-export → encrypt keys into dotfiles/.ssh/keys/*.gpg" info " dotfiles push → commit and push to $DOTFILES_REMOTE" info " dotfiles ssh-import → decrypt on any other machine after sync" } # ----------------------------------------------------------------------- # COMMAND: ssh-export (GPG-encrypt private keys → dotfiles/.ssh/keys/) # ----------------------------------------------------------------------- cmd_ssh_export() { bold "=== Export SSH Private Keys (GPG-encrypted) ===" command -v gpg &>/dev/null || die "gpg not installed. Install with: brew install gnupg" local dotfiles_ssh="$DOTFILES_DIR/.ssh/keys" mkdir -p "$dotfiles_ssh" # Collect private keys (id_* files, no .pub extension, no .gpg) local private_keys=() while IFS= read -r -d '' f; do private_keys+=("$f") done < <(find "$HOME/.ssh" -maxdepth 1 -type f \ \( -name "id_*" ! -name "*.pub" ! -name "*.gpg" \) -print0 2>/dev/null) if [ ${#private_keys[@]} -eq 0 ]; then info "No private keys found in ~/.ssh/" return fi info "Found private keys:" for k in "${private_keys[@]}"; do echo " $k"; done echo warn "Each key will be GPG-encrypted with a symmetric passphrase you choose." warn "Store this passphrase in a password manager — you need it to restore keys." echo read -r -p "Proceed? (y/n): " yn [[ "$yn" == [yY] ]] || return for key in "${private_keys[@]}"; do local key_name; key_name="$(basename "$key")" local encrypted="$dotfiles_ssh/${key_name}.gpg" if [ -f "$encrypted" ]; then read -r -p " $key_name.gpg exists. Re-encrypt? (y/n): " re_enc [[ "$re_enc" == [yY] ]] || continue rm "$encrypted" fi if gpg --symmetric --cipher-algo AES256 --output "$encrypted" "$key"; then success "Encrypted: $key → $encrypted" else warn "Encryption failed for $key_name — skipping." rm -f "$encrypted" fi done # Ensure .gitignore allows .gpg files (idempotent) local gitignore="$DOTFILES_DIR/.gitignore" if ! grep -q "^!.ssh/keys/\*.gpg" "$gitignore" 2>/dev/null; then printf '\n# GPG-encrypted private key backups are safe to commit\n!.ssh/keys/*.gpg\n' >> "$gitignore" success "Updated .gitignore to allow .gpg files" fi echo success "SSH export complete. Run 'push' to save to $DOTFILES_REMOTE" } # ----------------------------------------------------------------------- # COMMAND: ssh-import (decrypt GPG keys from dotfiles/.ssh/keys/ → ~/.ssh/) # ----------------------------------------------------------------------- cmd_ssh_import() { bold "=== Import SSH Private Keys (GPG decrypt) ===" command -v gpg &>/dev/null || die "gpg not installed. Install with: brew install gnupg" local dotfiles_ssh="$DOTFILES_DIR/.ssh/keys" local gpg_keys=() while IFS= read -r -d '' f; do gpg_keys+=("$f") done < <(find "$dotfiles_ssh" -maxdepth 1 -name "*.gpg" -print0 2>/dev/null) if [ ${#gpg_keys[@]} -eq 0 ]; then info "No GPG-encrypted keys found in dotfiles/.ssh/keys/" info "Run 'ssh-export' first to encrypt your private keys." return fi info "Found encrypted keys:" for k in "${gpg_keys[@]}"; do echo " $(basename "$k")"; done echo read -r -p "Decrypt and install to ~/.ssh/? (y/n): " yn [[ "$yn" == [yY] ]] || return mkdir -p "$HOME/.ssh" chmod 700 "$HOME/.ssh" local imported=0 for gpg_key in "${gpg_keys[@]}"; do local key_name; key_name="$(basename "${gpg_key%.gpg}")" local dest="$HOME/.ssh/$key_name" if [ -f "$dest" ]; then warn " $dest already exists — skipping. Remove it first to re-import." continue fi if gpg --decrypt --output "$dest" "$gpg_key"; then chmod 600 "$dest" success "Decrypted: $(basename "$gpg_key") → $dest" (( imported++ )) || true else warn " Failed to decrypt $(basename "$gpg_key")" rm -f "$dest" fi done # Add freshly imported keys to the running ssh-agent if [[ "$imported" -gt 0 ]] && ssh-add -l &>/dev/null 2>&1; then for gpg_key in "${gpg_keys[@]}"; do local key_name; key_name="$(basename "${gpg_key%.gpg}")" local dest="$HOME/.ssh/$key_name" [ -f "$dest" ] && ssh-add "$dest" 2>/dev/null && info " Added to agent: $key_name" done fi echo success "SSH import complete ($imported key(s) decrypted)." } # ----------------------------------------------------------------------- # COMMAND: remote-bootstrap (SSH into another machine and run full setup) # ----------------------------------------------------------------------- cmd_remote_bootstrap() { [ $# -ge 1 ] || die "Usage: remote-bootstrap [--profile work|personal]" local target="$1"; shift local profile="personal" # Parse optional --profile flag while [[ $# -gt 0 ]]; do case "$1" in --profile) profile="${2:-personal}"; shift 2 ;; *) die "Unknown option: $1" ;; esac done bold "=== Remote Bootstrap: $target (profile: $profile) ===" echo # ---- 1. Verify SSH connectivity (prefer key-based auth) ---- info "Testing SSH connectivity..." if ! ssh -o ConnectTimeout=10 -o BatchMode=yes -o StrictHostKeyChecking=accept-new \ "$target" "exit 0" 2>/dev/null; then warn "Key-based SSH auth failed for $target." read -r -p "Copy your SSH public key to $target now? (y/n): " yn if [[ "$yn" == [yY] ]]; then ssh-copy-id "$target" || die "ssh-copy-id failed. Fix SSH access to $target first." else die "SSH key access required. Run:\n ssh-copy-id $target" fi fi success "SSH connection OK." echo # ---- 2. Upload scripts ---- local self_dir; self_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" info "Uploading scripts to $target:~/scripts/ ..." ssh "$target" "mkdir -p ~/scripts" scp -q \ "$self_dir/dotfiles_manager.sh" \ "$self_dir/setup_enterprise_ai_bash.sh" \ "$target:~/scripts/" ssh "$target" "chmod +x ~/scripts/dotfiles_manager.sh ~/scripts/setup_enterprise_ai_bash.sh" success "Scripts uploaded." echo # ---- 3. Run setup interactively over SSH (-t allocates a tty) ---- info "Launching setup on $target (profile=$profile)..." info "You will be prompted interactively on the remote machine." echo ssh -t "$target" \ "MACHINE_PROFILE=${profile} DOTFILES_REMOTE=${DOTFILES_REMOTE} bash ~/scripts/setup_enterprise_ai_bash.sh" echo success "Remote bootstrap of $target complete." info "Log in and verify: ssh $target 'dotfiles status'" } # ----------------------------------------------------------------------- # COMMAND: help # ----------------------------------------------------------------------- cmd_help() { cat < [args] ${BOLD}COMMANDS — Core${RESET} init Clone from remote or init locally with remote set add [...] Track file(s): move to ~/dotfiles, symlink back to HOME remove Untrack: restore file to HOME install Reapply all symlinks (use on a new machine after cloning) sync Pull latest from remote, reapply symlinks push [message] Commit all changes and push to $DOTFILES_REMOTE status / list Show tracked files and symlink health ${BOLD}COMMANDS — SSH & Keys${RESET} ssh-setup Guided SSH config + key migration ssh-export GPG-encrypt private keys → dotfiles/.ssh/keys/*.gpg ssh-import Decrypt GPG-encrypted keys from dotfiles to ~/.ssh/ ${BOLD}COMMANDS — Multi-machine${RESET} remote-bootstrap [--profile work|personal] Upload scripts and run full setup on a remote machine ${BOLD}QUICK START — this machine (work)${RESET} ./dotfiles_manager.sh init ./dotfiles_manager.sh add ~/.bashrc ~/.bash_profile ~/.gitconfig ./dotfiles_manager.sh ssh-setup ./dotfiles_manager.sh ssh-export # GPG-encrypt private keys ./dotfiles_manager.sh push "initial migration" ${BOLD}QUICK START — personal machine restore${RESET} # Option A: remote bootstrap from work machine ./dotfiles_manager.sh remote-bootstrap user@personal-mac --profile personal # Option B: manual steps on the personal machine git clone $DOTFILES_REMOTE ~/dotfiles ~/dotfiles/install.sh # applies symlinks, prompts for key decrypt ${BOLD}.bashrc STRATEGY${RESET} ~/.bashrc → tracked in dotfiles, shared across all machines ~/.bashrc.local → NOT tracked, written by setup_enterprise_ai_bash.sh contains profile-specific vars (cloud paths, MACHINE_PROFILE) Work: OneDrive paths, CLOUD_ROOT=~/OneDrive Personal: ProtonDrive + Google Drive, CLOUD_ROOT=~/Cloud ${BOLD}SSH KEY SHARING STRATEGY${RESET} ~/.ssh/config → tracked in dotfiles (shared) ~/.ssh/*.pub → copied to dotfiles/.ssh/keys/ (shared) ~/.ssh/id_* → NOT committed plain-text ~/.ssh/id_*.gpg → GPG-encrypted backups committed to dotfiles, decrypted on new machines with 'ssh-import' ${BOLD}ENVIRONMENT OVERRIDES${RESET} DOTFILES_DIR=$DOTFILES_DIR DOTFILES_REMOTE=$DOTFILES_REMOTE HELP } # ----------------------------------------------------------------------- # GENERATE PORTABLE install.sh (regenerated on each push) # ----------------------------------------------------------------------- generate_install_sh() { cat > "$DOTFILES_DIR/install.sh" </dev/null || true ln -sf "\$src" "\$dest" echo " Linked: ~/\$rel" done < "\$MANIFEST" echo echo "✓ Dotfiles symlinks applied." # ---- Machine-local config ---- # Write a minimal .bashrc.local if one does not exist (user edits profile) if [ ! -f "\$HOME/.bashrc.local" ]; then echo echo "Select profile for this machine:" echo " [1] work — OneDrive" echo " [2] personal — ProtonDrive + GoogleDrive" read -r -p "Profile (1/2) [2]: " _choice _profile="personal" [[ "\${_choice:-2}" == "1" ]] && _profile="work" # setup script lives inside dotfiles now: dotfiles/scripts/setup_enterprise_ai_bash.sh if [ -f "\$DOTFILES_DIR/scripts/setup_enterprise_ai_bash.sh" ]; then MACHINE_PROFILE="\$_profile" bash "\$DOTFILES_DIR/scripts/setup_enterprise_ai_bash.sh" else echo " ⚠ Could not find setup_enterprise_ai_bash.sh — create ~/.bashrc.local manually." fi fi # ---- SSH key decrypt ---- gpg_count=\$(find "\$DOTFILES_DIR/.ssh/keys" -maxdepth 1 -name '*.gpg' 2>/dev/null | wc -l | tr -d ' ') if [[ "\$gpg_count" -gt 0 ]]; then echo echo "Found \$gpg_count GPG-encrypted SSH key(s) in dotfiles." read -r -p "Decrypt SSH private keys now? (y/n): " _dec if [[ "\$_dec" == [yY] ]]; then # dotfiles_manager.sh lives inside dotfiles now: dotfiles/scripts/dotfiles_manager.sh bash "\$DOTFILES_DIR/scripts/dotfiles_manager.sh" ssh-import fi fi echo echo "✓ Restore complete. Run: source ~/.bash_profile" INSTALL chmod +x "$DOTFILES_DIR/install.sh" } # ----------------------------------------------------------------------- # ENTRYPOINT # ----------------------------------------------------------------------- main() { local cmd="${1:-help}" shift || true # Auto-generate install.sh when pushing case "$cmd" in init) cmd_init "$@" ;; add) cmd_add "$@" ;; remove) cmd_remove "$@" ;; install) cmd_install "$@" ;; sync) cmd_sync "$@" ;; push) generate_install_sh cmd_push "$@" ;; status|list) cmd_status "$@" ;; ssh-setup) cmd_ssh_setup "$@" ;; ssh-export) cmd_ssh_export "$@" ;; ssh-import) cmd_ssh_import "$@" ;; remote-bootstrap) cmd_remote_bootstrap "$@" ;; help|--help|-h) cmd_help ;; *) error "Unknown command: $cmd" cmd_help exit 1 ;; esac } main "$@"