diff --git a/.bash_profile b/.bash_profile deleted file mode 100644 index 4bed812..0000000 --- a/.bash_profile +++ /dev/null @@ -1,5 +0,0 @@ - -# Load .bashrc if it exists -if [ -f ~/.bashrc ]; then - source ~/.bashrc -fi diff --git a/.bash_profile b/.bash_profile new file mode 120000 index 0000000..ebf3398 --- /dev/null +++ b/.bash_profile @@ -0,0 +1 @@ +/home/kenjim/dotfiles/dotfiles/.bash_profile \ No newline at end of file diff --git a/.bashrc b/.bashrc deleted file mode 100644 index 126656a..0000000 --- a/.bashrc +++ /dev/null @@ -1,159 +0,0 @@ -# ~/.bashrc: executed by bash(1) for non-login shells. -# see /usr/share/doc/bash/examples/startup-files (in the package bash-doc) -# for examples - -# If not running interactively, don't do anything -case $- in - *i*) ;; - *) return;; -esac - -# don't put duplicate lines or lines starting with space in the history. -# See bash(1) for more options -HISTCONTROL=ignoreboth - -# append to the history file, don't overwrite it -shopt -s histappend - -# for setting history length see HISTSIZE and HISTFILESIZE in bash(1) -HISTSIZE=1000 -HISTFILESIZE=2000 - -# check the window size after each command and, if necessary, -# update the values of LINES and COLUMNS. -shopt -s checkwinsize - -# If set, the pattern "**" used in a pathname expansion context will -# match all files and zero or more directories and subdirectories. -#shopt -s globstar - -# make less more friendly for non-text input files, see lesspipe(1) -[ -x /usr/bin/lesspipe ] && eval "$(SHELL=/bin/sh lesspipe)" - -# set variable identifying the chroot you work in (used in the prompt below) -if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then - debian_chroot=$(cat /etc/debian_chroot) -fi - -# set a fancy prompt (non-color, unless we know we "want" color) -case "$TERM" in - xterm-color|*-256color) color_prompt=yes;; -esac - -# uncomment for a colored prompt, if the terminal has the capability; turned -# off by default to not distract the user: the focus in a terminal window -# should be on the output of commands, not on the prompt -#force_color_prompt=yes - -if [ -n "$force_color_prompt" ]; then - if [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then - # We have color support; assume it's compliant with Ecma-48 - # (ISO/IEC-6429). (Lack of such support is extremely rare, and such - # a case would tend to support setf rather than setaf.) - color_prompt=yes - else - color_prompt= - fi -fi - -if [ "$color_prompt" = yes ]; then - PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ ' -else - PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ ' -fi -unset color_prompt force_color_prompt - -# If this is an xterm set the title to user@host:dir -case "$TERM" in -xterm*|rxvt*) - PS1="\[\e]0;${debian_chroot:+($debian_chroot)}\u@\h: \w\a\]$PS1" - ;; -*) - ;; -esac - -# enable color support of ls and also add handy aliases -if [ -x /usr/bin/dircolors ]; then - test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)" - alias ls='ls --color=auto' - #alias dir='dir --color=auto' - #alias vdir='vdir --color=auto' - - alias grep='grep --color=auto' - alias fgrep='fgrep --color=auto' - alias egrep='egrep --color=auto' -fi - -# colored GCC warnings and errors -#export GCC_COLORS='error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01' - -# some more ls aliases -alias ll='ls -alF' -alias la='ls -A' -alias l='ls -CF' - -# Add an "alert" alias for long running commands. Use like so: -# sleep 10; alert -alias alert='notify-send --urgency=low -i "$([ $? = 0 ] && echo terminal || echo error)" "$(history|tail -n1|sed -e '\''s/^\s*[0-9]\+\s*//;s/[;&|]\s*alert$//'\'')"' - -# Alias definitions. -# You may want to put all your additions into a separate file like -# ~/.bash_aliases, instead of adding them here directly. -# See /usr/share/doc/bash-doc/examples in the bash-doc package. - -if [ -f ~/.bash_aliases ]; then - . ~/.bash_aliases -fi - -# enable programmable completion features (you don't need to enable -# this, if it's already enabled in /etc/bash.bashrc and /etc/profile -# sources /etc/bash.bashrc). -if ! shopt -oq posix; then - if [ -f /usr/share/bash-completion/bash_completion ]; then - . /usr/share/bash-completion/bash_completion - elif [ -f /etc/bash_completion ]; then - . /etc/bash_completion - fi -fi - -function dbash(){ - docker exec -it $1 bash -} - -# Created by `pipx` on 2025-09-18 03:06:34 -export PATH="$PATH:/home/kenjim/.local/bin" - -### 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 - - -### DOTFILES_ALIASES ### -export DOTFILES_DIR="$HOME/dotfiles" -export DOTFILES_REMOTE="http://172.27.0.35:3000/kenjim/dotfiles" - -# 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' - diff --git a/.bashrc b/.bashrc new file mode 120000 index 0000000..d381d19 --- /dev/null +++ b/.bashrc @@ -0,0 +1 @@ +/home/kenjim/dotfiles/dotfiles/.bashrc \ No newline at end of file diff --git a/.dotfiles_manifest b/.dotfiles_manifest index 32d60c3..98d4871 100644 --- a/.dotfiles_manifest +++ b/.dotfiles_manifest @@ -1,5 +1,11 @@ .bash_profile .bashrc +dotfiles/.bash_profile +dotfiles/.bashrc +dotfiles/.gitconfig +dotfiles/scripts/bootstrap.sh +dotfiles/scripts/dotfiles_manager.sh +dotfiles/scripts/setup_enterprise_ai_bash.sh .gitconfig scripts/bootstrap.sh scripts/dotfiles_manager.sh diff --git a/.gitconfig b/.gitconfig deleted file mode 100644 index 1b6300d..0000000 --- a/.gitconfig +++ /dev/null @@ -1,3 +0,0 @@ -[user] - name = Kenji M - email = kenji@kenjim.com diff --git a/.gitconfig b/.gitconfig new file mode 120000 index 0000000..621b18e --- /dev/null +++ b/.gitconfig @@ -0,0 +1 @@ +/home/kenjim/dotfiles/dotfiles/.gitconfig \ No newline at end of file diff --git a/dotfiles/.bash_profile b/dotfiles/.bash_profile new file mode 100644 index 0000000..4bed812 --- /dev/null +++ b/dotfiles/.bash_profile @@ -0,0 +1,5 @@ + +# Load .bashrc if it exists +if [ -f ~/.bashrc ]; then + source ~/.bashrc +fi diff --git a/dotfiles/.bashrc b/dotfiles/.bashrc new file mode 100644 index 0000000..126656a --- /dev/null +++ b/dotfiles/.bashrc @@ -0,0 +1,159 @@ +# ~/.bashrc: executed by bash(1) for non-login shells. +# see /usr/share/doc/bash/examples/startup-files (in the package bash-doc) +# for examples + +# If not running interactively, don't do anything +case $- in + *i*) ;; + *) return;; +esac + +# don't put duplicate lines or lines starting with space in the history. +# See bash(1) for more options +HISTCONTROL=ignoreboth + +# append to the history file, don't overwrite it +shopt -s histappend + +# for setting history length see HISTSIZE and HISTFILESIZE in bash(1) +HISTSIZE=1000 +HISTFILESIZE=2000 + +# check the window size after each command and, if necessary, +# update the values of LINES and COLUMNS. +shopt -s checkwinsize + +# If set, the pattern "**" used in a pathname expansion context will +# match all files and zero or more directories and subdirectories. +#shopt -s globstar + +# make less more friendly for non-text input files, see lesspipe(1) +[ -x /usr/bin/lesspipe ] && eval "$(SHELL=/bin/sh lesspipe)" + +# set variable identifying the chroot you work in (used in the prompt below) +if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then + debian_chroot=$(cat /etc/debian_chroot) +fi + +# set a fancy prompt (non-color, unless we know we "want" color) +case "$TERM" in + xterm-color|*-256color) color_prompt=yes;; +esac + +# uncomment for a colored prompt, if the terminal has the capability; turned +# off by default to not distract the user: the focus in a terminal window +# should be on the output of commands, not on the prompt +#force_color_prompt=yes + +if [ -n "$force_color_prompt" ]; then + if [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then + # We have color support; assume it's compliant with Ecma-48 + # (ISO/IEC-6429). (Lack of such support is extremely rare, and such + # a case would tend to support setf rather than setaf.) + color_prompt=yes + else + color_prompt= + fi +fi + +if [ "$color_prompt" = yes ]; then + PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ ' +else + PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ ' +fi +unset color_prompt force_color_prompt + +# If this is an xterm set the title to user@host:dir +case "$TERM" in +xterm*|rxvt*) + PS1="\[\e]0;${debian_chroot:+($debian_chroot)}\u@\h: \w\a\]$PS1" + ;; +*) + ;; +esac + +# enable color support of ls and also add handy aliases +if [ -x /usr/bin/dircolors ]; then + test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)" + alias ls='ls --color=auto' + #alias dir='dir --color=auto' + #alias vdir='vdir --color=auto' + + alias grep='grep --color=auto' + alias fgrep='fgrep --color=auto' + alias egrep='egrep --color=auto' +fi + +# colored GCC warnings and errors +#export GCC_COLORS='error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01' + +# some more ls aliases +alias ll='ls -alF' +alias la='ls -A' +alias l='ls -CF' + +# Add an "alert" alias for long running commands. Use like so: +# sleep 10; alert +alias alert='notify-send --urgency=low -i "$([ $? = 0 ] && echo terminal || echo error)" "$(history|tail -n1|sed -e '\''s/^\s*[0-9]\+\s*//;s/[;&|]\s*alert$//'\'')"' + +# Alias definitions. +# You may want to put all your additions into a separate file like +# ~/.bash_aliases, instead of adding them here directly. +# See /usr/share/doc/bash-doc/examples in the bash-doc package. + +if [ -f ~/.bash_aliases ]; then + . ~/.bash_aliases +fi + +# enable programmable completion features (you don't need to enable +# this, if it's already enabled in /etc/bash.bashrc and /etc/profile +# sources /etc/bash.bashrc). +if ! shopt -oq posix; then + if [ -f /usr/share/bash-completion/bash_completion ]; then + . /usr/share/bash-completion/bash_completion + elif [ -f /etc/bash_completion ]; then + . /etc/bash_completion + fi +fi + +function dbash(){ + docker exec -it $1 bash +} + +# Created by `pipx` on 2025-09-18 03:06:34 +export PATH="$PATH:/home/kenjim/.local/bin" + +### 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 + + +### DOTFILES_ALIASES ### +export DOTFILES_DIR="$HOME/dotfiles" +export DOTFILES_REMOTE="http://172.27.0.35:3000/kenjim/dotfiles" + +# 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' + diff --git a/dotfiles/.gitconfig b/dotfiles/.gitconfig new file mode 100644 index 0000000..1b6300d --- /dev/null +++ b/dotfiles/.gitconfig @@ -0,0 +1,3 @@ +[user] + name = Kenji M + email = kenji@kenjim.com diff --git a/dotfiles/scripts/bootstrap.sh b/dotfiles/scripts/bootstrap.sh new file mode 100755 index 0000000..51fa23f --- /dev/null +++ b/dotfiles/scripts/bootstrap.sh @@ -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." diff --git a/dotfiles/scripts/dotfiles_manager.sh b/dotfiles/scripts/dotfiles_manager.sh new file mode 100755 index 0000000..5c714ee --- /dev/null +++ b/dotfiles/scripts/dotfiles_manager.sh @@ -0,0 +1,1328 @@ +#!/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 +# deploy-to SCP tracked dotfiles + scripts to a server (no git needed) +# 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" "$@" +} + +# Credentials file (never committed — chmod 600) +CREDS_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/dotfiles/credentials" + +# Read saved token (returns empty string if not set) +_read_token() { + [ -f "$CREDS_FILE" ] && grep -m1 '^GITEA_TOKEN=' "$CREDS_FILE" | cut -d= -f2- || true +} + +# Inject token into an HTTP(S) remote URL for a single git operation. +# Keeps the stored remote URL clean — only the transient call gets credentials. +_authed_url() { + local url="$1" + local token; token="${GITEA_TOKEN:-$(_read_token)}" + if [[ -n "$token" && "$url" == http* ]]; then + local host_path; host_path="${url#*://}" + host_path="${host_path#*@}" # strip any existing user:pass + echo "${url%%://*}://kenjim:${token}@${host_path}" + else + echo "$url" + fi +} + +# Check gpg is installed with OS-appropriate install hint +_require_gpg() { + command -v gpg &>/dev/null && return 0 + case "$(uname -s)" in + Darwin*) die "gpg not installed. Install with: brew install gnupg" ;; + *) die "gpg not installed. Install with: sudo apt-get install gnupg" ;; + esac +} + +# ----------------------------------------------------------------------- +# 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 + +# Credential store — never commit +.config/dotfiles/credentials +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/directory + if [ -e "$dest" ] && [ ! -L "$dest" ]; then + mkdir -p "$BACKUP_DIR/$(dirname "$rel")" + cp -a "$dest" "$BACKUP_DIR/$rel" + warn "Backed up existing $dest" + # rm -rf needed when $dest is a real directory (e.g. ~/.bashrc.d created manually) + if [ -d "$dest" ]; then rm -rf "$dest"; else rm -f "$dest"; fi + 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 + + # -n: treat dest as a file (don't place link inside) if dest is a dir symlink + ln -sfn "$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." + + local pull_url; pull_url="$(_authed_url "$DOTFILES_REMOTE")" + + # 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 "$pull_url" main 2>/dev/null || \ + dgit pull --rebase "$pull_url" master 2>/dev/null || { + warn "Could not pull (remote unreachable or no token set). Working offline." + warn "Run 'dotfiles auth' to save credentials if push/pull keep failing." + } + + if $stashed; then + dgit stash pop || warn "Stash pop had conflicts. Resolve in $DOTFILES_DIR" + fi + + cmd_install + success "Sync complete." +} + +# ----------------------------------------------------------------------- +# CREDENTIAL HELPERS — cmd_auth is defined further below +# (CREDS_FILE, _read_token, _authed_url are in the HELPERS block above) +# ----------------------------------------------------------------------- + +# ----------------------------------------------------------------------- +# COMMAND: auth — save Gitea credentials once +# ----------------------------------------------------------------------- +cmd_auth() { + bold "=== Gitea Authentication Setup ===" + echo + info "Remote: $DOTFILES_REMOTE" + info "Credentials are saved to $CREDS_FILE (chmod 600, never committed)" + echo + echo "Options:" + echo " [1] Personal Access Token (recommended — Settings → Applications → Generate Token)" + echo " [2] Password (not recommended)" + read -r -p "Choice [1]: " _method + _method="${_method:-1}" + + local token + if [[ "$_method" == "2" ]]; then + read -r -s -p "Gitea password: " token; echo + else + echo + info "To generate a token: $DOTFILES_REMOTE (open in browser)" + info " → Settings (top-right avatar) → Applications → Generate Token" + info " → Name it 'dotfiles', tick Contents+Write, copy the token" + echo + read -r -s -p "Paste token: " token; echo + fi + + [[ -z "$token" ]] && die "No token entered." + + mkdir -p "$(dirname "$CREDS_FILE")" + chmod 700 "$(dirname "$CREDS_FILE")" + printf 'GITEA_TOKEN=%s\n' "$token" > "$CREDS_FILE" + chmod 600 "$CREDS_FILE" + + success "Credentials saved to $CREDS_FILE" + info "Test with: dotfiles push" +} + +# ----------------------------------------------------------------------- +# 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) + + # Build push URL — inject token for HTTP remotes so we never hang on a prompt + local push_url; push_url="$(_authed_url "$DOTFILES_REMOTE")" + if [[ "$push_url" == "$DOTFILES_REMOTE" && "$DOTFILES_REMOTE" == http* ]]; then + warn "No Gitea token found. Run 'dotfiles auth' to save one." + warn "Attempting push anyway (may hang waiting for password)..." + fi + + # Set upstream on first push if needed + if ! dgit config "branch.$branch.remote" &>/dev/null; then + dgit push --set-upstream "$push_url" "$branch" + # Record the clean URL as upstream (not the token-embedded one) + dgit remote set-url origin "$DOTFILES_REMOTE" 2>/dev/null || true + dgit config "branch.$branch.remote" origin + dgit config "branch.$branch.merge" "refs/heads/$branch" + else + dgit push "$push_url" "$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) ===" + _require_gpg + + 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) ===" + _require_gpg + + 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: deploy-to (push dotfiles to a server that can't reach Gitea) +# ----------------------------------------------------------------------- +cmd_deploy_to() { + [ $# -ge 1 ] || die "Usage: deploy-to [--scripts-only] [--include-ssh] [--no-backup] [--run-setup] [--profile work|personal] [--dry-run]" + + local target="$1"; shift + local scripts_only=false + local skip_ssh=true # default: skip .ssh/ — avoid pushing keys/config to servers + local no_backup=false + local run_setup=false + local setup_profile="work" # default profile for servers + local dry_run=false + + while [[ $# -gt 0 ]]; do + case "$1" in + --scripts-only) scripts_only=true; shift ;; + --include-ssh) skip_ssh=false; shift ;; + --no-backup) no_backup=true; shift ;; + --run-setup) run_setup=true; shift ;; + --profile) setup_profile="${2:-work}"; shift 2 ;; + --dry-run) dry_run=true; shift ;; + *) die "Unknown option: $1" ;; + esac + done + + bold "=== Deploy dotfiles → $target ===" + echo + info "Strategy: SCP files directly to their HOME paths (no git required on remote)" + $dry_run && warn "DRY RUN — no files will be transferred." + echo + + # ---- 1. Check SSH ---- + info "Testing SSH connectivity..." + if ! ssh -o ConnectTimeout=10 -o BatchMode=yes -o StrictHostKeyChecking=accept-new \ + "$target" "exit 0" 2>/dev/null; then + die "Cannot reach $target. Ensure SSH key access is set up:\n ssh-copy-id $target" + fi + success "SSH connection OK." + echo + + local self_dir; self_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + local deployed=0 skipped=0 + + # ---- 2. Back up remote files locally before overwriting ---- + # Saved to: ~/.dotfiles_backup/remote--/ + if ! $no_backup && ! $dry_run; then + local remote_host; remote_host="${target##*@}" # strip user@ prefix for dir name + local backup_base="$HOME/.dotfiles_backup/remote-${remote_host}-$(date +%Y%m%d_%H%M%S)" + info "Backing up existing remote files → $backup_base" + + # Build the list of remote paths to fetch: + # - tracked dotfiles from manifest (applying same filters as deploy) + # - remote ~/scripts/ if it exists + local remote_paths=() + if [ -f "$MANIFEST" ] && ! $scripts_only; then + while IFS= read -r rel || [ -n "$rel" ]; do + [ -z "$rel" ] && continue + $skip_ssh && [[ "$rel" == .ssh/* ]] && continue + [[ "$rel" == scripts/* ]] && continue + remote_paths+=("$rel") + done < "$MANIFEST" + fi + + # Always attempt to back up remote scripts/ + remote_paths+=("scripts/dotfiles_manager.sh" "scripts/bootstrap.sh" "scripts/setup_enterprise_ai_bash.sh") + + local backed_up=0 + for rel in "${remote_paths[@]}"; do + # Check if the file/dir actually exists on the remote before fetching + if ssh "$target" "[ -e ~/$rel ]" 2>/dev/null; then + local local_dest="$backup_base/$rel" + mkdir -p "$(dirname "$local_dest")" + # Use -r for directories, plain scp for files + if ssh "$target" "[ -d ~/$rel ]" 2>/dev/null; then + mkdir -p "$local_dest" + if scp -rq "$target:~/$rel/." "$local_dest/" 2>/dev/null; then + (( backed_up++ )) || true + fi + elif scp -q "$target:~/$rel" "$local_dest" 2>/dev/null; then + (( backed_up++ )) || true + fi + fi + done + + if [[ "$backed_up" -gt 0 ]]; then + success "Backed up $backed_up remote file(s) → $backup_base" + else + info "No existing remote files found to back up." + rmdir "$backup_base" 2>/dev/null || true + fi + echo + fi + $no_backup && ! $dry_run && warn "Skipping remote backup (--no-backup set)." + + # ---- 3. Deploy scripts ---- + info "Deploying scripts → $target:~/scripts/ ..." + local script_files=() + for f in \ + "$self_dir/dotfiles_manager.sh" \ + "$self_dir/setup_enterprise_ai_bash.sh" \ + "$self_dir/bootstrap.sh"; do + [ -f "$f" ] && script_files+=("$f") + done + + if [[ ${#script_files[@]} -gt 0 ]]; then + if $dry_run; then + for f in "${script_files[@]}"; do + echo " [dry-run] scp $(basename "$f") → $target:~/scripts/$(basename "$f")" + done + else + ssh "$target" "mkdir -p ~/scripts" + scp -q "${script_files[@]}" "$target:~/scripts/" + ssh "$target" "chmod +x ~/scripts/dotfiles_manager.sh ~/scripts/setup_enterprise_ai_bash.sh ~/scripts/bootstrap.sh 2>/dev/null; true" + (( deployed += ${#script_files[@]} )) || true + success "Scripts deployed (${#script_files[@]} files)." + fi + fi + + $scripts_only && { echo; success "Done (scripts only)."; return; } + + # ---- 4. Deploy tracked dotfiles from manifest ---- + [ -f "$MANIFEST" ] || { warn "No manifest found — only scripts were deployed."; return; } + + echo + info "Deploying tracked dotfiles → $target home directory..." + echo + + while IFS= read -r rel || [ -n "$rel" ]; do + [ -z "$rel" ] && continue + + # Skip .ssh/ by default — avoids pushing your private keys/known_hosts to servers + if $skip_ssh && [[ "$rel" == .ssh/* ]]; then + info "Skipping: ~/$rel (use --include-ssh to override)" + (( skipped++ )) || true + continue + fi + + # Scripts are handled above + [[ "$rel" == scripts/* ]] && continue + + local src="$DOTFILES_DIR/$rel" + if [ ! -e "$src" ]; then + warn "Missing in dotfiles, skipping: $rel" + (( skipped++ )) || true + continue + fi + + if $dry_run; then + echo " [dry-run] ~/$rel$([ -d "$src" ] && echo '/')" + else + local parent; parent="$(dirname "$rel")" + if [ -d "$src" ]; then + # For directories: remove stale dest first (avoids double-nesting on re-run) + # then scp -r into the parent so the dir name is preserved correctly. + ssh "$target" "rm -rf ~/$rel" + if [[ "$parent" == "." ]]; then + scp -rq "$src" "$target:~/" + else + ssh "$target" "mkdir -p ~/$parent" + scp -rq "$src" "$target:~/$parent/" + fi + else + [[ "$parent" != "." ]] && ssh "$target" "mkdir -p ~/$parent" + scp -q "$src" "$target:~/$rel" + fi + success "Deployed: ~/$rel" + (( deployed++ )) || true + fi + done < "$MANIFEST" + + # ---- 5. Deploy hosts/.bashrc.local → ~/.bashrc.local ---- + # Strip user@ prefix, then strip domain suffix to get short hostname + local remote_short; remote_short="${target##*@}" + remote_short="${remote_short%%.*}" + local host_local="$DOTFILES_DIR/hosts/${remote_short}.bashrc.local" + + if [ -f "$host_local" ]; then + echo + info "Found host-specific config: hosts/${remote_short}.bashrc.local" + if $dry_run; then + echo " [dry-run] hosts/${remote_short}.bashrc.local → ~/.bashrc.local" + else + # Back up existing remote .bashrc.local if not already captured above + if $no_backup; then + : # skip + elif ssh "$target" '[ -L ~/.bashrc.local ]' 2>/dev/null; then + # It's a symlink (NFS setup) — back up the target file + local real_path + real_path=$(ssh "$target" 'readlink -f ~/.bashrc.local' 2>/dev/null || true) + if [[ -n "$real_path" ]]; then + local bl_backup="$HOME/.dotfiles_backup/remote-${remote_short}-$(date +%Y%m%d_%H%M%S)" + mkdir -p "$bl_backup" + scp -q "$target:$real_path" "$bl_backup/.bashrc.local" 2>/dev/null || true + info "Backed up remote $real_path → $bl_backup/.bashrc.local" + fi + elif ssh "$target" '[ -f ~/.bashrc.local ]' 2>/dev/null; then + local bl_backup="$HOME/.dotfiles_backup/remote-${remote_short}-$(date +%Y%m%d_%H%M%S)" + mkdir -p "$bl_backup" + scp -q "$target:~/.bashrc.local" "$bl_backup/.bashrc.local" 2>/dev/null || true + info "Backed up remote ~/.bashrc.local → $bl_backup/.bashrc.local" + fi + + # Detect NFS home on the remote and deploy to /opt/kenjim/ if needed + local _remote_nfs=false + if ssh "$target" 'df -P "$HOME" 2>/dev/null | tail -1 | grep -qE ":|nfs"' 2>/dev/null; then + _remote_nfs=true + fi + + if $_remote_nfs; then + info "NFS home detected on $target — deploying to /opt/kenjim/.bashrc.local" + # Ensure /opt/kenjim exists — skip sudo if it already does + if ! ssh "$target" '[ -d /opt/kenjim ]' 2>/dev/null; then + info "Creating /opt/kenjim on $target (requires sudo)..." + ssh -t "$target" 'sudo mkdir -p /opt/kenjim && sudo chown $(id -u):$(id -g) /opt/kenjim && chmod 700 /opt/kenjim' + fi + scp -q "$host_local" "$target:/opt/kenjim/.bashrc.local" + ssh "$target" 'chmod 600 /opt/kenjim/.bashrc.local && ln -sfn /opt/kenjim/.bashrc.local ~/.bashrc.local' + success "Deployed: hosts/${remote_short}.bashrc.local → /opt/kenjim/.bashrc.local (symlinked from ~)" + else + scp -q "$host_local" "$target:~/.bashrc.local" + success "Deployed: hosts/${remote_short}.bashrc.local → ~/.bashrc.local" + fi + (( deployed++ )) || true + fi + else + info "No host-specific config found at hosts/${remote_short}.bashrc.local — skipping." + info "Create one to manage ~/.bashrc.local centrally: dotfiles/hosts/${remote_short}.bashrc.local" + fi + + # ---- 6. Optionally run setup on the remote ---- + if ! $dry_run; then + local do_setup=$run_setup + if ! $run_setup; then + echo + read -r -p "Run setup_enterprise_ai_bash.sh on $target now? Creates dirs, writes shell config. (y/n): " _ans + [[ "$_ans" == [yY] ]] && do_setup=true + fi + + if $do_setup; then + echo + info "Running setup on $target (profile=$setup_profile)..." + info "This will create the directory structure and write shell config." + echo + ssh -t "$target" \ + "MACHINE_PROFILE=${setup_profile} DOTFILES_REMOTE=${DOTFILES_REMOTE} bash ~/scripts/setup_enterprise_ai_bash.sh" + success "Setup complete on $target." + fi + fi + + echo + if $dry_run; then + info "Dry run complete. Re-run without --dry-run to transfer files." + else + bold "Deploy complete: $deployed file(s) deployed, $skipped skipped." + info "Files were copied directly (no symlinks). Re-run deploy-to to push updates." + $skip_ssh && info "~/.ssh/ was skipped. Use --include-ssh to also deploy ~/.ssh/config." + ! $run_setup && info "Tip: add --run-setup to also create directories and shell config on the remote." + fi +} + +# ----------------------------------------------------------------------- +# 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 + auth Save Gitea token so push never prompts for credentials + +${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 + deploy-to [--scripts-only] [--include-ssh] [--no-backup] [--run-setup] [--profile work|personal] [--dry-run] + SCP tracked dotfiles + scripts directly to a server. + Use when the server can't reach the Gitea repo. + Backs up existing remote files locally before overwriting. + --scripts-only Only push ~/scripts/, skip dotfiles + --include-ssh Also deploy ~/.ssh/config (skipped by default) + --no-backup Skip the pre-deploy remote backup + --run-setup Run setup_enterprise_ai_bash.sh on the remote + after deploy (creates dirs, shell config) + --profile work|personal Profile to pass to setup (default: work) + --dry-run Preview what would be transferred + +${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 -sfn "\$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 "$@" ;; + auth) cmd_auth "$@" ;; + remote-bootstrap) cmd_remote_bootstrap "$@" ;; + deploy-to) cmd_deploy_to "$@" ;; + help|--help|-h) cmd_help ;; + *) + error "Unknown command: $cmd" + cmd_help + exit 1 + ;; + esac +} + +main "$@" diff --git a/dotfiles/scripts/setup_enterprise_ai_bash.sh b/dotfiles/scripts/setup_enterprise_ai_bash.sh new file mode 100755 index 0000000..43a3451 --- /dev/null +++ b/dotfiles/scripts/setup_enterprise_ai_bash.sh @@ -0,0 +1,473 @@ +#!/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-/ (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" <> "$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" + +# ---- NFS-aware placement for work servers ---- +# Work servers with NFS-mounted home dirs share ~/.bashrc.local across hosts. +# To keep per-machine overrides, store the real file in /opt/kenjim/ and symlink. +if [[ "$MACHINE_PROFILE" == "work" ]]; then + _is_nfs_home=false + case "$(hostname -s)" in + etqc-*|etbg-*|engtech-dev-*|qnc-kenjim-toby-shell*|qtaas*|bng-kenjim-toby-shell*|btaas*|kenjim-taas*) + if df -P "$HOME" 2>/dev/null | tail -1 | grep -qE ':|nfs'; then + _is_nfs_home=true + elif mount | grep -q "on ${HOME%%/} .*type nfs"; then + _is_nfs_home=true + fi + ;; + esac + + if $_is_nfs_home; then + OPT_DIR="/opt/kenjim" + OPT_LOCAL="$OPT_DIR/.bashrc.local" + echo "🔗 NFS home detected — relocating .bashrc.local to $OPT_LOCAL" + + if [ ! -d "$OPT_DIR" ]; then + echo " Creating $OPT_DIR ..." + sudo mkdir -p "$OPT_DIR" + sudo chown "$(id -u):$(id -g)" "$OPT_DIR" + chmod 700 "$OPT_DIR" + fi + + # If ~/.bashrc.local is already a symlink (previous run), the cat above + # wrote through it into the target. Remove the symlink first so mv + # doesn't see source and destination as the same file. + if [ -L "$BASHRC_LOCAL" ]; then + rm -f "$BASHRC_LOCAL" + # Content already landed at the symlink target; just ensure perms + chmod 600 "$OPT_LOCAL" + else + mv "$BASHRC_LOCAL" "$OPT_LOCAL" + chmod 600 "$OPT_LOCAL" + fi + + ln -sfn "$OPT_LOCAL" "$BASHRC_LOCAL" + echo " Linked: ~/.bashrc.local → $OPT_LOCAL" + + # VS Code Remote stores large caches in ~/.vscode-server and + # ~/.vscode-remote-containers — keep them on local disk, not NFS. + for _vsdir in .vscode-server .vscode-remote-containers; do + _opt_target="$OPT_DIR/$_vsdir" + _home_link="$HOME_DIR/$_vsdir" + mkdir -p "$_opt_target" + if [ -d "$_home_link" ] && [ ! -L "$_home_link" ]; then + # Existing real directory — move contents then replace with symlink + echo " Moving ~/$_vsdir → $OPT_DIR/$_vsdir ..." + rsync -a "$_home_link/" "$_opt_target/" 2>/dev/null || \ + cp -a "$_home_link/." "$_opt_target/" 2>/dev/null || true + rm -rf "$_home_link" + fi + ln -sfn "$_opt_target" "$_home_link" + echo " Linked: ~/$_vsdir → $_opt_target" + done + + echo "✅ Per-machine .bashrc.local and VS Code dirs stored outside NFS at $OPT_DIR" + fi +fi + +# ------------------------------------------------------ +# 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" < "$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 + diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh deleted file mode 100755 index 51fa23f..0000000 --- a/scripts/bootstrap.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/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." diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh new file mode 120000 index 0000000..5bab0ab --- /dev/null +++ b/scripts/bootstrap.sh @@ -0,0 +1 @@ +/home/kenjim/dotfiles/dotfiles/scripts/bootstrap.sh \ No newline at end of file diff --git a/scripts/dotfiles_manager.sh b/scripts/dotfiles_manager.sh deleted file mode 100755 index 5c714ee..0000000 --- a/scripts/dotfiles_manager.sh +++ /dev/null @@ -1,1328 +0,0 @@ -#!/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 -# deploy-to SCP tracked dotfiles + scripts to a server (no git needed) -# 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" "$@" -} - -# Credentials file (never committed — chmod 600) -CREDS_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/dotfiles/credentials" - -# Read saved token (returns empty string if not set) -_read_token() { - [ -f "$CREDS_FILE" ] && grep -m1 '^GITEA_TOKEN=' "$CREDS_FILE" | cut -d= -f2- || true -} - -# Inject token into an HTTP(S) remote URL for a single git operation. -# Keeps the stored remote URL clean — only the transient call gets credentials. -_authed_url() { - local url="$1" - local token; token="${GITEA_TOKEN:-$(_read_token)}" - if [[ -n "$token" && "$url" == http* ]]; then - local host_path; host_path="${url#*://}" - host_path="${host_path#*@}" # strip any existing user:pass - echo "${url%%://*}://kenjim:${token}@${host_path}" - else - echo "$url" - fi -} - -# Check gpg is installed with OS-appropriate install hint -_require_gpg() { - command -v gpg &>/dev/null && return 0 - case "$(uname -s)" in - Darwin*) die "gpg not installed. Install with: brew install gnupg" ;; - *) die "gpg not installed. Install with: sudo apt-get install gnupg" ;; - esac -} - -# ----------------------------------------------------------------------- -# 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 - -# Credential store — never commit -.config/dotfiles/credentials -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/directory - if [ -e "$dest" ] && [ ! -L "$dest" ]; then - mkdir -p "$BACKUP_DIR/$(dirname "$rel")" - cp -a "$dest" "$BACKUP_DIR/$rel" - warn "Backed up existing $dest" - # rm -rf needed when $dest is a real directory (e.g. ~/.bashrc.d created manually) - if [ -d "$dest" ]; then rm -rf "$dest"; else rm -f "$dest"; fi - 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 - - # -n: treat dest as a file (don't place link inside) if dest is a dir symlink - ln -sfn "$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." - - local pull_url; pull_url="$(_authed_url "$DOTFILES_REMOTE")" - - # 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 "$pull_url" main 2>/dev/null || \ - dgit pull --rebase "$pull_url" master 2>/dev/null || { - warn "Could not pull (remote unreachable or no token set). Working offline." - warn "Run 'dotfiles auth' to save credentials if push/pull keep failing." - } - - if $stashed; then - dgit stash pop || warn "Stash pop had conflicts. Resolve in $DOTFILES_DIR" - fi - - cmd_install - success "Sync complete." -} - -# ----------------------------------------------------------------------- -# CREDENTIAL HELPERS — cmd_auth is defined further below -# (CREDS_FILE, _read_token, _authed_url are in the HELPERS block above) -# ----------------------------------------------------------------------- - -# ----------------------------------------------------------------------- -# COMMAND: auth — save Gitea credentials once -# ----------------------------------------------------------------------- -cmd_auth() { - bold "=== Gitea Authentication Setup ===" - echo - info "Remote: $DOTFILES_REMOTE" - info "Credentials are saved to $CREDS_FILE (chmod 600, never committed)" - echo - echo "Options:" - echo " [1] Personal Access Token (recommended — Settings → Applications → Generate Token)" - echo " [2] Password (not recommended)" - read -r -p "Choice [1]: " _method - _method="${_method:-1}" - - local token - if [[ "$_method" == "2" ]]; then - read -r -s -p "Gitea password: " token; echo - else - echo - info "To generate a token: $DOTFILES_REMOTE (open in browser)" - info " → Settings (top-right avatar) → Applications → Generate Token" - info " → Name it 'dotfiles', tick Contents+Write, copy the token" - echo - read -r -s -p "Paste token: " token; echo - fi - - [[ -z "$token" ]] && die "No token entered." - - mkdir -p "$(dirname "$CREDS_FILE")" - chmod 700 "$(dirname "$CREDS_FILE")" - printf 'GITEA_TOKEN=%s\n' "$token" > "$CREDS_FILE" - chmod 600 "$CREDS_FILE" - - success "Credentials saved to $CREDS_FILE" - info "Test with: dotfiles push" -} - -# ----------------------------------------------------------------------- -# 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) - - # Build push URL — inject token for HTTP remotes so we never hang on a prompt - local push_url; push_url="$(_authed_url "$DOTFILES_REMOTE")" - if [[ "$push_url" == "$DOTFILES_REMOTE" && "$DOTFILES_REMOTE" == http* ]]; then - warn "No Gitea token found. Run 'dotfiles auth' to save one." - warn "Attempting push anyway (may hang waiting for password)..." - fi - - # Set upstream on first push if needed - if ! dgit config "branch.$branch.remote" &>/dev/null; then - dgit push --set-upstream "$push_url" "$branch" - # Record the clean URL as upstream (not the token-embedded one) - dgit remote set-url origin "$DOTFILES_REMOTE" 2>/dev/null || true - dgit config "branch.$branch.remote" origin - dgit config "branch.$branch.merge" "refs/heads/$branch" - else - dgit push "$push_url" "$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) ===" - _require_gpg - - 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) ===" - _require_gpg - - 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: deploy-to (push dotfiles to a server that can't reach Gitea) -# ----------------------------------------------------------------------- -cmd_deploy_to() { - [ $# -ge 1 ] || die "Usage: deploy-to [--scripts-only] [--include-ssh] [--no-backup] [--run-setup] [--profile work|personal] [--dry-run]" - - local target="$1"; shift - local scripts_only=false - local skip_ssh=true # default: skip .ssh/ — avoid pushing keys/config to servers - local no_backup=false - local run_setup=false - local setup_profile="work" # default profile for servers - local dry_run=false - - while [[ $# -gt 0 ]]; do - case "$1" in - --scripts-only) scripts_only=true; shift ;; - --include-ssh) skip_ssh=false; shift ;; - --no-backup) no_backup=true; shift ;; - --run-setup) run_setup=true; shift ;; - --profile) setup_profile="${2:-work}"; shift 2 ;; - --dry-run) dry_run=true; shift ;; - *) die "Unknown option: $1" ;; - esac - done - - bold "=== Deploy dotfiles → $target ===" - echo - info "Strategy: SCP files directly to their HOME paths (no git required on remote)" - $dry_run && warn "DRY RUN — no files will be transferred." - echo - - # ---- 1. Check SSH ---- - info "Testing SSH connectivity..." - if ! ssh -o ConnectTimeout=10 -o BatchMode=yes -o StrictHostKeyChecking=accept-new \ - "$target" "exit 0" 2>/dev/null; then - die "Cannot reach $target. Ensure SSH key access is set up:\n ssh-copy-id $target" - fi - success "SSH connection OK." - echo - - local self_dir; self_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - local deployed=0 skipped=0 - - # ---- 2. Back up remote files locally before overwriting ---- - # Saved to: ~/.dotfiles_backup/remote--/ - if ! $no_backup && ! $dry_run; then - local remote_host; remote_host="${target##*@}" # strip user@ prefix for dir name - local backup_base="$HOME/.dotfiles_backup/remote-${remote_host}-$(date +%Y%m%d_%H%M%S)" - info "Backing up existing remote files → $backup_base" - - # Build the list of remote paths to fetch: - # - tracked dotfiles from manifest (applying same filters as deploy) - # - remote ~/scripts/ if it exists - local remote_paths=() - if [ -f "$MANIFEST" ] && ! $scripts_only; then - while IFS= read -r rel || [ -n "$rel" ]; do - [ -z "$rel" ] && continue - $skip_ssh && [[ "$rel" == .ssh/* ]] && continue - [[ "$rel" == scripts/* ]] && continue - remote_paths+=("$rel") - done < "$MANIFEST" - fi - - # Always attempt to back up remote scripts/ - remote_paths+=("scripts/dotfiles_manager.sh" "scripts/bootstrap.sh" "scripts/setup_enterprise_ai_bash.sh") - - local backed_up=0 - for rel in "${remote_paths[@]}"; do - # Check if the file/dir actually exists on the remote before fetching - if ssh "$target" "[ -e ~/$rel ]" 2>/dev/null; then - local local_dest="$backup_base/$rel" - mkdir -p "$(dirname "$local_dest")" - # Use -r for directories, plain scp for files - if ssh "$target" "[ -d ~/$rel ]" 2>/dev/null; then - mkdir -p "$local_dest" - if scp -rq "$target:~/$rel/." "$local_dest/" 2>/dev/null; then - (( backed_up++ )) || true - fi - elif scp -q "$target:~/$rel" "$local_dest" 2>/dev/null; then - (( backed_up++ )) || true - fi - fi - done - - if [[ "$backed_up" -gt 0 ]]; then - success "Backed up $backed_up remote file(s) → $backup_base" - else - info "No existing remote files found to back up." - rmdir "$backup_base" 2>/dev/null || true - fi - echo - fi - $no_backup && ! $dry_run && warn "Skipping remote backup (--no-backup set)." - - # ---- 3. Deploy scripts ---- - info "Deploying scripts → $target:~/scripts/ ..." - local script_files=() - for f in \ - "$self_dir/dotfiles_manager.sh" \ - "$self_dir/setup_enterprise_ai_bash.sh" \ - "$self_dir/bootstrap.sh"; do - [ -f "$f" ] && script_files+=("$f") - done - - if [[ ${#script_files[@]} -gt 0 ]]; then - if $dry_run; then - for f in "${script_files[@]}"; do - echo " [dry-run] scp $(basename "$f") → $target:~/scripts/$(basename "$f")" - done - else - ssh "$target" "mkdir -p ~/scripts" - scp -q "${script_files[@]}" "$target:~/scripts/" - ssh "$target" "chmod +x ~/scripts/dotfiles_manager.sh ~/scripts/setup_enterprise_ai_bash.sh ~/scripts/bootstrap.sh 2>/dev/null; true" - (( deployed += ${#script_files[@]} )) || true - success "Scripts deployed (${#script_files[@]} files)." - fi - fi - - $scripts_only && { echo; success "Done (scripts only)."; return; } - - # ---- 4. Deploy tracked dotfiles from manifest ---- - [ -f "$MANIFEST" ] || { warn "No manifest found — only scripts were deployed."; return; } - - echo - info "Deploying tracked dotfiles → $target home directory..." - echo - - while IFS= read -r rel || [ -n "$rel" ]; do - [ -z "$rel" ] && continue - - # Skip .ssh/ by default — avoids pushing your private keys/known_hosts to servers - if $skip_ssh && [[ "$rel" == .ssh/* ]]; then - info "Skipping: ~/$rel (use --include-ssh to override)" - (( skipped++ )) || true - continue - fi - - # Scripts are handled above - [[ "$rel" == scripts/* ]] && continue - - local src="$DOTFILES_DIR/$rel" - if [ ! -e "$src" ]; then - warn "Missing in dotfiles, skipping: $rel" - (( skipped++ )) || true - continue - fi - - if $dry_run; then - echo " [dry-run] ~/$rel$([ -d "$src" ] && echo '/')" - else - local parent; parent="$(dirname "$rel")" - if [ -d "$src" ]; then - # For directories: remove stale dest first (avoids double-nesting on re-run) - # then scp -r into the parent so the dir name is preserved correctly. - ssh "$target" "rm -rf ~/$rel" - if [[ "$parent" == "." ]]; then - scp -rq "$src" "$target:~/" - else - ssh "$target" "mkdir -p ~/$parent" - scp -rq "$src" "$target:~/$parent/" - fi - else - [[ "$parent" != "." ]] && ssh "$target" "mkdir -p ~/$parent" - scp -q "$src" "$target:~/$rel" - fi - success "Deployed: ~/$rel" - (( deployed++ )) || true - fi - done < "$MANIFEST" - - # ---- 5. Deploy hosts/.bashrc.local → ~/.bashrc.local ---- - # Strip user@ prefix, then strip domain suffix to get short hostname - local remote_short; remote_short="${target##*@}" - remote_short="${remote_short%%.*}" - local host_local="$DOTFILES_DIR/hosts/${remote_short}.bashrc.local" - - if [ -f "$host_local" ]; then - echo - info "Found host-specific config: hosts/${remote_short}.bashrc.local" - if $dry_run; then - echo " [dry-run] hosts/${remote_short}.bashrc.local → ~/.bashrc.local" - else - # Back up existing remote .bashrc.local if not already captured above - if $no_backup; then - : # skip - elif ssh "$target" '[ -L ~/.bashrc.local ]' 2>/dev/null; then - # It's a symlink (NFS setup) — back up the target file - local real_path - real_path=$(ssh "$target" 'readlink -f ~/.bashrc.local' 2>/dev/null || true) - if [[ -n "$real_path" ]]; then - local bl_backup="$HOME/.dotfiles_backup/remote-${remote_short}-$(date +%Y%m%d_%H%M%S)" - mkdir -p "$bl_backup" - scp -q "$target:$real_path" "$bl_backup/.bashrc.local" 2>/dev/null || true - info "Backed up remote $real_path → $bl_backup/.bashrc.local" - fi - elif ssh "$target" '[ -f ~/.bashrc.local ]' 2>/dev/null; then - local bl_backup="$HOME/.dotfiles_backup/remote-${remote_short}-$(date +%Y%m%d_%H%M%S)" - mkdir -p "$bl_backup" - scp -q "$target:~/.bashrc.local" "$bl_backup/.bashrc.local" 2>/dev/null || true - info "Backed up remote ~/.bashrc.local → $bl_backup/.bashrc.local" - fi - - # Detect NFS home on the remote and deploy to /opt/kenjim/ if needed - local _remote_nfs=false - if ssh "$target" 'df -P "$HOME" 2>/dev/null | tail -1 | grep -qE ":|nfs"' 2>/dev/null; then - _remote_nfs=true - fi - - if $_remote_nfs; then - info "NFS home detected on $target — deploying to /opt/kenjim/.bashrc.local" - # Ensure /opt/kenjim exists — skip sudo if it already does - if ! ssh "$target" '[ -d /opt/kenjim ]' 2>/dev/null; then - info "Creating /opt/kenjim on $target (requires sudo)..." - ssh -t "$target" 'sudo mkdir -p /opt/kenjim && sudo chown $(id -u):$(id -g) /opt/kenjim && chmod 700 /opt/kenjim' - fi - scp -q "$host_local" "$target:/opt/kenjim/.bashrc.local" - ssh "$target" 'chmod 600 /opt/kenjim/.bashrc.local && ln -sfn /opt/kenjim/.bashrc.local ~/.bashrc.local' - success "Deployed: hosts/${remote_short}.bashrc.local → /opt/kenjim/.bashrc.local (symlinked from ~)" - else - scp -q "$host_local" "$target:~/.bashrc.local" - success "Deployed: hosts/${remote_short}.bashrc.local → ~/.bashrc.local" - fi - (( deployed++ )) || true - fi - else - info "No host-specific config found at hosts/${remote_short}.bashrc.local — skipping." - info "Create one to manage ~/.bashrc.local centrally: dotfiles/hosts/${remote_short}.bashrc.local" - fi - - # ---- 6. Optionally run setup on the remote ---- - if ! $dry_run; then - local do_setup=$run_setup - if ! $run_setup; then - echo - read -r -p "Run setup_enterprise_ai_bash.sh on $target now? Creates dirs, writes shell config. (y/n): " _ans - [[ "$_ans" == [yY] ]] && do_setup=true - fi - - if $do_setup; then - echo - info "Running setup on $target (profile=$setup_profile)..." - info "This will create the directory structure and write shell config." - echo - ssh -t "$target" \ - "MACHINE_PROFILE=${setup_profile} DOTFILES_REMOTE=${DOTFILES_REMOTE} bash ~/scripts/setup_enterprise_ai_bash.sh" - success "Setup complete on $target." - fi - fi - - echo - if $dry_run; then - info "Dry run complete. Re-run without --dry-run to transfer files." - else - bold "Deploy complete: $deployed file(s) deployed, $skipped skipped." - info "Files were copied directly (no symlinks). Re-run deploy-to to push updates." - $skip_ssh && info "~/.ssh/ was skipped. Use --include-ssh to also deploy ~/.ssh/config." - ! $run_setup && info "Tip: add --run-setup to also create directories and shell config on the remote." - fi -} - -# ----------------------------------------------------------------------- -# 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 - auth Save Gitea token so push never prompts for credentials - -${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 - deploy-to [--scripts-only] [--include-ssh] [--no-backup] [--run-setup] [--profile work|personal] [--dry-run] - SCP tracked dotfiles + scripts directly to a server. - Use when the server can't reach the Gitea repo. - Backs up existing remote files locally before overwriting. - --scripts-only Only push ~/scripts/, skip dotfiles - --include-ssh Also deploy ~/.ssh/config (skipped by default) - --no-backup Skip the pre-deploy remote backup - --run-setup Run setup_enterprise_ai_bash.sh on the remote - after deploy (creates dirs, shell config) - --profile work|personal Profile to pass to setup (default: work) - --dry-run Preview what would be transferred - -${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 -sfn "\$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 "$@" ;; - auth) cmd_auth "$@" ;; - remote-bootstrap) cmd_remote_bootstrap "$@" ;; - deploy-to) cmd_deploy_to "$@" ;; - help|--help|-h) cmd_help ;; - *) - error "Unknown command: $cmd" - cmd_help - exit 1 - ;; - esac -} - -main "$@" diff --git a/scripts/dotfiles_manager.sh b/scripts/dotfiles_manager.sh new file mode 120000 index 0000000..8838bcc --- /dev/null +++ b/scripts/dotfiles_manager.sh @@ -0,0 +1 @@ +/home/kenjim/dotfiles/dotfiles/scripts/dotfiles_manager.sh \ No newline at end of file diff --git a/scripts/setup_enterprise_ai_bash.sh b/scripts/setup_enterprise_ai_bash.sh deleted file mode 100755 index 43a3451..0000000 --- a/scripts/setup_enterprise_ai_bash.sh +++ /dev/null @@ -1,473 +0,0 @@ -#!/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-/ (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" <> "$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" - -# ---- NFS-aware placement for work servers ---- -# Work servers with NFS-mounted home dirs share ~/.bashrc.local across hosts. -# To keep per-machine overrides, store the real file in /opt/kenjim/ and symlink. -if [[ "$MACHINE_PROFILE" == "work" ]]; then - _is_nfs_home=false - case "$(hostname -s)" in - etqc-*|etbg-*|engtech-dev-*|qnc-kenjim-toby-shell*|qtaas*|bng-kenjim-toby-shell*|btaas*|kenjim-taas*) - if df -P "$HOME" 2>/dev/null | tail -1 | grep -qE ':|nfs'; then - _is_nfs_home=true - elif mount | grep -q "on ${HOME%%/} .*type nfs"; then - _is_nfs_home=true - fi - ;; - esac - - if $_is_nfs_home; then - OPT_DIR="/opt/kenjim" - OPT_LOCAL="$OPT_DIR/.bashrc.local" - echo "🔗 NFS home detected — relocating .bashrc.local to $OPT_LOCAL" - - if [ ! -d "$OPT_DIR" ]; then - echo " Creating $OPT_DIR ..." - sudo mkdir -p "$OPT_DIR" - sudo chown "$(id -u):$(id -g)" "$OPT_DIR" - chmod 700 "$OPT_DIR" - fi - - # If ~/.bashrc.local is already a symlink (previous run), the cat above - # wrote through it into the target. Remove the symlink first so mv - # doesn't see source and destination as the same file. - if [ -L "$BASHRC_LOCAL" ]; then - rm -f "$BASHRC_LOCAL" - # Content already landed at the symlink target; just ensure perms - chmod 600 "$OPT_LOCAL" - else - mv "$BASHRC_LOCAL" "$OPT_LOCAL" - chmod 600 "$OPT_LOCAL" - fi - - ln -sfn "$OPT_LOCAL" "$BASHRC_LOCAL" - echo " Linked: ~/.bashrc.local → $OPT_LOCAL" - - # VS Code Remote stores large caches in ~/.vscode-server and - # ~/.vscode-remote-containers — keep them on local disk, not NFS. - for _vsdir in .vscode-server .vscode-remote-containers; do - _opt_target="$OPT_DIR/$_vsdir" - _home_link="$HOME_DIR/$_vsdir" - mkdir -p "$_opt_target" - if [ -d "$_home_link" ] && [ ! -L "$_home_link" ]; then - # Existing real directory — move contents then replace with symlink - echo " Moving ~/$_vsdir → $OPT_DIR/$_vsdir ..." - rsync -a "$_home_link/" "$_opt_target/" 2>/dev/null || \ - cp -a "$_home_link/." "$_opt_target/" 2>/dev/null || true - rm -rf "$_home_link" - fi - ln -sfn "$_opt_target" "$_home_link" - echo " Linked: ~/$_vsdir → $_opt_target" - done - - echo "✅ Per-machine .bashrc.local and VS Code dirs stored outside NFS at $OPT_DIR" - fi -fi - -# ------------------------------------------------------ -# 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" < "$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 - diff --git a/scripts/setup_enterprise_ai_bash.sh b/scripts/setup_enterprise_ai_bash.sh new file mode 120000 index 0000000..3af2654 --- /dev/null +++ b/scripts/setup_enterprise_ai_bash.sh @@ -0,0 +1 @@ +/home/kenjim/dotfiles/dotfiles/scripts/setup_enterprise_ai_bash.sh \ No newline at end of file