feat: add deploy-to command and reorganize ssh config + docs

- dotfiles_manager.sh: add 'deploy-to' command to SCP tracked dotfiles
  and scripts directly to servers with no Gitea access
  - backs up existing remote files to ~/.dotfiles_backup/remote-<host>-<timestamp>/
    before overwriting anything
  - flags: --scripts-only, --include-ssh, --no-backup, --dry-run
- ssh/config: reorganize into labeled sections, move work hosts to top,
  fix global defaults (proper Host * block, remove deprecated Protocol/KeepAlive),
  add inline comments on all port forwards
- README.md: full rewrite with directory layout, profiles, shell layering,
  env vars, aliases, bootstrap flow, symlink mechanics, SSH key strategy,
  two-machine sync workflow, and deploy-to docs
This commit is contained in:
Kenji Morishige
2026-02-23 16:52:56 -06:00
parent 180cd61e30
commit c3a92e8ca8
2 changed files with 210 additions and 6 deletions

View File

@@ -195,15 +195,20 @@ source ~/.bash_profile
### Day-to-day sync ### Day-to-day sync
After editing on one machine, push and sync on the other:
```bash ```bash
# Pull latest dotfiles from Gitea and reapply any new symlinks: # On the machine where you made changes:
dotfiles sync # or: bash ~/scripts/bootstrap.sh dotfiles push "describe what changed"
# Push local changes: # On the other machine to pick up the changes:
dotfiles push "my change description" dotfiles sync
```
# Check symlink health: ```bash
dotfiles status # Other useful commands:
dotfiles status # check symlink health
dotfiles sync # pull + rebase + reapply symlinks (also callable as: bash ~/scripts/bootstrap.sh)
``` ```
### Remote machine bootstrap (from this machine) ### Remote machine bootstrap (from this machine)
@@ -215,6 +220,35 @@ dotfiles remote-bootstrap user@hostname --profile work
This uploads the scripts, then runs the full setup interactively over SSH. This uploads the scripts, then runs the full setup interactively over SSH.
### Deploy to a server with no Gitea access
Linux servers at work can't clone the private Gitea repo. Use `deploy-to` to
SCP tracked dotfiles and scripts directly to their HOME paths — no git required
on the remote.
```bash
# Push everything (dotfiles + scripts):
dotfiles deploy-to user@server
# Scripts only (dotfiles_manager.sh, bootstrap.sh, setup script):
dotfiles deploy-to user@server --scripts-only
# Preview what would be transferred without doing it:
dotfiles deploy-to user@server --dry-run
# Also deploy ~/.ssh/config (skipped by default for security):
dotfiles deploy-to user@server --include-ssh
# Skip the pre-deploy backup (faster, but no safety net):
dotfiles deploy-to user@server --no-backup
```
Before overwriting any file, `deploy-to` SCPs the server's existing versions
to `~/.dotfiles_backup/remote-<host>-<timestamp>/` on your local machine.
Files are copied directly (not symlinked). Re-run `deploy-to` any time you
want to push updates. `~/.ssh/` is skipped by default to avoid accidentally
pushing private keys or your personal known_hosts to a shared server.
--- ---
## Dotfiles Management — How Symlinks Work ## Dotfiles Management — How Symlinks Work

View File

@@ -24,6 +24,7 @@
# ssh-export GPG-encrypt private keys → dotfiles/.ssh/keys/*.gpg # ssh-export GPG-encrypt private keys → dotfiles/.ssh/keys/*.gpg
# ssh-import Decrypt GPG-encrypted keys from dotfiles to ~/.ssh/ # ssh-import Decrypt GPG-encrypted keys from dotfiles to ~/.ssh/
# remote-bootstrap SSH into another machine and run full setup # 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 # help Show this message
# #
# File layout inside ~/dotfiles/: # File layout inside ~/dotfiles/:
@@ -878,6 +879,166 @@ cmd_remote_bootstrap() {
info "Log in and verify: ssh $target 'dotfiles status'" 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 <user@host> [--scripts-only] [--include-ssh] [--no-backup] [--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 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 ;;
--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-<host>-<timestamp>/
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 actually exists on the remote before fetching
if ssh "$target" "[ -f ~/$rel ]" 2>/dev/null; then
local local_dest="$backup_base/$rel"
mkdir -p "$(dirname "$local_dest")"
if 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"
else
# Ensure parent directory exists on remote
local parent; parent="$(dirname "$rel")"
[[ "$parent" != "." ]] && ssh "$target" "mkdir -p ~/$parent"
scp -q "$src" "$target:~/$rel"
success "Deployed: ~/$rel"
(( deployed++ )) || true
fi
done < "$MANIFEST"
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."
fi
}
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# COMMAND: help # COMMAND: help
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
@@ -906,6 +1067,14 @@ ${BOLD}COMMANDS — SSH & Keys${RESET}
${BOLD}COMMANDS — Multi-machine${RESET} ${BOLD}COMMANDS — Multi-machine${RESET}
remote-bootstrap <user@host> [--profile work|personal] remote-bootstrap <user@host> [--profile work|personal]
Upload scripts and run full setup on a remote machine Upload scripts and run full setup on a remote machine
deploy-to <user@host> [--scripts-only] [--include-ssh] [--no-backup] [--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
--dry-run Preview what would be transferred
${BOLD}QUICK START — this machine (work)${RESET} ${BOLD}QUICK START — this machine (work)${RESET}
./dotfiles_manager.sh init ./dotfiles_manager.sh init
@@ -1042,6 +1211,7 @@ main() {
ssh-import) cmd_ssh_import "$@" ;; ssh-import) cmd_ssh_import "$@" ;;
auth) cmd_auth "$@" ;; auth) cmd_auth "$@" ;;
remote-bootstrap) cmd_remote_bootstrap "$@" ;; remote-bootstrap) cmd_remote_bootstrap "$@" ;;
deploy-to) cmd_deploy_to "$@" ;;
help|--help|-h) cmd_help ;; help|--help|-h) cmd_help ;;
*) *)
error "Unknown command: $cmd" error "Unknown command: $cmd"