initial dotfiles migration from kenjim-mbp-work

This commit is contained in:
Kenji Morishige
2026-02-23 12:49:59 -06:00
parent 4460dee13f
commit 96536c640c
17 changed files with 2312 additions and 0 deletions

26
scripts/bootstrap.sh Executable file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# bootstrap.sh — Restore Enterprise AI Environment on this machine
set -euo pipefail
echo "🔄 Restoring Enterprise AI Environment..."
# Reload shell config
# shellcheck disable=SC1091
[ -f ~/.bashrc ] && source ~/.bashrc
[ -f ~/.bash_profile ] && source ~/.bash_profile
echo " WORKSPACE : ${WORKSPACE:-not set}"
echo " DATA_ROOT : ${DATA_ROOT:-not set}"
echo " MODEL_ROOT : ${MODEL_ROOT:-not set}"
echo
# Sync latest dotfiles from git server
if [ -f "$HOME/scripts/dotfiles_manager.sh" ]; then
echo "📦 Syncing dotfiles..."
bash "$HOME/scripts/dotfiles_manager.sh" sync
else
echo "⚠️ dotfiles_manager.sh not found — run setup_enterprise_ai_bash.sh first."
fi
echo
echo "✅ Bootstrap complete."

958
scripts/dotfiles_manager.sh Executable file
View File

