docs(setup): add Debian 13 dev-VM setup procedure + scripts + devcontainer
CI / commits (pull_request) Successful in 2m52s
CI / check (pull_request) Successful in 3m3s
CI / scan (pull_request) Successful in 3m11s
CI / a11y (pull_request) Successful in 3m5s
Docs site / build (pull_request) Successful in 3m10s
CI / perf (pull_request) Successful in 6m53s
CI / commits (pull_request) Successful in 2m52s
CI / check (pull_request) Successful in 3m3s
CI / scan (pull_request) Successful in 3m11s
CI / a11y (pull_request) Successful in 3m5s
Docs site / build (pull_request) Successful in 3m10s
CI / perf (pull_request) Successful in 6m53s
New procedure in docs/setup/01-dev-debian-vm-setup.md targeting the new dev VM (10.100.201.21, Debian 13) that replaces the WSL workflow. The existing setup files are renumbered (01->02, 02->03, 03->04) to free prefix 01 for the default flow. 10 modular idempotent scripts under docs/setup/scripts/ install the stack: zsh + Oh My Zsh + Powerlevel10k + plugins, modern CLI tooling (bat, eza, fd-find, ripgrep, fzf, zoxide, ncdu, keychain + dev extras), nvm + Node from .nvmrc + corepack + pnpm, Docker CE + compose plugin, inotify watches + swap + hostname tuning, best-effort UFW + sshd hardening that probes-before-applying, and notes/aliases.zsh + notes/gitconfig.txt installation. A systemd template (apf-portal-infra@.service) auto-starts the local dev infra stack at boot per user. .devcontainer/ ships a VSCode Dev Container spec pinning Node 24 and attaching to the host's apf-portal-dev Compose network so postgres / redis / otel resolve by DNS from inside the container. initializeCommand fails fast if the infra hasn't been brought up yet. CLAUDE.md Environment conventions documents the two envs (local + development) plus the hybrid sub-mode (workstation IDE + VM infra via SSH LocalForward) and the two IDE flows (Remote-SSH + Devcontainer). Adjacent follow-ups (not in this PR): preview infra on the GitLab VM (10.100.201.10), GitLab Runner migration alongside the Gitea -> GitLab cutover, private dotfiles repo.
This commit is contained in:
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# apt update + install base packages every other script assumes.
|
||||
set -euo pipefail
|
||||
# shellcheck source=./lib.sh
|
||||
source "$(cd "$(dirname "$0")" && pwd)/lib.sh"
|
||||
require_debian
|
||||
require_not_root
|
||||
|
||||
log "apt update"
|
||||
sudo apt-get update -qq
|
||||
|
||||
apt_install \
|
||||
curl wget git ca-certificates gnupg lsb-release \
|
||||
software-properties-common apt-transport-https \
|
||||
build-essential pkg-config
|
||||
|
||||
ok "Base packages ready."
|
||||
Executable
+93
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env bash
|
||||
# zsh + Oh My Zsh + Powerlevel10k + plugins (autosuggestions, syntax-highlighting).
|
||||
# Patches ~/.zshrc to set the theme, plugin list, fzf hook.
|
||||
set -euo pipefail
|
||||
# shellcheck source=./lib.sh
|
||||
source "$(cd "$(dirname "$0")" && pwd)/lib.sh"
|
||||
require_debian
|
||||
require_not_root
|
||||
|
||||
apt_install zsh
|
||||
|
||||
# Make zsh the default shell for $USER (idempotent).
|
||||
current_shell="$(getent passwd "$USER" | cut -d: -f7)"
|
||||
zsh_path="$(command -v zsh)"
|
||||
if [[ "$current_shell" != "$zsh_path" ]]; then
|
||||
log "Setting zsh as default shell for $USER (sudo will prompt)."
|
||||
sudo chsh -s "$zsh_path" "$USER"
|
||||
ok "Default shell set to $zsh_path. Takes effect on next login."
|
||||
else
|
||||
skip "zsh already default shell."
|
||||
fi
|
||||
|
||||
# Oh My Zsh — install with RUNZSH=no to avoid spawning zsh during the script.
|
||||
OMZ_DIR="$HOME/.oh-my-zsh"
|
||||
if [[ -d "$OMZ_DIR" ]]; then
|
||||
skip "Oh My Zsh already installed."
|
||||
else
|
||||
log "Installing Oh My Zsh."
|
||||
RUNZSH=no CHSH=no KEEP_ZSHRC=yes sh -c \
|
||||
"$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
|
||||
ok "Oh My Zsh installed."
|
||||
fi
|
||||
|
||||
ZSH_CUSTOM_DIR="${ZSH_CUSTOM:-$OMZ_DIR/custom}"
|
||||
|
||||
clone_or_skip() {
|
||||
local name="$1" url="$2" dir="$3"
|
||||
if [[ -d "$dir" ]]; then
|
||||
skip "$name already cloned at $dir."
|
||||
else
|
||||
log "Cloning $name."
|
||||
git clone --depth=1 "$url" "$dir"
|
||||
ok "$name installed."
|
||||
fi
|
||||
}
|
||||
|
||||
# Powerlevel10k theme.
|
||||
clone_or_skip "Powerlevel10k" \
|
||||
"https://github.com/romkatv/powerlevel10k.git" \
|
||||
"$ZSH_CUSTOM_DIR/themes/powerlevel10k"
|
||||
|
||||
# Plugins.
|
||||
clone_or_skip "zsh-autosuggestions" \
|
||||
"https://github.com/zsh-users/zsh-autosuggestions.git" \
|
||||
"$ZSH_CUSTOM_DIR/plugins/zsh-autosuggestions"
|
||||
|
||||
clone_or_skip "zsh-syntax-highlighting" \
|
||||
"https://github.com/zsh-users/zsh-syntax-highlighting.git" \
|
||||
"$ZSH_CUSTOM_DIR/plugins/zsh-syntax-highlighting"
|
||||
|
||||
# Patch ~/.zshrc — theme + plugin list + fzf hook.
|
||||
ZSHRC="$HOME/.zshrc"
|
||||
[[ -f "$ZSHRC" ]] || touch "$ZSHRC"
|
||||
|
||||
if grep -q '^ZSH_THEME=' "$ZSHRC"; then
|
||||
if grep -q '^ZSH_THEME="powerlevel10k/powerlevel10k"' "$ZSHRC"; then
|
||||
skip "ZSH_THEME already powerlevel10k."
|
||||
else
|
||||
sed -i 's#^ZSH_THEME=.*#ZSH_THEME="powerlevel10k/powerlevel10k"#' "$ZSHRC"
|
||||
ok "ZSH_THEME → powerlevel10k/powerlevel10k."
|
||||
fi
|
||||
else
|
||||
echo 'ZSH_THEME="powerlevel10k/powerlevel10k"' >> "$ZSHRC"
|
||||
ok 'Set ZSH_THEME=powerlevel10k/powerlevel10k.'
|
||||
fi
|
||||
|
||||
PLUGINS_LINE='plugins=(git fzf zoxide direnv zsh-autosuggestions zsh-syntax-highlighting)'
|
||||
if grep -q '^plugins=' "$ZSHRC"; then
|
||||
if grep -q "^$PLUGINS_LINE" "$ZSHRC"; then
|
||||
skip "plugins=(...) already configured."
|
||||
else
|
||||
sed -i "s#^plugins=.*#$PLUGINS_LINE#" "$ZSHRC"
|
||||
ok "plugins=(...) updated."
|
||||
fi
|
||||
else
|
||||
echo "$PLUGINS_LINE" >> "$ZSHRC"
|
||||
ok "plugins=(...) added."
|
||||
fi
|
||||
|
||||
ensure_line '[ -f ~/.fzf.zsh ] && source ~/.fzf.zsh' "$ZSHRC"
|
||||
|
||||
ok "zsh + Oh My Zsh + Powerlevel10k ready."
|
||||
log "Next: run 'exec zsh -l' then 'p10k configure' to pick a Powerlevel10k preset."
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# Modern CLI tools — what notes/aliases.zsh assumes + a fullstack-dev kit.
|
||||
set -euo pipefail
|
||||
# shellcheck source=./lib.sh
|
||||
source "$(cd "$(dirname "$0")" && pwd)/lib.sh"
|
||||
require_debian
|
||||
require_not_root
|
||||
|
||||
# Requested in the project brief.
|
||||
apt_install bat eza fd-find ripgrep fzf zoxide ncdu keychain
|
||||
|
||||
# Useful extras for a fullstack dev workflow.
|
||||
apt_install jq yq httpie make tree htop tmux direnv dnsutils unzip rsync
|
||||
|
||||
# Debian renames `bat`→`batcat` and `fd`→`fdfind` to avoid binary
|
||||
# conflicts. notes/aliases.zsh aliases the Debian names back, but a
|
||||
# direct binary on PATH is more robust (works for non-interactive
|
||||
# scripts and other shells). Symlink in ~/.local/bin.
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
for pair in "batcat:bat" "fdfind:fd"; do
|
||||
src="${pair%%:*}"
|
||||
dst="${pair##*:}"
|
||||
src_path="$(command -v "$src" 2>/dev/null || true)"
|
||||
if [[ -z "$src_path" ]]; then
|
||||
warn "$src not found on PATH — skipping symlink."
|
||||
continue
|
||||
fi
|
||||
if [[ -L "$HOME/.local/bin/$dst" && "$(readlink "$HOME/.local/bin/$dst")" == "$src_path" ]]; then
|
||||
skip "$HOME/.local/bin/$dst already linked to $src."
|
||||
elif [[ -e "$HOME/.local/bin/$dst" ]]; then
|
||||
skip "$HOME/.local/bin/$dst exists (not a managed symlink) — leaving as-is."
|
||||
else
|
||||
ln -s "$src_path" "$HOME/.local/bin/$dst"
|
||||
ok "Linked $HOME/.local/bin/$dst → $src."
|
||||
fi
|
||||
done
|
||||
|
||||
# Make sure ~/.local/bin is on PATH for both bash and zsh.
|
||||
for rcfile in "$HOME/.bashrc" "$HOME/.zshrc"; do
|
||||
[[ -f "$rcfile" ]] || touch "$rcfile"
|
||||
ensure_line 'export PATH="$HOME/.local/bin:$PATH"' "$rcfile"
|
||||
done
|
||||
|
||||
ok "CLI tools installed."
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
# nvm + Node from the repo's .nvmrc + corepack + pnpm at the version pinned
|
||||
# in package.json's packageManager field.
|
||||
set -euo pipefail
|
||||
# shellcheck source=./lib.sh
|
||||
source "$(cd "$(dirname "$0")" && pwd)/lib.sh"
|
||||
require_debian
|
||||
require_not_root
|
||||
|
||||
NVM_VERSION="v0.40.1"
|
||||
NVM_DIR="$HOME/.nvm"
|
||||
|
||||
if [[ -d "$NVM_DIR" ]]; then
|
||||
skip "nvm already installed at $NVM_DIR."
|
||||
else
|
||||
log "Installing nvm $NVM_VERSION."
|
||||
curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/$NVM_VERSION/install.sh" | bash
|
||||
ok "nvm installed."
|
||||
fi
|
||||
|
||||
# Source nvm so we can use it in this shell.
|
||||
export NVM_DIR
|
||||
# shellcheck disable=SC1091
|
||||
source "$NVM_DIR/nvm.sh"
|
||||
|
||||
# Read .nvmrc from the repo root.
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
if [[ -z "$REPO_ROOT" ]]; then
|
||||
err "Run from inside a git checkout of apf_portal (no repo root found)."
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$REPO_ROOT/.nvmrc" ]]; then
|
||||
warn "$REPO_ROOT/.nvmrc not found — installing Node LTS as fallback."
|
||||
nvm install --lts
|
||||
nvm alias default 'lts/*'
|
||||
else
|
||||
NODE_VER="$(<"$REPO_ROOT/.nvmrc")"
|
||||
log "Installing Node $NODE_VER (from $REPO_ROOT/.nvmrc)."
|
||||
nvm install "$NODE_VER"
|
||||
nvm alias default "$NODE_VER"
|
||||
ok "Node $(node --version) active."
|
||||
fi
|
||||
|
||||
# corepack — provides pnpm + yarn shims, reads packageManager from package.json.
|
||||
corepack enable
|
||||
ok "corepack enabled."
|
||||
|
||||
# Warm pnpm to download the pinned version.
|
||||
if [[ -f "$REPO_ROOT/package.json" ]]; then
|
||||
pushd "$REPO_ROOT" >/dev/null
|
||||
pnpm --version >/dev/null
|
||||
ok "pnpm $(pnpm --version) ready (from package.json packageManager)."
|
||||
popd >/dev/null
|
||||
fi
|
||||
|
||||
log "Reload your shell (exec zsh -l) so nvm + node + pnpm are on PATH."
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
# Docker CE + compose plugin from the docker.com apt repo. Adds the
|
||||
# current user to the `docker` group and enables the daemon at boot.
|
||||
set -euo pipefail
|
||||
# shellcheck source=./lib.sh
|
||||
source "$(cd "$(dirname "$0")" && pwd)/lib.sh"
|
||||
require_debian
|
||||
require_not_root
|
||||
|
||||
if command -v docker >/dev/null 2>&1 \
|
||||
&& docker compose version >/dev/null 2>&1; then
|
||||
skip "docker + compose plugin already present."
|
||||
else
|
||||
log "Installing Docker CE from docker.com apt repo."
|
||||
|
||||
# GPG key.
|
||||
sudo install -m 0755 -d /etc/apt/keyrings
|
||||
if [[ ! -f /etc/apt/keyrings/docker.asc ]]; then
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg \
|
||||
| sudo tee /etc/apt/keyrings/docker.asc >/dev/null
|
||||
sudo chmod a+r /etc/apt/keyrings/docker.asc
|
||||
fi
|
||||
|
||||
# Repo definition.
|
||||
ARCH="$(dpkg --print-architecture)"
|
||||
# shellcheck disable=SC1091
|
||||
CODENAME="$(. /etc/os-release && echo "${VERSION_CODENAME}")"
|
||||
REPO_LINE="deb [arch=${ARCH} signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian ${CODENAME} stable"
|
||||
|
||||
if [[ -f /etc/apt/sources.list.d/docker.list ]] \
|
||||
&& grep -qxF "$REPO_LINE" /etc/apt/sources.list.d/docker.list; then
|
||||
skip "docker.list already configured for $CODENAME."
|
||||
else
|
||||
echo "$REPO_LINE" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
|
||||
ok "Wrote /etc/apt/sources.list.d/docker.list."
|
||||
fi
|
||||
|
||||
sudo apt-get update -qq
|
||||
apt_install docker-ce docker-ce-cli containerd.io \
|
||||
docker-buildx-plugin docker-compose-plugin
|
||||
ok "Docker installed."
|
||||
fi
|
||||
|
||||
# Add user to docker group.
|
||||
if id -nG "$USER" | grep -qw docker; then
|
||||
skip "User $USER already in docker group."
|
||||
else
|
||||
sudo usermod -aG docker "$USER"
|
||||
warn "Added $USER to docker group — log out and back in for it to apply."
|
||||
fi
|
||||
|
||||
# Enable docker.service at boot.
|
||||
if systemctl is-enabled docker.service >/dev/null 2>&1; then
|
||||
skip "docker.service already enabled at boot."
|
||||
else
|
||||
sudo systemctl enable --now docker.service
|
||||
ok "docker.service enabled and started."
|
||||
fi
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
# System-level tuning for Node / Vite / Nx development on a Debian VM:
|
||||
# - inotify watches limit (default 8K is too low for monorepos)
|
||||
# - hostname (prompt only if generic)
|
||||
# - swapfile (4 GB, only if none exists)
|
||||
set -euo pipefail
|
||||
# shellcheck source=./lib.sh
|
||||
source "$(cd "$(dirname "$0")" && pwd)/lib.sh"
|
||||
require_debian
|
||||
require_not_root
|
||||
|
||||
# 1) inotify watches.
|
||||
SYSCTL_FILE="/etc/sysctl.d/99-apf-portal.conf"
|
||||
INOTIFY_LINE="fs.inotify.max_user_watches=524288"
|
||||
if sudo grep -qxF "$INOTIFY_LINE" "$SYSCTL_FILE" 2>/dev/null; then
|
||||
skip "inotify watches already configured in $SYSCTL_FILE."
|
||||
else
|
||||
echo "$INOTIFY_LINE" | sudo tee "$SYSCTL_FILE" >/dev/null
|
||||
sudo sysctl --system >/dev/null
|
||||
ok "fs.inotify.max_user_watches → 524288."
|
||||
fi
|
||||
|
||||
# 2) Hostname — only prompt if it looks generic.
|
||||
current_hostname="$(hostname)"
|
||||
case "$current_hostname" in
|
||||
debian|debian13|localhost|trixie|debian12)
|
||||
read -r -p "Current hostname '$current_hostname' looks generic. New hostname (blank to keep): " new_hostname
|
||||
if [[ -n "$new_hostname" ]]; then
|
||||
sudo hostnamectl set-hostname "$new_hostname"
|
||||
ok "Hostname set to '$new_hostname'. Will reflect on next login."
|
||||
else
|
||||
skip "Hostname kept as '$current_hostname'."
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
skip "Hostname '$current_hostname' looks intentional — keeping."
|
||||
;;
|
||||
esac
|
||||
|
||||
# 3) Swap.
|
||||
if swapon --show | grep -q .; then
|
||||
skip "Swap already active: $(swapon --show --noheadings | head -1 | awk '{print $1, $3}')."
|
||||
else
|
||||
if confirm "No swap detected. Add a 4 GB swapfile at /swapfile?"; then
|
||||
sudo fallocate -l 4G /swapfile
|
||||
sudo chmod 600 /swapfile
|
||||
sudo mkswap /swapfile
|
||||
sudo swapon /swapfile
|
||||
ensure_line_sudo "/swapfile none swap sw 0 0" /etc/fstab
|
||||
ok "4 GB swapfile active."
|
||||
else
|
||||
skip "No swap configured (user choice)."
|
||||
fi
|
||||
fi
|
||||
Executable
+92
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env bash
|
||||
# Best-effort VM hardening — probes current state, only applies what's missing.
|
||||
# Skips cleanly when infra has already configured a step.
|
||||
# - UFW (allow SSH only)
|
||||
# - unattended-upgrades (security channel)
|
||||
# - fail2ban
|
||||
# - sshd: disable root login + password auth (always validates first)
|
||||
set -euo pipefail
|
||||
# shellcheck source=./lib.sh
|
||||
source "$(cd "$(dirname "$0")" && pwd)/lib.sh"
|
||||
require_debian
|
||||
require_not_root
|
||||
|
||||
log "Hardening pass — each section probes and skips if already done."
|
||||
|
||||
# ─── 1. UFW ────────────────────────────────────────────────────────────
|
||||
if ! command -v ufw >/dev/null; then
|
||||
apt_install ufw
|
||||
fi
|
||||
if sudo ufw status | head -1 | grep -q 'Status: active'; then
|
||||
skip "UFW already active. Current rules:"
|
||||
sudo ufw status numbered | sed 's/^/ /'
|
||||
else
|
||||
if confirm "Enable UFW with 'deny incoming, allow SSH'?"; then
|
||||
sudo ufw default deny incoming >/dev/null
|
||||
sudo ufw default allow outgoing >/dev/null
|
||||
sudo ufw allow OpenSSH >/dev/null
|
||||
sudo ufw --force enable
|
||||
ok "UFW enabled (SSH-only ingress)."
|
||||
else
|
||||
skip "UFW left disabled."
|
||||
fi
|
||||
fi
|
||||
|
||||
# ─── 2. unattended-upgrades ────────────────────────────────────────────
|
||||
apt_install unattended-upgrades
|
||||
AUTO_UP="/etc/apt/apt.conf.d/20auto-upgrades"
|
||||
if [[ -f "$AUTO_UP" ]] \
|
||||
&& sudo grep -q 'APT::Periodic::Update-Package-Lists "1"' "$AUTO_UP" \
|
||||
&& sudo grep -q 'APT::Periodic::Unattended-Upgrade "1"' "$AUTO_UP"; then
|
||||
skip "unattended-upgrades already enabled."
|
||||
else
|
||||
cat <<'EOF' | sudo tee "$AUTO_UP" >/dev/null
|
||||
APT::Periodic::Update-Package-Lists "1";
|
||||
APT::Periodic::Unattended-Upgrade "1";
|
||||
EOF
|
||||
ok "unattended-upgrades enabled."
|
||||
fi
|
||||
|
||||
# ─── 3. fail2ban ───────────────────────────────────────────────────────
|
||||
apt_install fail2ban
|
||||
if systemctl is-active fail2ban.service >/dev/null 2>&1; then
|
||||
skip "fail2ban already running."
|
||||
else
|
||||
sudo systemctl enable --now fail2ban.service
|
||||
ok "fail2ban started + enabled at boot."
|
||||
fi
|
||||
|
||||
# ─── 4. sshd hardening ────────────────────────────────────────────────
|
||||
# Drop-in file rather than editing /etc/ssh/sshd_config directly.
|
||||
# We always validate with `sshd -t` before reloading the daemon.
|
||||
SSHD_DROPIN="/etc/ssh/sshd_config.d/99-apf-portal-hardening.conf"
|
||||
if sudo test -f "$SSHD_DROPIN"; then
|
||||
skip "sshd hardening drop-in already at $SSHD_DROPIN."
|
||||
else
|
||||
warn "About to disable password auth + root login on SSH."
|
||||
warn "Make sure your SSH key already works from your workstation."
|
||||
if confirm "Continue?"; then
|
||||
cat <<'EOF' | sudo tee "$SSHD_DROPIN" >/dev/null
|
||||
# Managed by docs/setup/scripts/70-hardening.sh.
|
||||
# To override or extend, drop another file in /etc/ssh/sshd_config.d/.
|
||||
PermitRootLogin no
|
||||
PasswordAuthentication no
|
||||
ChallengeResponseAuthentication no
|
||||
KbdInteractiveAuthentication no
|
||||
UsePAM yes
|
||||
AllowAgentForwarding yes
|
||||
EOF
|
||||
if sudo sshd -t 2>/dev/null; then
|
||||
sudo systemctl reload ssh.service 2>/dev/null || sudo systemctl reload sshd.service
|
||||
ok "sshd reloaded with hardened config."
|
||||
else
|
||||
err "sshd config validation failed — removing $SSHD_DROPIN."
|
||||
sudo rm -f "$SSHD_DROPIN"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
skip "sshd hardening declined."
|
||||
fi
|
||||
fi
|
||||
|
||||
ok "Hardening pass complete."
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
# Wire up the project-specific dotfiles:
|
||||
# - symlink notes/aliases.zsh → ~/.oh-my-zsh/custom/aliases.zsh
|
||||
# - copy notes/gitconfig.txt to ~/.gitconfig and overlay your identity
|
||||
set -euo pipefail
|
||||
# shellcheck source=./lib.sh
|
||||
source "$(cd "$(dirname "$0")" && pwd)/lib.sh"
|
||||
require_debian
|
||||
require_not_root
|
||||
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
if [[ -z "$REPO_ROOT" ]]; then
|
||||
err "Run from inside a git checkout of apf_portal."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ─── 1. Aliases ───────────────────────────────────────────────────────
|
||||
SOURCE_ALIASES="$REPO_ROOT/notes/aliases.zsh"
|
||||
if [[ ! -f "$SOURCE_ALIASES" ]]; then
|
||||
err "$SOURCE_ALIASES not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OMZ_CUSTOM="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}"
|
||||
if [[ ! -d "$OMZ_CUSTOM" ]]; then
|
||||
err "$OMZ_CUSTOM not found — run 20-zsh.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TARGET_ALIASES="$OMZ_CUSTOM/aliases.zsh"
|
||||
if [[ -L "$TARGET_ALIASES" \
|
||||
&& "$(readlink -f "$TARGET_ALIASES")" == "$(readlink -f "$SOURCE_ALIASES")" ]]; then
|
||||
skip "aliases.zsh symlink already correct."
|
||||
else
|
||||
if [[ -e "$TARGET_ALIASES" || -L "$TARGET_ALIASES" ]]; then
|
||||
backup="$TARGET_ALIASES.bak.$(date +%s)"
|
||||
mv "$TARGET_ALIASES" "$backup"
|
||||
warn "Existing $TARGET_ALIASES backed up to $backup."
|
||||
fi
|
||||
ln -s "$SOURCE_ALIASES" "$TARGET_ALIASES"
|
||||
ok "Linked $TARGET_ALIASES → $SOURCE_ALIASES."
|
||||
fi
|
||||
|
||||
# ─── 2. gitconfig — copy + identity overlay ───────────────────────────
|
||||
SOURCE_GITCONFIG="$REPO_ROOT/notes/gitconfig.txt"
|
||||
if [[ ! -f "$SOURCE_GITCONFIG" ]]; then
|
||||
err "$SOURCE_GITCONFIG not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TARGET_GITCONFIG="$HOME/.gitconfig"
|
||||
if [[ -f "$TARGET_GITCONFIG" ]] && ! confirm "$TARGET_GITCONFIG exists — overwrite (existing backed up)?"; then
|
||||
skip "Keeping existing $TARGET_GITCONFIG."
|
||||
else
|
||||
if [[ -f "$TARGET_GITCONFIG" ]]; then
|
||||
cp "$TARGET_GITCONFIG" "$TARGET_GITCONFIG.bak.$(date +%s)"
|
||||
fi
|
||||
|
||||
# Prompt for identity. Defaults: pre-existing config values, else blank.
|
||||
current_name="$(git config --global user.name 2>/dev/null || true)"
|
||||
current_email="$(git config --global user.email 2>/dev/null || true)"
|
||||
read -r -p "Git user.name [${current_name:-<required>}]: " new_name
|
||||
new_name="${new_name:-$current_name}"
|
||||
read -r -p "Git user.email [${current_email:-<required>}]: " new_email
|
||||
new_email="${new_email:-$current_email}"
|
||||
if [[ -z "$new_name" || -z "$new_email" ]]; then
|
||||
err "Both user.name and user.email are required."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp "$SOURCE_GITCONFIG" "$TARGET_GITCONFIG"
|
||||
# `git config --global` finds and rewrites the [user] section
|
||||
# cleanly even when the source carries a different identity.
|
||||
git config --global user.name "$new_name"
|
||||
git config --global user.email "$new_email"
|
||||
|
||||
ok "Wrote $TARGET_GITCONFIG with identity $new_name <$new_email>."
|
||||
fi
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
# Orchestrator — runs every numbered setup script in order with a confirmation prompt.
|
||||
# Each script is also runnable individually:
|
||||
# ./docs/setup/scripts/40-node.sh
|
||||
# See docs/setup/01-dev-debian-vm-setup.md for the step-by-step doc.
|
||||
|
||||
set -euo pipefail
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
# shellcheck source=./lib.sh
|
||||
source "$HERE/lib.sh"
|
||||
|
||||
require_debian
|
||||
require_not_root
|
||||
|
||||
SCRIPTS=(
|
||||
10-base-packages.sh
|
||||
20-zsh.sh
|
||||
30-cli-tools.sh
|
||||
40-node.sh
|
||||
50-docker.sh
|
||||
60-tuning.sh
|
||||
70-hardening.sh
|
||||
80-dotfiles.sh
|
||||
)
|
||||
|
||||
log "Bootstrap will run, in order:"
|
||||
for s in "${SCRIPTS[@]}"; do
|
||||
printf ' - %s\n' "$s"
|
||||
done
|
||||
echo
|
||||
|
||||
if ! confirm "Continue?"; then
|
||||
log "Aborted before any change."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for s in "${SCRIPTS[@]}"; do
|
||||
echo
|
||||
log "═══ $s ═══"
|
||||
if confirm "Run $s now?"; then
|
||||
"$HERE/$s"
|
||||
ok "$s done"
|
||||
else
|
||||
skip "$s"
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
ok "Bootstrap complete."
|
||||
log "Next steps:"
|
||||
printf ' - exec zsh -l # reload shell to pick up zsh + PATH\n'
|
||||
printf ' - p10k configure # only if you skipped during 20-zsh.sh\n'
|
||||
printf ' - cp infra/local/.env.example infra/local/.env && $EDITOR infra/local/.env\n'
|
||||
printf ' - ./infra/local/dev.sh up\n'
|
||||
printf ' - pnpm install\n'
|
||||
Executable
+83
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared helpers for docs/setup/scripts/*.sh — sourced from each script.
|
||||
# Idempotency utilities + nice logging.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -t 1 ]]; then
|
||||
C_GREEN=$'\033[1;32m'
|
||||
C_YELLOW=$'\033[1;33m'
|
||||
C_RED=$'\033[1;31m'
|
||||
C_BLUE=$'\033[1;34m'
|
||||
C_DIM=$'\033[2m'
|
||||
C_RESET=$'\033[0m'
|
||||
else
|
||||
C_GREEN= C_YELLOW= C_RED= C_BLUE= C_DIM= C_RESET=
|
||||
fi
|
||||
|
||||
log() { printf '%s[%s]%s %s\n' "$C_BLUE" "$(date +%H:%M:%S)" "$C_RESET" "$*"; }
|
||||
ok() { printf '%s✓%s %s\n' "$C_GREEN" "$C_RESET" "$*"; }
|
||||
warn() { printf '%s⚠%s %s\n' "$C_YELLOW" "$C_RESET" "$*" >&2; }
|
||||
err() { printf '%s✗%s %s\n' "$C_RED" "$C_RESET" "$*" >&2; }
|
||||
skip() { printf '%s↪ skip%s %s\n' "$C_DIM" "$C_RESET" "$*"; }
|
||||
|
||||
require_debian() {
|
||||
if ! grep -q '^ID=debian' /etc/os-release 2>/dev/null; then
|
||||
err "This script targets Debian (read from /etc/os-release)."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_not_root() {
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
err "Run as your regular user (sudo is invoked where needed)."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
confirm() {
|
||||
local prompt="${1:-Proceed?}"
|
||||
read -r -p "$prompt [y/N] " ans
|
||||
[[ "$ans" =~ ^[yY]$ ]]
|
||||
}
|
||||
|
||||
# Install apt packages, skipping those already present. Idempotent.
|
||||
apt_install() {
|
||||
local missing=()
|
||||
for pkg in "$@"; do
|
||||
if ! dpkg -s "$pkg" >/dev/null 2>&1; then
|
||||
missing+=("$pkg")
|
||||
fi
|
||||
done
|
||||
if (( ${#missing[@]} == 0 )); then
|
||||
skip "All packages already present: $*"
|
||||
return 0
|
||||
fi
|
||||
log "apt install: ${missing[*]}"
|
||||
sudo apt-get install -y "${missing[@]}"
|
||||
ok "Installed: ${missing[*]}"
|
||||
}
|
||||
|
||||
# Append a line to a file iff not already present (exact match). Idempotent.
|
||||
ensure_line() {
|
||||
local line="$1"
|
||||
local file="$2"
|
||||
if grep -qxF -- "$line" "$file" 2>/dev/null; then
|
||||
skip "Already in $(basename "$file"): $line"
|
||||
else
|
||||
echo "$line" >> "$file"
|
||||
ok "Appended to $file: $line"
|
||||
fi
|
||||
}
|
||||
|
||||
# Same but with sudo (system files).
|
||||
ensure_line_sudo() {
|
||||
local line="$1"
|
||||
local file="$2"
|
||||
if sudo grep -qxF -- "$line" "$file" 2>/dev/null; then
|
||||
skip "Already in $(basename "$file"): $line"
|
||||
else
|
||||
echo "$line" | sudo tee -a "$file" >/dev/null
|
||||
ok "Appended to $file: $line"
|
||||
fi
|
||||
}
|
||||
Reference in New Issue
Block a user