@@ -0,0 +1,958 @@
#!/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 <command> [args]
#
# Commands:
# init Clone remote repo (or init + set remote if new machine)
# add <file> Track a file: move it into ~/dotfiles, create symlink back
# remove <file> 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
# dotfiles
Centralized configuration management for $(whoami)@$(hostname -s).
## Remote
\`$DOTFILES_REMOTE\`
## Structure
\`\`\`
dotfiles/
├── .bashrc → ~/.bashrc
├── .bash_profile → ~/.bash_profile
├── .bash_aliases → ~/.bash_aliases
├── .gitconfig → ~/.gitconfig
├── .ssh/
│ ├── config → ~/.ssh/config
│ └── keys/ (public keys + GPG-encrypted private keys)
├── scripts/
│ ├── dotfiles_manager.sh → ~/scripts/dotfiles_manager.sh
│ ├── setup_enterprise_ai_bash.sh → ~/scripts/setup_enterprise_ai_bash.sh
│ └── bootstrap.sh → ~/scripts/bootstrap.sh
├── .vimrc → ~/.vimrc
├── .dotfiles_manifest (list of managed HOME paths)
├── install.sh (new-machine restore script)
└── README.md
\`\`\`
## Quick start — restore on a new machine
\`\`\`bash
git clone $DOTFILES_REMOTE ~/dotfiles
bash ~/dotfiles/install.sh
\`\`\`
## Managed files
<!-- auto-updated by dotfiles_manager.sh status -->
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 <file-or-dir> [<file-or-dir> ...]"
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 <file>"
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 <user@host> [--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 <<HELP
${BOLD}dotfiles_manager.sh${RESET} — Centralized dotfiles management
${BOLD}USAGE${RESET}
./dotfiles_manager.sh <command> [args]
${BOLD}COMMANDS — Core${RESET}
init Clone from remote or init locally with remote set
add <file> [...] Track file(s): move to ~/dotfiles, symlink back to HOME
remove <file> 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 <user@host> [--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" <<INSTALL
#!/usr/bin/env bash
# Portable dotfiles restore script — generated by dotfiles_manager.sh
# Run on a new machine after: git clone $DOTFILES_REMOTE ~/dotfiles
set -euo pipefail
DOTFILES_DIR="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
MANIFEST="\$DOTFILES_DIR/.dotfiles_manifest"
BACKUP_DIR="\$HOME/.dotfiles_backup/\$(date +%Y%m%d_%H%M%S)"
echo "=== Restoring dotfiles from \$DOTFILES_DIR ==="
[ -f "\$MANIFEST" ] || { echo "No manifest found."; exit 1; }
while IFS= read -r rel || [ -n "\$rel" ]; do
[ -z "\$rel" ] && continue
src="\$DOTFILES_DIR/\$rel"
dest="\$HOME/\$rel"
[ -e "\$src" ] || { echo " MISSING: \$src"; continue; }
mkdir -p "\$(dirname "\$dest")"
if [ -e "\$dest" ] && [ ! -L "\$dest" ]; then
mkdir -p "\$BACKUP_DIR/\$(dirname "\$rel")"
cp -a "\$dest" "\$BACKUP_DIR/\$rel"
echo " Backed up: \$dest"
rm -f "\$dest"
elif [ -L "\$dest" ]; then
rm "\$dest"
fi
[[ "\$rel" == .ssh/* ]] && chmod 600 "\$src" 2>/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 "$@"

View File

@@ -0,0 +1,410 @@
#!/usr/bin/env bash
set -euo pipefail
echo "🚀 Setting up Enterprise + AI Development Environment (Bash Edition)"
echo
HOME_DIR="$HOME"
BASHRC="$HOME_DIR/.bashrc"
BASH_PROFILE="$HOME_DIR/.bash_profile"
# Dotfiles remote — your local Gitea server
DOTFILES_REMOTE="http://172.27.0.35:3000/kenjim/dotfiles"
DOTFILES_DIR="$HOME_DIR/dotfiles"
# ------------------------------------------------------
# 0⃣ MACHINE PROFILE SELECTION
# ------------------------------------------------------
# Override: MACHINE_PROFILE=work ./setup_enterprise_ai_bash.sh
# MACHINE_PROFILE=personal ./setup_enterprise_ai_bash.sh
MACHINE_PROFILE="${MACHINE_PROFILE:-}"
if [[ -z "$MACHINE_PROFILE" ]]; then
echo "Select machine profile:"
echo " [1] work — OneDrive cloud, work workspace"
echo " [2] personal — ProtonDrive + GoogleDrive, personal workspace"
echo
read -r -p "Profile (1=work / 2=personal) [2]: " _choice
case "${_choice:-2}" in
1|work) MACHINE_PROFILE="work" ;;
2|personal) MACHINE_PROFILE="personal" ;;
*) MACHINE_PROFILE="personal" ;;
esac
fi
export MACHINE_PROFILE
echo " ▶ Profile: $MACHINE_PROFILE"
echo
# Derive hostname tag used to tag commits
MACHINE_TAG="$(hostname -s)-${MACHINE_PROFILE}"
# ------------------------------------------------------
# 1⃣ CREATE DIRECTORY STRUCTURE
# ------------------------------------------------------
echo "📁 Creating directory structure..."
mkdir -p "$HOME_DIR/workspace/src/"{personal,work,research}
mkdir -p "$HOME_DIR/workspace/"{experiments,notebooks,sandboxes,archive}
mkdir -p "$HOME_DIR/data/"{raw,processed,embeddings,synthetic}
mkdir -p "$HOME_DIR/models/"{huggingface,ollama,fine-tuned}
mkdir -p "$HOME_DIR/infra/"{docker,terraform,scripts}
mkdir -p "$HOME_DIR/ops"
mkdir -p "$HOME_DIR/scripts"
mkdir -p "$HOME_DIR/vault"
mkdir -p "$HOME_DIR/dotfiles"
mkdir -p "$HOME_DIR/dotfiles/.ssh/keys"
# Profile-specific cloud directories
if [[ "$MACHINE_PROFILE" == "work" ]]; then
# OneDrive is managed by the Microsoft OneDrive app and auto-mounts at:
# ~/Library/CloudStorage/OneDrive-<OrgName>/ (modern macOS)
# We create a stable symlink at ~/OneDrive for convenience.
ONEDRIVE_MOUNT=$(find "$HOME_DIR/Library/CloudStorage" -maxdepth 1 -iname 'OneDrive*' -type d 2>/dev/null | head -1 || true)
if [[ -n "$ONEDRIVE_MOUNT" ]]; then
ln -sfn "$ONEDRIVE_MOUNT" "$HOME_DIR/OneDrive" 2>/dev/null || true
echo " Linked ~/OneDrive → $ONEDRIVE_MOUNT"
else
mkdir -p "$HOME_DIR/OneDrive"
echo " Created ~/OneDrive placeholder (link manually once OneDrive app is signed in)"
fi
else
# Personal machine: ProtonDrive + Google Drive
mkdir -p "$HOME_DIR/Cloud/"{ProtonDrive,GoogleDrive}
fi
echo "✅ Directories created."
echo
# ------------------------------------------------------
# 2⃣ ENSURE .bash_profile LOADS .bashrc (macOS FIX)
# ------------------------------------------------------
if ! grep -q "source ~/.bashrc" "$BASH_PROFILE" 2>/dev/null; then
cat <<'EOF' >> "$BASH_PROFILE"
# Load .bashrc if it exists
if [ -f ~/.bashrc ]; then
source ~/.bashrc
fi
EOF
echo "✅ .bash_profile updated to load .bashrc"
fi
# ------------------------------------------------------
# 3⃣ ADD ENTERPRISE AI ENV VARIABLES
# ------------------------------------------------------
if ! grep -q "### ENTERPRISE_AI_ENV ###" "$BASHRC" 2>/dev/null; then
cat <<'EOF' >> "$BASHRC"
### ENTERPRISE_AI_ENV ###
export WORKSPACE="$HOME/workspace"
export DATA_ROOT="$HOME/data"
export MODEL_ROOT="$HOME/models"
# HuggingFace cache location
export HF_HOME="$MODEL_ROOT/huggingface"
# Ollama model location
export OLLAMA_MODELS="$MODEL_ROOT/ollama"
# Convenience aliases
alias ws='cd $WORKSPACE'
alias src='cd $WORKSPACE/src'
alias data='cd $DATA_ROOT'
alias models='cd $MODEL_ROOT'
# Machine-local overrides (cloud paths, work vs personal — not synced via dotfiles)
[ -f ~/.bashrc.local ] && source ~/.bashrc.local
EOF
echo "✅ Environment variables added to .bashrc"
else
# Ensure .bashrc.local sourcing is present even on existing installs
if ! grep -q ".bashrc.local" "$BASHRC" 2>/dev/null; then
cat >> "$BASHRC" <<'LOCALEOF'
# Machine-local overrides (cloud paths, work vs personal — not synced via dotfiles)
[ -f ~/.bashrc.local ] && source ~/.bashrc.local
LOCALEOF
echo "✅ Added .bashrc.local sourcing to existing .bashrc"
fi
echo " Environment already configured."
fi
# ---- Write machine-specific .bashrc.local (never committed to dotfiles) ----
BASHRC_LOCAL="$HOME_DIR/.bashrc.local"
# Preserve any existing custom content below the managed block
LOCAL_CUSTOM=""
if [ -f "$BASHRC_LOCAL" ] && grep -q "### MACHINE_LOCAL_END ###" "$BASHRC_LOCAL" 2>/dev/null; then
LOCAL_CUSTOM=$(awk '/### MACHINE_LOCAL_END ###/{found=1; next} found{print}' "$BASHRC_LOCAL")
fi
cat > "$BASHRC_LOCAL" <<LOCALEOF
### MACHINE_LOCAL — managed by setup_enterprise_ai_bash.sh — DO NOT SYNC ###
# Profile : ${MACHINE_PROFILE}
# Host : $(hostname -s)
# Generated: $(date)
export MACHINE_PROFILE="${MACHINE_PROFILE}"
export MACHINE_HOST="$(hostname -s)"
LOCALEOF
if [[ "$MACHINE_PROFILE" == "work" ]]; then
cat >> "$BASHRC_LOCAL" <<'WORKEOF'
# --- Work / OneDrive ---
export CLOUD_ROOT="$HOME/OneDrive"
export ONEDRIVE_ROOT="$HOME/OneDrive"
alias cloud='cd $CLOUD_ROOT'
alias onedrive='cd $ONEDRIVE_ROOT'
WORKEOF
else
cat >> "$BASHRC_LOCAL" <<'PERSONALEOF'
# --- Personal / ProtonDrive + Google Drive ---
export CLOUD_ROOT="$HOME/Cloud"
export PROTON_ROOT="$HOME/Cloud/ProtonDrive"
export GDRIVE_ROOT="$HOME/Cloud/GoogleDrive"
alias cloud='cd $CLOUD_ROOT'
alias proton='cd $PROTON_ROOT'
alias gdrive='cd $GDRIVE_ROOT'
PERSONALEOF
fi
cat >> "$BASHRC_LOCAL" <<'TAILEOF'
### MACHINE_LOCAL_END ###
TAILEOF
# Re-append any custom content that was below the managed block
if [[ -n "$LOCAL_CUSTOM" ]]; then
echo "$LOCAL_CUSTOM" >> "$BASHRC_LOCAL"
fi
echo "✅ .bashrc.local written for profile: $MACHINE_PROFILE"
# ------------------------------------------------------
# 4⃣ INITIALIZE DOTFILES REPO (with Gitea remote)
# ------------------------------------------------------
echo "📦 Setting up dotfiles repo..."
# Copy the dotfiles manager into scripts/ if it isn't already there
SCRIPTS_DIR="$HOME_DIR/scripts"
DFM="$SCRIPTS_DIR/dotfiles_manager.sh"
if [ ! -f "$DFM" ]; then
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -f "$SELF_DIR/dotfiles_manager.sh" ]; then
cp "$SELF_DIR/dotfiles_manager.sh" "$DFM"
chmod +x "$DFM"
echo "✅ dotfiles_manager.sh copied to $DFM"
else
echo "⚠️ dotfiles_manager.sh not found next to this script — skipping copy."
fi
fi
# Run init via the manager (handles clone-or-init and remote setup)
if [ -f "$DFM" ]; then
DOTFILES_DIR="$DOTFILES_DIR" DOTFILES_REMOTE="$DOTFILES_REMOTE" bash "$DFM" init
else
# Minimal fallback if manager isn't available
cd "$DOTFILES_DIR"
if [ ! -d ".git" ]; then
git init
git remote add origin "$DOTFILES_REMOTE"
echo ".DS_Store" > .gitignore
touch README.md
git add .
git commit -m "Initial dotfiles commit"
echo "✅ Dotfiles repo initialized."
else
echo " Dotfiles repo already exists."
fi
fi
echo
# ------------------------------------------------------
# 5⃣ ADD DOTFILES ALIASES TO .bashrc
# ------------------------------------------------------
if ! grep -q "### DOTFILES_ALIASES ###" "$BASHRC" 2>/dev/null; then
cat >> "$BASHRC" <<BASHEOF
### DOTFILES_ALIASES ###
export DOTFILES_DIR="\$HOME/dotfiles"
export DOTFILES_REMOTE="$DOTFILES_REMOTE"
# Dotfiles manager shortcut
alias dotfiles='bash \$HOME/scripts/dotfiles_manager.sh'
alias dot='bash \$HOME/scripts/dotfiles_manager.sh'
# Quick sync alias
alias dots-sync='bash \$HOME/scripts/dotfiles_manager.sh sync'
alias dots-push='bash \$HOME/scripts/dotfiles_manager.sh push'
alias dots-status='bash \$HOME/scripts/dotfiles_manager.sh status'
BASHEOF
echo "✅ Dotfiles aliases added to .bashrc"
else
echo " Dotfiles aliases already configured."
fi
# ------------------------------------------------------
# 6⃣ CREATE MACHINE BOOTSTRAP SCRIPT
# ------------------------------------------------------
cat <<'EOF' > "$HOME_DIR/scripts/bootstrap.sh"
#!/usr/bin/env bash
# bootstrap.sh — Restore Enterprise AI Environment on this machine
set -euo pipefail
echo "🔄 Restoring Enterprise AI Environment..."
# Reload shell config
# shellcheck disable=SC1091
[ -f ~/.bashrc ] && source ~/.bashrc
[ -f ~/.bash_profile ] && source ~/.bash_profile
echo " WORKSPACE : ${WORKSPACE:-not set}"
echo " DATA_ROOT : ${DATA_ROOT:-not set}"
echo " MODEL_ROOT : ${MODEL_ROOT:-not set}"
echo
# Sync latest dotfiles from git server
if [ -f "$HOME/scripts/dotfiles_manager.sh" ]; then
echo "📦 Syncing dotfiles..."
bash "$HOME/scripts/dotfiles_manager.sh" sync
else
echo "⚠️ dotfiles_manager.sh not found — run setup_enterprise_ai_bash.sh first."
fi
echo
echo "✅ Bootstrap complete."
EOF
chmod +x "$HOME_DIR/scripts/bootstrap.sh"
echo "✅ Bootstrap script created at ~/scripts/bootstrap.sh"
# ------------------------------------------------------
# 7⃣ INITIAL DOTFILES MIGRATION (interactive)
# ------------------------------------------------------
echo
read -p "Migrate critical shell & config files into dotfiles repo now? (y/n): " MIGRATE_NOW
if [[ "$MIGRATE_NOW" == "y" ]] && [ -f "$DFM" ]; then
echo
echo "📂 Tracking shell config files..."
# .bashrc.local is machine-specific — never track it in dotfiles
TRACK_FILES=()
[ -f "$BASHRC" ] && TRACK_FILES+=("$BASHRC")
[ -f "$BASH_PROFILE" ] && TRACK_FILES+=("$BASH_PROFILE")
[ -f "$HOME_DIR/.bash_aliases" ] && TRACK_FILES+=("$HOME_DIR/.bash_aliases")
[ -f "$HOME_DIR/.inputrc" ] && TRACK_FILES+=("$HOME_DIR/.inputrc")
[ -f "$HOME_DIR/.gitconfig" ] && TRACK_FILES+=("$HOME_DIR/.gitconfig")
[ -f "$HOME_DIR/.vimrc" ] && TRACK_FILES+=("$HOME_DIR/.vimrc")
[ -f "$HOME_DIR/.tmux.conf" ] && TRACK_FILES+=("$HOME_DIR/.tmux.conf")
# Scripts — track so they are part of the dotfiles repo and re-bootstrap any machine
[ -f "$SCRIPTS_DIR/dotfiles_manager.sh" ] && TRACK_FILES+=("$SCRIPTS_DIR/dotfiles_manager.sh")
[ -f "$SCRIPTS_DIR/setup_enterprise_ai_bash.sh" ] && TRACK_FILES+=("$SCRIPTS_DIR/setup_enterprise_ai_bash.sh")
[ -f "$SCRIPTS_DIR/bootstrap.sh" ] && TRACK_FILES+=("$SCRIPTS_DIR/bootstrap.sh")
# NOTE: ~/.bashrc.local intentionally excluded — it is machine-specific
if [ ${#TRACK_FILES[@]} -gt 0 ]; then
DOTFILES_DIR="$DOTFILES_DIR" DOTFILES_REMOTE="$DOTFILES_REMOTE" \
bash "$DFM" add "${TRACK_FILES[@]}"
else
echo " No standard config files found to track."
fi
echo
read -p "Set up SSH config and keys now? (y/n): " DO_SSH
if [[ "$DO_SSH" == "y" ]]; then
DOTFILES_DIR="$DOTFILES_DIR" DOTFILES_REMOTE="$DOTFILES_REMOTE" \
bash "$DFM" ssh-setup
echo
read -p "GPG-encrypt and store private SSH keys in dotfiles? (y/n): " DO_EXPORT
if [[ "$DO_EXPORT" == "y" ]]; then
DOTFILES_DIR="$DOTFILES_DIR" DOTFILES_REMOTE="$DOTFILES_REMOTE" \
bash "$DFM" ssh-export
fi
fi
echo
read -p "Push initial dotfiles to $DOTFILES_REMOTE now? (y/n): " DO_PUSH
if [[ "$DO_PUSH" == "y" ]]; then
DOTFILES_DIR="$DOTFILES_DIR" DOTFILES_REMOTE="$DOTFILES_REMOTE" \
bash "$DFM" push "initial dotfiles migration from ${MACHINE_TAG}"
fi
else
echo " Skipped. Run manually:"
echo " dotfiles add ~/.bashrc ~/.bash_profile ~/.gitconfig"
echo " dotfiles ssh-setup"
echo " dotfiles push"
fi
# ------------------------------------------------------
# 8⃣ OPTIONAL TIME MACHINE EXCLUSIONS
# ------------------------------------------------------
echo
read -p "Exclude large AI folders from Time Machine? (y/n): " EXCLUDE_TM
if [[ "$EXCLUDE_TM" == "y" ]]; then
sudo tmutil addexclusion "$HOME_DIR/data/raw"
sudo tmutil addexclusion "$HOME_DIR/models"
echo "✅ Time Machine exclusions added."
else
echo " Skipped Time Machine exclusions."
fi
# ------------------------------------------------------
# DONE
# ------------------------------------------------------
# ------------------------------------------------------
# 9⃣ REMOTE BOOTSTRAP (optional)
# ------------------------------------------------------
echo
read -r -p "Bootstrap a remote machine over SSH now? (y/n): " DO_REMOTE
if [[ "$DO_REMOTE" == "y" ]] && [ -f "$DFM" ]; then
read -r -p "Remote target (user@host): " REMOTE_TARGET
read -r -p "Profile for remote machine (work/personal) [personal]: " REMOTE_PROFILE
REMOTE_PROFILE="${REMOTE_PROFILE:-personal}"
DOTFILES_DIR="$DOTFILES_DIR" DOTFILES_REMOTE="$DOTFILES_REMOTE" \
bash "$DFM" remote-bootstrap "$REMOTE_TARGET" --profile "$REMOTE_PROFILE"
else
echo " To bootstrap a remote machine later:"
echo " dotfiles remote-bootstrap user@hostname"
echo " dotfiles remote-bootstrap user@hostname --profile work"
fi
# ------------------------------------------------------
# DONE
# ------------------------------------------------------
echo
echo "🎉 Enterprise + AI Bash Environment Setup Complete!"
echo " Profile : $MACHINE_PROFILE"
echo " Host : $(hostname -s)"
echo
echo "👉 Run : source ~/.bash_profile"
echo "👉 Spaces : ws | src | data | models"
if [[ "$MACHINE_PROFILE" == "work" ]]; then
echo "👉 Cloud : onedrive"
else
echo "👉 Cloud : cloud | proton | gdrive"
fi
echo "👉 Dotfiles: dotfiles status"
echo "👉 Sync : dotfiles sync"
echo "👉 SSH keys: dotfiles ssh-setup (then: dotfiles ssh-export)"
echo "👉 Remote : dotfiles remote-bootstrap user@personal-mac"
echo "👉 Restore : git clone $DOTFILES_REMOTE ~/dotfiles && ~/dotfiles/install.sh"
echo