Compare commits
No commits in common. "146c79a57e216287ed6e823144370b8673e60f55" and "5e0e96e8e2cba3293fa20ab603b6060eb48577e9" have entirely different histories.
146c79a57e
...
5e0e96e8e2
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
*.bak.*
|
|
||||||
178
README.md
178
README.md
@ -1,178 +0,0 @@
|
|||||||
# bootstrap
|
|
||||||
|
|
||||||
Turn a fresh Debian/Ubuntu/Arch VPS into a familiar workstation in one command. Detects the distro, installs a curated CLI package set, sets up zsh + oh-my-zsh with your ported config, brings your nvim config, hardens sane system defaults, and reports state.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```sh
|
|
||||||
curl -fsSL <shortener> | bash
|
|
||||||
# run a single step:
|
|
||||||
curl -fsSL <shortener> | bash -s -- --only packages
|
|
||||||
# run everything non-interactively and create a sudo user 'mk':
|
|
||||||
curl -fsSL <shortener> | bash -s -- --yes --username mk
|
|
||||||
```
|
|
||||||
|
|
||||||
The shortener points at the **raw `install.sh`** in this repo, which clones the
|
|
||||||
full repo to `~/.bootstrap` and runs `setup.sh`. To update: `cd ~/.bootstrap &&
|
|
||||||
git pull` (or just re-run the one-liner — it pulls existing clones).
|
|
||||||
|
|
||||||
Re-run individual steps locally after the initial bootstrap:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
bash ~/.bootstrap/setup.sh --only shell-zsh
|
|
||||||
bash ~/.bootstrap/setup.sh --only hardening
|
|
||||||
```
|
|
||||||
|
|
||||||
## Steps
|
|
||||||
|
|
||||||
| # | step | what it does |
|
|
||||||
|---|-------------|--------------------------------------------------------------------|
|
|
||||||
| 0 | preflight | verify distro + network + privileges |
|
|
||||||
| 1 | create-user | (when direct root) create a passwordless sudo user, copy root's authorized_keys, retarget config at them |
|
|
||||||
| 2 | packages | git, byobu, ufw, mosh, btop, eza, fzf, ripgrep, bat, zoxide, jq, fd, tree, ncdu, lazygit, starship, … (+ `yay` on Arch) |
|
|
||||||
| 3 | docker | Docker engine + compose v2 plugin, `$TARGET_USER` in `docker` group |
|
|
||||||
| 4 | angie | Angie (nginx fork) web server |
|
|
||||||
| 5 | shell-zsh | zsh + oh-my-zsh + plugins, set default shell, ship config (to `~$TARGET_USER/.config/zsh`) |
|
|
||||||
| 6 | nvm | nvm → `~$TARGET_USER/.nvm` + Node LTS |
|
|
||||||
| 7 | nvim | clone `g.mk.fo/bootstrap/nvim-minimal` → `~$TARGET_USER/.config/nvim` |
|
|
||||||
| 8 | hardening | ssh (key-only root), ufw, updates, unattended-upgrades, tz, hostname, swapfile, locale, fail2ban, **angie config layout + built-in ACME** |
|
|
||||||
| 9 | sanity | read-only report + recommended manual follow-ups |
|
|
||||||
|
|
||||||
Every step is idempotent — re-running is safe.
|
|
||||||
|
|
||||||
## Sudo user (when running as direct root)
|
|
||||||
|
|
||||||
Running the one-liner as **direct root** (no `SUDO_USER`) is the common
|
|
||||||
provider-provisioned-VPS case. The `create-user` step then:
|
|
||||||
|
|
||||||
- **Arch**: auto-creates a non-root sudoer (AUR/`makepkg` can't run as root),
|
|
||||||
with a `y/N` confirm on the username (default `mk`).
|
|
||||||
- **Debian/Ubuntu**: asks `y/N` whether to create one (optional — apt works
|
|
||||||
as root).
|
|
||||||
- Always when sudoed-root (`SUDO_USER` set): uses that invoking user as the target,
|
|
||||||
syncing their sudoers + ssh keys instead of creating a new account.
|
|
||||||
- Never when not root: you're already a normal user; yourself is the target.
|
|
||||||
|
|
||||||
Flags:
|
|
||||||
- `--create-user` force the prompt regardless of distro,
|
|
||||||
- `--username NAME` use that name and skip the name prompt.
|
|
||||||
|
|
||||||
The created user:
|
|
||||||
- is added to `wheel` (Arch) or `sudo`+`adm` (Debian), shell set to zsh,
|
|
||||||
- has **no password** (`passwd -d`) — SSH key login only (you'll disable ssh
|
|
||||||
password auth in the hardening step),
|
|
||||||
- gets `/root/.ssh/authorized_keys` copied to `~user/.ssh/` (chmod 600/700),
|
|
||||||
with the fingerprints printed so you can sanity-check they're yours,
|
|
||||||
- gets `/etc/sudoers.d/<user>`: `Defaults:<user> !tty_tickets`,
|
|
||||||
`Defaults:<user> timestamp_timeout=10080` (7-day cache), and
|
|
||||||
`<user> ALL=(ALL) NOPASSWD:ALL`. Validated with `visudo -c` before install.
|
|
||||||
|
|
||||||
All later config steps (zsh/omz, nvim, nvm, docker group) target
|
|
||||||
`~$TARGET_USER`. After bootstrap, **test `ssh <user>@<host>` from a new
|
|
||||||
terminal before closing the root session.**
|
|
||||||
|
|
||||||
## Zsh plugins enabled (oh-my-zsh)
|
|
||||||
|
|
||||||
`git sudo command-not-found colored-man-pages extract copypath dirhistory`
|
|
||||||
plus, cloned into `$ZSH_CUSTOM`: `zsh-autosuggestions`,
|
|
||||||
`zsh-syntax-highlighting` (loaded last), `zsh-completions`,
|
|
||||||
`history-substring-search`, `fzf-tab`. Prompt: **starship**.
|
|
||||||
|
|
||||||
## Config layout (shipped to `~/.config/zsh`)
|
|
||||||
|
|
||||||
```
|
|
||||||
config/zsh/
|
|
||||||
zshrc # main config (sources the pieces below)
|
|
||||||
path.zsh # PATH + env (XDG, EDITOR, LANG, nvm, pnpm)
|
|
||||||
aliases.zsh # ported from fish + curated defaults
|
|
||||||
aliases-extras.zsh # your personal additions (preserved across re-runs)
|
|
||||||
functions.zsh # mcd, conf, so, ssh-wrapper, dot-expansion
|
|
||||||
plugins.zsh # omz plugin list + load oh-my-zsh
|
|
||||||
```
|
|
||||||
|
|
||||||
`~/.zshrc` is a tiny shim that sets `ZDOTDIR=~/.config/zsh` and sources the
|
|
||||||
real config there — keeps omz "happy" while your config lives under XDG.
|
|
||||||
|
|
||||||
## Hardening choices
|
|
||||||
|
|
||||||
- **SSH**: `PermitRootLogin prohibit-password` + `PasswordAuthentication no`.
|
|
||||||
Root key login stays available; password login is disabled. A backup is
|
|
||||||
made and `sshd -t` runs before reload. **Always keep your current session
|
|
||||||
open and test a new login before closing it.**
|
|
||||||
- **ufw**: deny incoming, allow `22/tcp`, `60000:61000/udp` (mosh),
|
|
||||||
`80,443/tcp` (angie).
|
|
||||||
- **updates + unattended-upgrades** (Debian), system upgrade on Arch.
|
|
||||||
- **timezone** (default UTC), **hostname** (optional).
|
|
||||||
- **swapfile** 2G if RAM < 2G and no swap present.
|
|
||||||
- **locale** en_US.UTF-8.
|
|
||||||
- **fail2ban** with an sshd jail.
|
|
||||||
- **angie config** — see [Angie config layout](#angie-config-layout) below.
|
|
||||||
|
|
||||||
## Supported hosts
|
|
||||||
|
|
||||||
- Debian / Ubuntu (via apt)
|
|
||||||
- Arch (via pacman + yay for AUR)
|
|
||||||
- Architectures: `amd64`, `arm64`
|
|
||||||
|
|
||||||
## Angie config layout
|
|
||||||
|
|
||||||
The hardening step sets up a sane `/etc/angie/` structure (Angie is an nginx fork; same config syntax):
|
|
||||||
|
|
||||||
```
|
|
||||||
/etc/angie/
|
|
||||||
angie.conf # managed by bootstrap (written once; not overwritten on re-runs)
|
|
||||||
mime.types # shipped by the package — left alone
|
|
||||||
_on/*.conf # enabled hosts (sites-enabled alt.) ← included from http{}
|
|
||||||
targets/*.conf # all host definitions (sites-available alt.)
|
|
||||||
modules/*.conf # top-level context snippets (stream{}, env, load_module, ...)
|
|
||||||
modules/http/*.conf # http{}-level snippets (upstreams, maps, real_ip, proxy defaults)
|
|
||||||
```
|
|
||||||
|
|
||||||
Convention: write each host in `targets/<name>.conf`, then enable it with
|
|
||||||
`angie-enable <name>` (symlinks `targets/<name>.conf` → `_on/<name>.conf`, validates,
|
|
||||||
reloads). Disable with `angie-disable <name>`. Both helpers are installed to
|
|
||||||
`/usr/local/bin/`.
|
|
||||||
|
|
||||||
The root `angie.conf` ships: `worker_processes auto`, `server_tokens off`,
|
|
||||||
gzip, `client_max_body_size 16m`, a main `log_format`, and includes
|
|
||||||
`modules/http/*.conf` then `_on/*.conf`. `user`/`pid`/`error_log`/`load_module`
|
|
||||||
directives are extracted from the original packaged `angie.conf` and preserved
|
|
||||||
(so Debian's `user angie` vs Arch's `user http` is carried over). A backup is
|
|
||||||
kept at `/etc/angie/angie.conf.orig.bootstrap`.
|
|
||||||
|
|
||||||
Defaults shipped:
|
|
||||||
- `modules/http/00-common.conf` — `real_ip` from private ranges (Cloudflare commented), `proxy_set_header` defaults (inherited by every server/location).
|
|
||||||
- `_on/00-default.conf` — `default_server` returning `444` for requests that match no enabled host (drops drive-by scanners / bare-IP probes).
|
|
||||||
- `targets/example-https.conf.example` — a TLS server template using Angie's built-in ACME.
|
|
||||||
- `angie-issue` helper — create a TLS host with a Let's Encrypt cert (no certbot).
|
|
||||||
|
|
||||||
## TLS / Let's Encrypt (Angie built-in ACME — no certbot)
|
|
||||||
|
|
||||||
Angie ships the `http_acme` module, so it obtains **and renews** TLS certificates
|
|
||||||
itself — no certbot, no separate renewal cron/timer. The hardening step ships an
|
|
||||||
example TLS host and an `angie-issue` helper:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
angie-issue example.com you@email.com # first call: writes the shared
|
|
||||||
# acme_client (Let's Encrypt) + email
|
|
||||||
angie-issue www.example.com # later domains reuse the same client
|
|
||||||
```
|
|
||||||
|
|
||||||
All enabled hosts using `acme default` share one certificate covering every
|
|
||||||
`server_name` that references it; Angie requests it on reload and renews before
|
|
||||||
expiry. The shared client lives at `/etc/angie/modules/http/acme.conf` (with a
|
|
||||||
`resolver`, required by `acme_client`). HTTP→HTTPS redirect, `http2`, and the cert
|
|
||||||
are wired into the target template — replace `location /` with your app/proxy.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Distro-guarded package-manager aliases ship in `aliases.zsh`:
|
|
||||||
`pac`/`pacs`/`pacu`/`pacy`/`pa` (Arch, `sudo pacman …`) and
|
|
||||||
`apt`/`apti`/`aptu`/`apts`/`aptsh` (Debian/Ubuntu, `sudo apt …`).
|
|
||||||
- `aliases-extras.zsh` is seeded once and then left alone — re-running the
|
|
||||||
shell-zsh step won't clobber your personal aliases.
|
|
||||||
- On Debian, `fd`/`bat` ship as `fdfind`/`batcat`; symlinks to the canonical
|
|
||||||
names are created in `~/.local/bin` (which is on `PATH` via `path.zsh`).
|
|
||||||
- Some Go/Rust CLIs (`dust`, `duf`, `procs`, `lazygit`) are fetched from
|
|
||||||
the latest GitHub release tarball when the distro package is missing; this
|
|
||||||
is best-effort and warned (not fatal) on failure.
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
# aliases-extras.zsh — YOUR personal additions go here.
|
|
||||||
# This file is preserved across re-runs of the shell-zsh step (not overwritten
|
|
||||||
# from the repo). Add your taskwarrior / neomutt / cargo / eso aliases here.
|
|
||||||
|
|
||||||
# Examples (uncomment + edit):
|
|
||||||
# alias tw='taskwarrior-tui'
|
|
||||||
# alias ta='task add'
|
|
||||||
# alias tl='task list'
|
|
||||||
# alias neomutt='neomutt -f ~/.local/share/mail/you/All\ Mail/'
|
|
||||||
# alias carwash="cargo-watch -x run"
|
|
||||||
# alias wttr="curl v2.wttr.in/59.95,30.35?FQM" # your city coords
|
|
||||||
@ -1,76 +0,0 @@
|
|||||||
# aliases.zsh — ported from ~/.config/fish/config.fish (server-relevant subset)
|
|
||||||
# Plus curated defaults. Edit aliases-extras.zsh for personal additions.
|
|
||||||
|
|
||||||
# ── editor ──────────────────────────────────────────────────────────────────
|
|
||||||
alias v="$EDITOR"
|
|
||||||
alias vi="$EDITOR"
|
|
||||||
alias vim="$EDITOR"
|
|
||||||
|
|
||||||
# ── ls / eza ───────────────────────────────────────────────────────────────
|
|
||||||
alias lso="/bin/ls" # plain ls escape hatch
|
|
||||||
if command -v eza >/dev/null 2>&1; then
|
|
||||||
alias ls="eza -l --group-directories-first --icons"
|
|
||||||
alias ll="eza -la --group-directories-first --icons"
|
|
||||||
alias la="eza -a --group-directories-first --icons"
|
|
||||||
alias lt="eza --tree --level=2 --icons"
|
|
||||||
else
|
|
||||||
alias ls="ls --color=auto -l"
|
|
||||||
alias ll="ls -la --color=auto"
|
|
||||||
alias la="ls -a --color=auto"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── cat / bat ───────────────────────────────────────────────────────────────
|
|
||||||
if command -v bat >/dev/null 2>&1; then
|
|
||||||
alias cat="bat --paging=never"
|
|
||||||
fi
|
|
||||||
alias catn="/bin/cat"
|
|
||||||
|
|
||||||
# ── file ops (verbose + interactive safety) ────────────────────────────────
|
|
||||||
alias cp="cp -iv"
|
|
||||||
alias mv="mv -iv"
|
|
||||||
alias rm="rm -iv"
|
|
||||||
alias ln="ln -sv"
|
|
||||||
alias mkdir="mkdir -pv"
|
|
||||||
alias md="mkdir"
|
|
||||||
|
|
||||||
# ── git ─────────────────────────────────────────────────────────────────────
|
|
||||||
alias g="git"
|
|
||||||
alias gcl="git clone"
|
|
||||||
alias gst="git status"
|
|
||||||
alias gd="git diff"
|
|
||||||
alias gco="git checkout"
|
|
||||||
|
|
||||||
# ── systemd ──────────────────────────────────────────────────────────────────
|
|
||||||
alias sy="sudo systemctl"
|
|
||||||
alias srv="sudo systemctl"
|
|
||||||
|
|
||||||
# ── sudo / misc ──────────────────────────────────────────────────────────────
|
|
||||||
alias sudoo="sudo -E -s"
|
|
||||||
alias chmox="chmod +x"
|
|
||||||
|
|
||||||
# ── network ─────────────────────────────────────────────────────────────────
|
|
||||||
alias whoamip="curl --silent https://ipinfo.io"
|
|
||||||
alias wttr="curl v2.wttr.in"
|
|
||||||
alias wtr="wttr"
|
|
||||||
|
|
||||||
# ── byobu ────────────────────────────────────────────────────────────────────
|
|
||||||
alias byb="byobu"
|
|
||||||
alias bybou="byobu"
|
|
||||||
|
|
||||||
# ── pacman (Arch only) ──────────────────────────────────────────────────────
|
|
||||||
if command -v pacman >/dev/null 2>&1; then
|
|
||||||
alias pac="sudo pacman"
|
|
||||||
alias pacs="sudo pacman -Sy"
|
|
||||||
alias pacu="sudo pacman -Syyu"
|
|
||||||
alias pacy="sudo pacman -Sy"
|
|
||||||
alias pa="sudo pacman"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── apt (Debian/Ubuntu only) ───────────────────────────────────────────────
|
|
||||||
if command -v apt-get >/dev/null 2>&1; then
|
|
||||||
alias apt="sudo apt"
|
|
||||||
alias apti="sudo apt install"
|
|
||||||
alias aptu="sudo apt update && sudo apt upgrade"
|
|
||||||
alias apts="sudo apt search"
|
|
||||||
alias aptsh="sudo apt show"
|
|
||||||
fi
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
# functions.zsh — ported from fish functions (server-relevant subset)
|
|
||||||
|
|
||||||
# make dir + cd into it
|
|
||||||
mcd() { mkdir -p "$1" && cd "$1"; }
|
|
||||||
|
|
||||||
# reload zsh config
|
|
||||||
so() { source "${ZDOTDIR:-$HOME/.config/zsh}/zshrc" 2>/dev/null || source "$HOME/.zshrc"; }
|
|
||||||
|
|
||||||
# cd into ~/.config, edit/open the named file/dir (interactive)
|
|
||||||
conf() {
|
|
||||||
local base="${XDG_CONFIG_HOME:-$HOME/.config}"
|
|
||||||
cd "$base" || return
|
|
||||||
if [[ -f "$1" ]]; then
|
|
||||||
echo "Editing $1"; $EDITOR "$1"
|
|
||||||
elif [[ -d "$1" ]]; then
|
|
||||||
echo "Opening config dir $1"; cd "$1" && ls -al
|
|
||||||
else
|
|
||||||
read "confirm?Can't find $1, open in \$EDITOR? (Y/n) "
|
|
||||||
if [[ -z "$confirm" || "$confirm" =~ ^[Yy]$ ]]; then
|
|
||||||
$EDITOR "$1"
|
|
||||||
else
|
|
||||||
echo "Exiting…"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ssh onward with a sane TERM (avoids terminfo breakage on older boxes)
|
|
||||||
ssh() { TERM=xterm command ssh "$@"; }
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
# path.zsh — PATH + environment (ported from ~/.config/fish/config.fish, server subset)
|
|
||||||
|
|
||||||
# XDG
|
|
||||||
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
|
|
||||||
export XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
|
|
||||||
export XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
|
|
||||||
export XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}"
|
|
||||||
|
|
||||||
export EDITOR="nvim"
|
|
||||||
export LANG="en_US.UTF-8"
|
|
||||||
export LC_COLLATE="C"
|
|
||||||
export PAGER="less"
|
|
||||||
export LESS="-R" # render ANSI colors in less
|
|
||||||
export BAT_THEME="base16"
|
|
||||||
export LESSHISTFILE="$XDG_STATE_HOME/less/history"
|
|
||||||
[[ -d "$XDG_STATE_HOME/less" ]] || mkdir -p "$XDG_STATE_HOME/less"
|
|
||||||
|
|
||||||
# user-local binaries (fd/bat shims, gh-release binaries, pip installs, ...)
|
|
||||||
case ":$PATH:" in
|
|
||||||
*":$HOME/.local/bin:"*) ;;
|
|
||||||
*) export PATH="$HOME/.local/bin:$PATH" ;;
|
|
||||||
esac
|
|
||||||
case ":$PATH:" in
|
|
||||||
*":/usr/local/bin:"*) ;;
|
|
||||||
*) export PATH="/usr/local/bin:$PATH" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# nvm (installed by the nvm step) — loaded lazily via its init snippet below
|
|
||||||
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
|
|
||||||
[[ -s "$NVM_DIR/nvm.sh" ]] && source "$NVM_DIR/nvm.sh"
|
|
||||||
|
|
||||||
# pnpm (if present)
|
|
||||||
if [[ -d "$HOME/.local/share/pnpm" ]] && ! [[ ":$PATH:" == *":$HOME/.local/share/pnpm:"* ]]; then
|
|
||||||
export PATH="$HOME/.local/share/pnpm:$PATH"
|
|
||||||
fi
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
# plugins.zsh — enable oh-my-zsh builtins + external plugins (cloned by the
|
|
||||||
# shell-zsh module into $ZSH_CUSTOM/plugins).
|
|
||||||
|
|
||||||
plugins=(
|
|
||||||
git
|
|
||||||
sudo
|
|
||||||
command-not-found
|
|
||||||
colored-man-pages
|
|
||||||
extract
|
|
||||||
copypath
|
|
||||||
dirhistory
|
|
||||||
zsh-autosuggestions
|
|
||||||
zsh-syntax-highlighting # must be last among syntax plugins
|
|
||||||
history-substring-search
|
|
||||||
fzf-tab
|
|
||||||
zsh-completions
|
|
||||||
)
|
|
||||||
|
|
||||||
source "$ZSH/oh-my-zsh.sh"
|
|
||||||
|
|
||||||
# history-substring-search keybindings (after omz is loaded)
|
|
||||||
bindkey '^[[A' history-substring-search-up
|
|
||||||
bindkey '^[[B' history-substring-search-down
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
# ~/.zshrc shim (managed by bootstrap) sources this file via ZDOTDIR.
|
|
||||||
# Edit freely; re-run the shell-zsh step to re-sync from the repo if needed.
|
|
||||||
|
|
||||||
# ── oh-my-zsh bootstrap ────────────────────────────────────────────────────
|
|
||||||
export ZSH="${ZSH:-$HOME/.oh-my-zsh}"
|
|
||||||
export ZSH_CUSTOM="${ZSH_CUSTOM:-$ZSH/custom}"
|
|
||||||
|
|
||||||
# theme disabled — we use starship (init at the bottom)
|
|
||||||
ZSH_THEME=""
|
|
||||||
|
|
||||||
# casewise history
|
|
||||||
HISTFILE="$HOME/.local/state/zsh/history"
|
|
||||||
HISTSIZE=100000
|
|
||||||
SAVEHIST=100000
|
|
||||||
setopt EXTENDED_HISTORY SHARE_HISTORY HIST_IGNORE_DUPS HIST_IGNORE_SPACE HIST_VERIFY
|
|
||||||
setopt AUTO_CD PUSHD_IGNORE_DUPS PROMPT_SUBST
|
|
||||||
setopt INTERACTIVE_COMMENTS
|
|
||||||
|
|
||||||
# load the modular pieces (order matters)
|
|
||||||
[[ -f "$ZDOTDIR/path.zsh" ]] && source "$ZDOTDIR/path.zsh"
|
|
||||||
[[ -f "$ZDOTDIR/aliases.zsh" ]] && source "$ZDOTDIR/aliases.zsh"
|
|
||||||
[[ -f "$ZDOTDIR/aliases-extras.zsh" ]] && source "$ZDOTDIR/aliases-extras.zsh"
|
|
||||||
[[ -f "$ZDOTDIR/functions.zsh" ]] && source "$ZDOTDIR/functions.zsh"
|
|
||||||
|
|
||||||
# ── completion styling (set before omz runs compinit) ───────────────
|
|
||||||
autoload -Uz compinit
|
|
||||||
zstyle ':completion:*' menu select
|
|
||||||
zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}'
|
|
||||||
zstyle ':completion:*' list-colors ${(s.:.)LS_COLORS}
|
|
||||||
|
|
||||||
# oh-my-zsh + plugins (omz runs compinit internally; lists our plugins)
|
|
||||||
[[ -f "$ZDOTDIR/plugins.zsh" ]] && source "$ZDOTDIR/plugins.zsh"
|
|
||||||
|
|
||||||
# ── standard zsh niceties ─────────────────────────────────────────────
|
|
||||||
autoload -U edit-command-line
|
|
||||||
zle -N edit-command-line
|
|
||||||
bindkey '^X^E' edit-command-line
|
|
||||||
|
|
||||||
|
|
||||||
# ... -> ../.. expansion (ported from fish)
|
|
||||||
rationalise-dot() {
|
|
||||||
if [[ $LBUFFER = *.. ]]; then LBUFFER+=/; else LBUFFER+=.; fi
|
|
||||||
}
|
|
||||||
zle -N rationalise-dot
|
|
||||||
bindkey . rationalise-dot
|
|
||||||
|
|
||||||
# ── keychain (commented) — fill in the key labels you use on this box ────
|
|
||||||
# if command -v keychain >/dev/null 2>&1; then
|
|
||||||
# keychain --quiet --nogui id_ed25519 # <- your server-relevant key(s) here
|
|
||||||
# [[ -f "$HOME/.keychain/$(hostname)-sh" ]] && source "$HOME/.keychain/$(hostname)-sh"
|
|
||||||
# fi
|
|
||||||
|
|
||||||
# ── integrations ────────────────────────────────────────────────────────────
|
|
||||||
command -v zoxide >/dev/null 2>&1 && eval "$(zoxide init zsh)"
|
|
||||||
command -v fzf >/dev/null 2>&1 && [[ -f "$HOME/.fzf.zsh" ]] && source "$HOME/.fzf.zsh"
|
|
||||||
# ── starship prompt ─────────────────────────────────────────────────────────
|
|
||||||
if command -v starship >/dev/null 2>&1; then
|
|
||||||
eval "$(starship init zsh)"
|
|
||||||
fi
|
|
||||||
@ -2,15 +2,9 @@
|
|||||||
# prompt.sh — plain y/N prompts.
|
# prompt.sh — plain y/N prompts.
|
||||||
|
|
||||||
# ask "prompt?" [default y|n] -> sets REPLY
|
# ask "prompt?" [default y|n] -> sets REPLY
|
||||||
# Under ${ALL:-0} (the --yes flag) assume the stated default without
|
|
||||||
# reading stdin, so a fully non-interactive run never blocks on a read.
|
|
||||||
ask() {
|
ask() {
|
||||||
local p="$1" d="${2:-y}" hint
|
local p="$1" d="${2:-y}" hint
|
||||||
if [[ "$d" == "y" ]]; then hint="[Y/n]"; else hint="[y/N]"; fi
|
if [[ "$d" == "y" ]]; then hint="[Y/n]"; else hint="[y/N]"; fi
|
||||||
if (( ${ALL:-0} )); then
|
|
||||||
REPLY="$d"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
read -rp "$p $hint " REPLY
|
read -rp "$p $hint " REPLY
|
||||||
[[ -z "$REPLY" ]] && REPLY="$d"
|
[[ -z "$REPLY" ]] && REPLY="$d"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,157 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# 05-create-user — create a daily-driver sudo user when running as direct root.
|
|
||||||
#
|
|
||||||
# Why this exists: `makepkg` (yay / AUR) refuses to run as root. On a fresh VPS
|
|
||||||
# that lands you in as root with no SUDO_USER, we create a non-root sudoer,
|
|
||||||
# copy root's authorized_keys to it, configure passwordless sudo with
|
|
||||||
# !tty_tickets + a long timestamp_timeout, and retarget the config steps
|
|
||||||
# (zsh, nvim, nvm, docker group) at that user's $HOME.
|
|
||||||
#
|
|
||||||
# Behaviour:
|
|
||||||
# - Not root -> skip (you're already a normal user).
|
|
||||||
# - Root + SUDO_USER set -> skip (you sudoed in; that user is the target).
|
|
||||||
# - Root + no SUDO_USER + Arch -> auto-create (AUR needs it), confirm name.
|
|
||||||
# - Root + no SUDO_USER + Debian/Ubuntu -> ask y/N (optional, apt works as root).
|
|
||||||
# - --create-user flag -> force prompt regardless.
|
|
||||||
# - --username NAME -> override default name, no prompt.
|
|
||||||
# - --yes (no --username) -> default to 'mk' silently (no stall).
|
|
||||||
#
|
|
||||||
# Passwordless, no user password (NOPASSWD). Idempotent: re-runs re-sync
|
|
||||||
# authorized_keys + sudoers rather than failing.
|
|
||||||
|
|
||||||
step_name="create-user"
|
|
||||||
step_desc="create a sudo user (passwordless) + copy root's ssh keys + retarget config"
|
|
||||||
step_run="run_create_user"
|
|
||||||
|
|
||||||
run_create_user() {
|
|
||||||
log "create-user step"
|
|
||||||
|
|
||||||
# not root -> nothing to do, target stays as current user
|
|
||||||
if (( EUID != 0 )); then
|
|
||||||
echo " not root — target stays $USER ($HOME)"
|
|
||||||
export TARGET_USER="$USER" TARGET_HOME="$HOME"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
# root but came in via sudo -> use that invoking user, don't create a new one
|
|
||||||
if [[ -z "${SUDO_USER:-}" ]]; then _handle_direct_root; else _handle_sudoed_root; fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── direct root (logged in as root; the case that actually needs a new user) ─
|
|
||||||
_handle_direct_root() {
|
|
||||||
local should_create=0
|
|
||||||
|
|
||||||
if (( CREATE_USER )) || [[ -n "${USERNAME_ARG:-}" ]]; then
|
|
||||||
[[ -n "${USERNAME_ARG:-}" ]] && log "--username given: forcing user creation"
|
|
||||||
should_create=1
|
|
||||||
elif [[ "$DISTRO" == "arch" ]]; then
|
|
||||||
# AUR/makepkg cannot run as root — we genuinely need a non-root sudoer.
|
|
||||||
echo " running as direct root on Arch: a non-root sudoer is needed for yay/AUR."
|
|
||||||
yn "Create a sudo user now? (recommended)" y && should_create=1
|
|
||||||
else
|
|
||||||
# Debian/Ubuntu: apt works as root, so this is convenience (key-first login).
|
|
||||||
yn "Create a daily-driver sudo user? (optional — apt works as root)" n && should_create=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if (( ! should_create )); then
|
|
||||||
echo " skipping user creation — config will target root ($HOME)"
|
|
||||||
warn "on Arch you won't be able to build AUR packages (yay/angie) as root"
|
|
||||||
export TARGET_USER="root" TARGET_HOME="$HOME"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
local user="${USERNAME_ARG:-}"
|
|
||||||
if [[ -z "$user" ]]; then
|
|
||||||
local default="mk"
|
|
||||||
read -rp " username [$default]: " user
|
|
||||||
[[ -z "$user" ]] && user="$default"
|
|
||||||
fi
|
|
||||||
|
|
||||||
_create_or_sync_user "$user"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── root via sudo: use the invoking user, don't create another ───────────────
|
|
||||||
_handle_sudoed_root() {
|
|
||||||
local user="$SUDO_USER"
|
|
||||||
local home; home="$(getent passwd "$user" | cut -d: -f6)"
|
|
||||||
log "running as root via sudo — using invoking user '$user' as target"
|
|
||||||
# ensure they have passwordless sudo + the ssh key + sudo group, then retarget
|
|
||||||
_create_or_sync_user "$user" skip_create
|
|
||||||
export TARGET_USER="$user" TARGET_HOME="$home"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── the meat: create if missing, sync sudoers, sync authorized_keys ──────────
|
|
||||||
_create_or_sync_user() { # username [skip_create]
|
|
||||||
local user="$1"
|
|
||||||
local skip_create="${2:-}"
|
|
||||||
|
|
||||||
if [[ "$skip_create" != "skip_create" ]]; then
|
|
||||||
if id "$user" >/dev/null 2>&1; then
|
|
||||||
ok "user '$user' already exists — syncing config only"
|
|
||||||
else
|
|
||||||
local shell_bin; shell_bin="$(command -v zsh 2>/dev/null || echo /bin/bash)"
|
|
||||||
local groups
|
|
||||||
case "$DISTRO" in
|
|
||||||
arch) groups="wheel" ;;
|
|
||||||
debian) groups="sudo,adm" ;;
|
|
||||||
esac
|
|
||||||
log "creating user '$user' (shell=$shell_bin, groups=$groups)"
|
|
||||||
useradd -m -s "$shell_bin" -G "$groups" "$user"
|
|
||||||
# passwordless account: clear the password field entirely
|
|
||||||
passwd -d "$user" >/dev/null 2>&1 || true
|
|
||||||
# unlock the account so SSH key login still works after passwd -d
|
|
||||||
usermod -p "" "$user" 2>/dev/null || true
|
|
||||||
ok "user '$user' created (no password set; ssh-key login only)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
local home; home="$(getent passwd "$user" | cut -d: -f6)"
|
|
||||||
|
|
||||||
_sync_authorized_keys "$user" "$home"
|
|
||||||
_install_sudoers "$user"
|
|
||||||
|
|
||||||
export TARGET_USER="$user" TARGET_HOME="$home"
|
|
||||||
ok "config target retargeted: user=$user home=$home"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── copy root's authorized_keys to the target user (idempotent) ──────────────
|
|
||||||
_sync_authorized_keys() { # user home
|
|
||||||
local user="$1" home="$2"
|
|
||||||
local src="/root/.ssh/authorized_keys"
|
|
||||||
if [[ ! -f "$src" ]]; then
|
|
||||||
warn "no /root/.ssh/authorized_keys — add your key manually:"
|
|
||||||
echo " install -d -m 700 -o $user -g $user $home/.ssh"
|
|
||||||
echo " # then: paste your pubkey into $home/.ssh/authorized_keys (chmod 600)"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
install -d -m 700 -o "$user" -g "$user" "$home/.ssh"
|
|
||||||
install -m 600 -o "$user" -g "$user" "$src" "$home/.ssh/authorized_keys"
|
|
||||||
local n; n=$(grep -cE '^[^#]' "$src" 2>/dev/null || echo 0)
|
|
||||||
ok "copied $n authorized key(s) to $home/.ssh/authorized_keys"
|
|
||||||
warn "sanity-check these are YOUR keys (fingerprints):"
|
|
||||||
ssh-keygen -lf "$src" 2>/dev/null | sed 's/^/ /' || true
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── write /etc/sudoers.d/<user>: NOPASSWD, !tty_tickets, 7-day cache ─────────
|
|
||||||
_install_sudoers() { # user
|
|
||||||
local user="$1" file="/etc/sudoers.d/${user}"
|
|
||||||
log "configuring sudoers for '$user' (passwordless, !tty_tickets, 7d timeout)"
|
|
||||||
# write to a temp, validate with visudo -c, then move into place atomically
|
|
||||||
local tmp; tmp="$(mktemp)"
|
|
||||||
cat > "$tmp" <<EOF
|
|
||||||
# managed by bootstrap — passwordless sudo for $user
|
|
||||||
Defaults:$user !tty_tickets
|
|
||||||
Defaults:$user timestamp_timeout=10080
|
|
||||||
$user ALL=(ALL) NOPASSWD:ALL
|
|
||||||
EOF
|
|
||||||
# visudo must see proper perms (0440) to be happy
|
|
||||||
chmod 0440 "$tmp"
|
|
||||||
if visudo -cf "$tmp" >/dev/null 2>&1; then
|
|
||||||
install -m 0440 -o root -g root "$tmp" "$file"
|
|
||||||
ok "sudoers installed: $file"
|
|
||||||
else
|
|
||||||
err "visudo -c rejected the sudoers snippet — NOT installed, inspect aborted"
|
|
||||||
cat "$tmp" | sed 's/^/ /'
|
|
||||||
fi
|
|
||||||
rm -f "$tmp"
|
|
||||||
}
|
|
||||||
@ -24,13 +24,13 @@ run_docker() {
|
|||||||
warn "systemctl unavailable — start docker manually if needed"
|
warn "systemctl unavailable — start docker manually if needed"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# add the config target user to the docker group (created-user or self)
|
# add current user to docker group
|
||||||
if getent group docker >/dev/null 2>&1; then
|
if getent group docker >/dev/null 2>&1; then
|
||||||
if id -nG "${TARGET_USER:-$USER}" | grep -qw docker; then
|
if id -nG "$USER" | grep -qw docker; then
|
||||||
ok "${TARGET_USER:-$USER} already in docker group"
|
ok "$USER already in docker group"
|
||||||
else
|
else
|
||||||
sudo_ usermod -aG docker "${TARGET_USER:-$USER}"
|
sudo_ usermod -aG docker "$USER"
|
||||||
ok "${TARGET_USER:-$USER} added to docker group (log out/in or 'newgrp docker' to use it)"
|
ok "$USER added to docker group (log out/in or 'newgrp docker' to use it)"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,31 +5,18 @@ step_name="shell-zsh"
|
|||||||
step_desc="zsh + oh-my-zsh + plugins, set default shell, ship config"
|
step_desc="zsh + oh-my-zsh + plugins, set default shell, ship config"
|
||||||
step_run="run_zsh"
|
step_run="run_zsh"
|
||||||
|
|
||||||
# All paths resolve against the config TARGET (set by 05-create-user).
|
ZSH="${ZSH:-$HOME/.oh-my-zsh}"
|
||||||
ZSH="${ZSH:-$TARGET_HOME/.oh-my-zsh}"
|
|
||||||
ZSH_CUSTOM="${ZSH_CUSTOM:-$ZSH/custom}"
|
ZSH_CUSTOM="${ZSH_CUSTOM:-$ZSH/custom}"
|
||||||
ZDOTDIR_TARGET="${ZDOTDIR_TARGET:-$TARGET_HOME/.config/zsh}"
|
ZDOTDIR_TARGET="${ZDOTDIR_TARGET:-$HOME/.config/zsh}"
|
||||||
|
|
||||||
run_zsh() {
|
run_zsh() {
|
||||||
log "installing zsh + oh-my-zsh"
|
log "installing zsh + oh-my-zsh"
|
||||||
_install_zsh
|
_install_zsh
|
||||||
_install_omz
|
_install_omz
|
||||||
_install_plugins
|
_install_plugins
|
||||||
_chown_target_tree
|
|
||||||
_ship_config
|
_ship_config
|
||||||
_set_default_shell
|
_set_default_shell
|
||||||
if [[ "$TARGET_USER" != "$USER" ]]; then
|
ok "zsh ready — start it with: exec zsh"
|
||||||
log "zsh ready. Start it as $TARGET_USER: sudo -iu $TARGET_USER"
|
|
||||||
else
|
|
||||||
ok "zsh ready — start it with: exec zsh"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
_chown_target_tree() {
|
|
||||||
# when running as root for a created user, make omz + plugins + config owned by them
|
|
||||||
if [[ "$TARGET_USER" == "$USER" ]]; then return; fi
|
|
||||||
local grp; grp="$(id -gn "$TARGET_USER" 2>/dev/null || echo "$TARGET_USER")"
|
|
||||||
chown -R "$TARGET_USER:$grp" "$ZSH" "$ZDOTDIR_TARGET" "$TARGET_HOME/.zshrc" 2>/dev/null || true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_install_zsh() {
|
_install_zsh() {
|
||||||
@ -68,6 +55,7 @@ _ship_config() {
|
|||||||
log "shipping zsh config to $ZDOTDIR_TARGET"
|
log "shipping zsh config to $ZDOTDIR_TARGET"
|
||||||
mkdir -p "$ZDOTDIR_TARGET"
|
mkdir -p "$ZDOTDIR_TARGET"
|
||||||
local f
|
local f
|
||||||
|
local f
|
||||||
for f in zshrc aliases.zsh functions.zsh path.zsh plugins.zsh; do
|
for f in zshrc aliases.zsh functions.zsh path.zsh plugins.zsh; do
|
||||||
local src="$BOOT_DIR/config/zsh/$f"
|
local src="$BOOT_DIR/config/zsh/$f"
|
||||||
[[ -f "$src" ]] || continue
|
[[ -f "$src" ]] || continue
|
||||||
@ -79,8 +67,8 @@ _ship_config() {
|
|||||||
if [[ ! -f "$ZDOTDIR_TARGET/aliases-extras.zsh" ]]; then
|
if [[ ! -f "$ZDOTDIR_TARGET/aliases-extras.zsh" ]]; then
|
||||||
cp "$BOOT_DIR/config/zsh/aliases-extras.zsh" "$ZDOTDIR_TARGET/aliases-extras.zsh"
|
cp "$BOOT_DIR/config/zsh/aliases-extras.zsh" "$ZDOTDIR_TARGET/aliases-extras.zsh"
|
||||||
fi
|
fi
|
||||||
# ~/.zshrc shim that loads our real config via ZDOTDIR
|
# ~/.zshrc shim that loads our real config via ZDOTDIR (so omz $ZSH + plugins resolve)
|
||||||
local shim="$TARGET_HOME/.zshrc"
|
local shim="$HOME/.zshrc"
|
||||||
if ! grep -q 'BOOTSTRAP ZDOTDIR' "$shim" 2>/dev/null; then
|
if ! grep -q 'BOOTSTRAP ZDOTDIR' "$shim" 2>/dev/null; then
|
||||||
cat >> "$shim" <<'EOF'
|
cat >> "$shim" <<'EOF'
|
||||||
|
|
||||||
@ -89,11 +77,7 @@ export ZDOTDIR="${ZDOTDIR:-$HOME/.config/zsh}"
|
|||||||
[[ -f "$ZDOTDIR/zshrc" ]] && source "$ZDOTDIR/zshrc"
|
[[ -f "$ZDOTDIR/zshrc" ]] && source "$ZDOTDIR/zshrc"
|
||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
# ensure the whole shipped tree is owned by the target user
|
ok "zsh config installed ($ZDOTDIR_TARGET) + ~/.zshrc shim"
|
||||||
if [[ "$TARGET_USER" != "$USER" ]]; then
|
|
||||||
chown -R "$TARGET_USER:$(id -gn "$TARGET_USER" 2>/dev/null || echo "$TARGET_USER")" "$ZDOTDIR_TARGET" "$shim"
|
|
||||||
fi
|
|
||||||
ok "zsh config installed ($ZDOTDIR_TARGET) + $shim shim"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_set_default_shell() {
|
_set_default_shell() {
|
||||||
@ -103,13 +87,13 @@ _set_default_shell() {
|
|||||||
if ! grep -qx "$zsh_bin" /etc/shells 2>/dev/null; then
|
if ! grep -qx "$zsh_bin" /etc/shells 2>/dev/null; then
|
||||||
echo "$zsh_bin" | sudo_ tee -a /etc/shells >/dev/null
|
echo "$zsh_bin" | sudo_ tee -a /etc/shells >/dev/null
|
||||||
fi
|
fi
|
||||||
if [[ "$(getent passwd "$TARGET_USER" | cut -d: -f7)" != "$zsh_bin" ]]; then
|
if [[ "$SHELL" != "$zsh_bin" ]] && [[ "$(getent passwd "$USER" | cut -d: -f7)" != "$zsh_bin" ]]; then
|
||||||
if yn "Set zsh as default login shell for $TARGET_USER?" y; then
|
if yn "Set zsh as default login shell for $USER?" y; then
|
||||||
sudo_ chsh -s "$zsh_bin" "$TARGET_USER" && ok "default shell set to zsh for $TARGET_USER"
|
sudo_ chsh -s "$zsh_bin" "$USER" && ok "default shell set to zsh"
|
||||||
else
|
else
|
||||||
warn "zsh not set as default — run 'chsh -s $zsh_bin' for $TARGET_USER' later"
|
warn "zsh not set as default — run 'chsh -s $zsh_bin' later"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
ok "zsh already the default shell for $TARGET_USER"
|
ok "zsh already your default shell"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@ -2,25 +2,30 @@
|
|||||||
# 35-nvm — Node Version Manager + Node LTS.
|
# 35-nvm — Node Version Manager + Node LTS.
|
||||||
|
|
||||||
step_name="nvm"
|
step_name="nvm"
|
||||||
step_desc="nvm into ~$TARGET_USER/.nvm, install Node LTS"
|
step_desc="nvm into ~/.nvm, install Node LTS"
|
||||||
step_run="run_nvm"
|
step_run="run_nvm"
|
||||||
|
|
||||||
run_nvm() {
|
run_nvm() {
|
||||||
log "installing nvm for $TARGET_USER"
|
log "installing nvm"
|
||||||
local nvm_home="$TARGET_HOME/.nvm"
|
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
|
||||||
export NVM_DIR="$nvm_home"
|
|
||||||
|
|
||||||
if [[ -s "$NVM_DIR/nvm.sh" ]]; then
|
if [[ -s "$NVM_DIR/nvm.sh" ]]; then
|
||||||
ok "nvm already present at $NVM_DIR"
|
ok "nvm already present at $NVM_DIR"
|
||||||
else
|
else
|
||||||
# fetch the install script at a pinned NVM version (update here to upgrade)
|
# fetch the install script at a pinned NVM version (update here to upgrade)
|
||||||
as_user env NVM_DIR="$nvm_home" bash -c 'curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash' \
|
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash \
|
||||||
|| { warn "nvm install failed"; return; }
|
|| { warn "nvm install failed"; return; }
|
||||||
ok "nvm installed to $NVM_DIR"
|
ok "nvm installed to $NVM_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# run node/lts install as the target user so binary ownership + default are theirs
|
# shellcheck disable=SC1091
|
||||||
as_user bash -lc 'export NVM_DIR='"$NVM_DIR"'; source "$NVM_DIR/nvm.sh"; nvm install --lts && nvm use --lts && nvm alias default lts/*' \
|
source "$NVM_DIR/nvm.sh"
|
||||||
|| warn "node lts install had issues (continue; run 'nvm install --lts' as $TARGET_USER)"
|
if command -v nvm >/dev/null 2>&1; then
|
||||||
as_user bash -lc 'export NVM_DIR='"$NVM_DIR"'; source "$NVM_DIR/nvm.sh"; command -v node >/dev/null && echo " node \$(node -v) active for $TARGET_USER"' 2>/dev/null || true
|
log "installing Node LTS via nvm"
|
||||||
|
nvm install --lts || warn "nvm install --lts failed"
|
||||||
|
nvm use --lts 2>/dev/null || true
|
||||||
|
nvm alias default 'lts/*' 2>/dev/null || true
|
||||||
|
command -v node >/dev/null 2>&1 && ok "node $(node -v) active"
|
||||||
|
else
|
||||||
|
warn "nvm.sh sourced but nvm not available — check $NVM_DIR"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# 40-nvim — clone your nvim-minimal config from gitea to ~$TARGET_USER/.config/nvim.
|
# 40-nvim — clone your nvim-minimal config from gitea to ~/.config/nvim.
|
||||||
|
|
||||||
step_name="nvim"
|
step_name="nvim"
|
||||||
step_desc="clone g.mk.fo/bootstrap/nvim-minimal -> ~$TARGET_USER/.config/nvim"
|
step_desc="clone g.mk.fo/bootstrap/nvim-minimal -> ~/.config/nvim"
|
||||||
step_run="run_nvim"
|
step_run="run_nvim"
|
||||||
|
|
||||||
NVIM_REPO_URL="${NVIM_REPO_URL:-https://g.mk.fo/bootstrap/nvim-minimal.git}"
|
NVIM_REPO_URL="${NVIM_REPO_URL:-https://g.mk.fo/bootstrap/nvim-minimal.git}"
|
||||||
@ -11,22 +11,24 @@ run_nvim() {
|
|||||||
log "installing nvim config"
|
log "installing nvim config"
|
||||||
command -v nvim >/dev/null 2>&1 || warn "nvim binary not found — install the packages step first"
|
command -v nvim >/dev/null 2>&1 || warn "nvim binary not found — install the packages step first"
|
||||||
|
|
||||||
local target="$TARGET_HOME/.config/nvim"
|
local target="${XDG_CONFIG_HOME:-$HOME/.config}/nvim"
|
||||||
if [[ -d "$target/.git" ]]; then
|
if [[ -d "$target/.git" ]]; then
|
||||||
log "existing nvim config is a git repo — pulling"
|
log "existing nvim config is a git repo — pulling"
|
||||||
as_user git -C "$target" pull --ff-only || warn "could not pull existing nvim config"
|
git -C "$target" pull --ff-only || warn "could not pull existing nvim config"
|
||||||
ok "nvim config updated at $target"
|
ok "nvim config updated at $target"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -e "$target" ]]; then
|
if [[ -e "$target" ]]; then
|
||||||
local bak="${target}.bak.$(date +%Y%m%d-%H%M%S)"
|
local bak="${target}.bak.$(date +%Y%m%d-%H%M%S)"
|
||||||
as_user mv "$target" "$bak" 2>/dev/null || sudo_ mv "$target" "$bak"
|
mv "$target" "$bak"
|
||||||
warn "backed up existing $target -> $bak"
|
warn "backed up existing $target -> $bak"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
as_user git clone "$NVIM_REPO_URL" "$target" && ok "nvim config cloned to $target" \
|
if git clone "$NVIM_REPO_URL" "$target"; then
|
||||||
|| { err "failed to clone $NVIM_REPO_URL"; sudo_ git clone "$NVIM_REPO_URL" "$target" \
|
ok "nvim config cloned to $target"
|
||||||
&& chown -R "$TARGET_USER:$(id -gn "$TARGET_USER" 2>/dev/null || echo "$TARGET_USER")" "$target" \
|
else
|
||||||
&& ok "nvim config cloned to $target (as root, then chowned)"; }
|
err "failed to clone $NVIM_REPO_URL"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
@ -2,7 +2,7 @@
|
|||||||
# 50-hardening — system defaults. Each sub-step y/N, all idempotent.
|
# 50-hardening — system defaults. Each sub-step y/N, all idempotent.
|
||||||
|
|
||||||
step_name="hardening"
|
step_name="hardening"
|
||||||
step_desc="ssh, ufw, updates, unattended, tz, hostname, swap, locale, fail2ban, angie config + built-in ACME"
|
step_desc="ssh(ufw-safe), ufw, updates, unattended, tz, hostname, swap, locale, fail2ban"
|
||||||
step_run="run_hardening"
|
step_run="run_hardening"
|
||||||
|
|
||||||
run_hardening() {
|
run_hardening() {
|
||||||
@ -14,8 +14,6 @@ run_hardening() {
|
|||||||
_h_swap
|
_h_swap
|
||||||
_h_locale
|
_h_locale
|
||||||
_h_fail2ban
|
_h_fail2ban
|
||||||
_h_angie_config
|
|
||||||
_h_angie_acme
|
|
||||||
ok "hardening pass complete"
|
ok "hardening pass complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,12 +103,8 @@ _h_tz_hostname() {
|
|||||||
if command -v timedatectl >/dev/null 2>&1; then
|
if command -v timedatectl >/dev/null 2>&1; then
|
||||||
if yn "Set timezone (default UTC)?" y; then
|
if yn "Set timezone (default UTC)?" y; then
|
||||||
local tz="${REPLY_TZ:-}"
|
local tz="${REPLY_TZ:-}"
|
||||||
if (( ${ALL:-0} )); then
|
read -rp " timezone [UTC]: " tz
|
||||||
tz="UTC"; ok "--yes: timezone defaulting to UTC"
|
[[ -z "$tz" ]] && tz="UTC"
|
||||||
else
|
|
||||||
read -rp " timezone [UTC]: " tz
|
|
||||||
[[ -z "$tz" ]] && tz="UTC"
|
|
||||||
fi
|
|
||||||
sudo_ timedatectl set-timezone "$tz" && ok "timezone set to $tz"
|
sudo_ timedatectl set-timezone "$tz" && ok "timezone set to $tz"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
@ -192,266 +186,3 @@ EOF
|
|||||||
fi
|
fi
|
||||||
ok "fail2ban installed + sshd jail enabled"
|
ok "fail2ban installed + sshd jail enabled"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── angie config: _on/ targets/ modules/ + sane root config ────────────────
|
|
||||||
_h_angie_config() {
|
|
||||||
if ! command -v angie >/dev/null 2>&1; then
|
|
||||||
echo " skip angie config (angie not installed — run the angie step first)"; return
|
|
||||||
fi
|
|
||||||
if ! yn "Set up Angie config layout (/etc/angie/_on targets modules) + sane root config?" y; then
|
|
||||||
echo " skip angie config"; return
|
|
||||||
fi
|
|
||||||
|
|
||||||
local d=/etc/angie
|
|
||||||
sudo_ install -d -m 0755 "$d/_on" "$d/targets" "$d/modules" "$d/modules/http"
|
|
||||||
|
|
||||||
# ── extract packaging-specific values from the stock angie.conf ──
|
|
||||||
# Angie packages set user/pid/error_log differently per distro; preserve them.
|
|
||||||
local oldcfg="$d/angie.conf" angie_user angie_pid angie_err angie_modules=""
|
|
||||||
if [[ -f "$oldcfg" ]]; then
|
|
||||||
angie_user=$(grep -E '^\s*user\s+' "$oldcfg" | head -1 | awk '{print $2}' | tr -d ';')
|
|
||||||
angie_pid=$(grep -E '^\s*pid\s+' "$oldcfg" | head -1 | awk '{print $2}' | tr -d ';')
|
|
||||||
angie_err=$(grep -E '^\s*error_log\s+' "$oldcfg" | head -1 | awk '{print $2}' | tr -d ';')
|
|
||||||
# collect any load_module lines (dynamic modules)
|
|
||||||
angie_modules=$(grep -E '^\s*load_module\s+' "$oldcfg" || true)
|
|
||||||
fi
|
|
||||||
# sane defaults if not found
|
|
||||||
[[ -z "$angie_user" ]] && angie_user=$({ [[ "$DISTRO" == "arch" ]] && echo http || echo angie; })
|
|
||||||
[[ -z "$angie_pid" ]] && angie_pid=/run/angie.pid
|
|
||||||
[[ -z "$angie_err" ]] && angie_err=/var/log/angie/error.log
|
|
||||||
|
|
||||||
# backup the original once
|
|
||||||
if [[ -f "$oldcfg" && ! -f "$oldcfg.orig.bootstrap" ]]; then
|
|
||||||
sudo_ cp -a "$oldcfg" "$oldcfg.orig.bootstrap"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── write our root angie.conf (once — guarded, won't clobber later edits) ──
|
|
||||||
if grep -q '# managed by bootstrap' "$oldcfg" 2>/dev/null; then
|
|
||||||
ok "$oldcfg already bootstrap-managed — leaving it (keeps your edits)"
|
|
||||||
else
|
|
||||||
sudo_ tee "$oldcfg" >/dev/null <<EOF
|
|
||||||
# angie.conf — managed by bootstrap. Original backup: angie.conf.orig.bootstrap
|
|
||||||
# Edit freely; re-running the hardening step will NOT overwrite (it writes once).
|
|
||||||
|
|
||||||
user $angie_user;
|
|
||||||
worker_processes auto;
|
|
||||||
pid $angie_pid;
|
|
||||||
error_log $angie_err warn;
|
|
||||||
|
|
||||||
# dynamic modules carried over from the original package config (if any)
|
|
||||||
$angie_modules
|
|
||||||
# top-level context snippets (stream{}, env, etc.)
|
|
||||||
include /etc/angie/modules/*.conf;
|
|
||||||
|
|
||||||
events {
|
|
||||||
worker_connections 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
http {
|
|
||||||
include /etc/angie/mime.types;
|
|
||||||
default_type application/octet-stream;
|
|
||||||
|
|
||||||
log_format main '\$remote_addr - \$remote_user [\$time_local] "\$request" '
|
|
||||||
'\$status \$body_bytes_sent "\$http_referer" "\$http_user_agent"';
|
|
||||||
access_log /var/log/angie/access.log main;
|
|
||||||
|
|
||||||
sendfile on;
|
|
||||||
tcp_nopush on;
|
|
||||||
tcp_nodelay on;
|
|
||||||
keepalive_timeout 65;
|
|
||||||
types_hash_max_size 2048;
|
|
||||||
|
|
||||||
server_tokens off;
|
|
||||||
client_max_body_size 16m;
|
|
||||||
|
|
||||||
gzip on;
|
|
||||||
gzip_vary on;
|
|
||||||
gzip_proxied any;
|
|
||||||
gzip_comp_level 5;
|
|
||||||
gzip_min_length 256;
|
|
||||||
gzip_types text/plain text/css application/json application/javascript
|
|
||||||
text/xml application/xml application/xml+rss text/javascript;
|
|
||||||
|
|
||||||
# http-level snippets: upstreams, maps, real_ip, proxy defaults
|
|
||||||
include /etc/angie/modules/http/*.conf;
|
|
||||||
|
|
||||||
# enabled hosts (symlinks from targets/*). _on/*.conf only — targets/ is staging.
|
|
||||||
include /etc/angie/_on/*.conf;
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── http-level common snippet (real_ip + proxy defaults) ──
|
|
||||||
if [[ ! -f "$d/modules/http/00-common.conf" ]]; then
|
|
||||||
sudo_ tee "$d/modules/http/00-common.conf" >/dev/null <<'EOF'
|
|
||||||
# real_ip — trust private ranges + (commented Cloudflare). Uncomment/edit for your upstream.
|
|
||||||
set_real_ip_from 10.0.0.0/8;
|
|
||||||
set_real_ip_from 172.16.0.0/12;
|
|
||||||
set_real_ip_from 192.168.0.0/16;
|
|
||||||
set_real_ip_from 169.254.0.0/16;
|
|
||||||
# set_real_ip_from 173.245.48.0/20; # Cloudflare, see https://www.cloudflare.com/ips/
|
|
||||||
real_ip_header X-Forwarded-For;
|
|
||||||
real_ip_recursive on;
|
|
||||||
|
|
||||||
# proxy defaults (inherited by every server/location unless overridden)
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── default catch-all server: unmatched Host -> drop ──
|
|
||||||
if [[ ! -f "$d/_on/00-default.conf" ]]; then
|
|
||||||
sudo_ tee "$d/_on/00-default.conf" >/dev/null <<'EOF'
|
|
||||||
# default_server: drop requests that match no enabled host (drive-by scanners, bare-IP)
|
|
||||||
server {
|
|
||||||
listen 80 default_server;
|
|
||||||
listen [::]:80 default_server;
|
|
||||||
listen 443 default_server;
|
|
||||||
listen [::]:443 default_server;
|
|
||||||
server_name _;
|
|
||||||
return 444;
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── angie-enable / angie-disable helpers ──
|
|
||||||
_angie_helpers
|
|
||||||
|
|
||||||
# validate + (best-effort) reload
|
|
||||||
if sudo_ angie -t 2>&1; then
|
|
||||||
ok "angie config valid"
|
|
||||||
if command -v systemctl >/dev/null 2>&1 && systemctl is-active angie >/dev/null 2>&1; then
|
|
||||||
sudo_ systemctl reload angie 2>/dev/null && ok "angie reloaded"
|
|
||||||
else
|
|
||||||
echo " start angie when ready: sudo systemctl enable --now angie"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
err "angie -t failed — review $d/angie.conf (backup at $oldcfg.orig.bootstrap)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
ok "angie layout ready: $d/{angie.conf,_on/,targets/,modules/,modules/http/}"
|
|
||||||
echo " enable a site: angie-enable <name> (after writing $d/targets/<name>.conf)"
|
|
||||||
echo " disable a site: angie-disable <name>"
|
|
||||||
|
|
||||||
# ufw already opened 80/443 in _h_ufw
|
|
||||||
}
|
|
||||||
|
|
||||||
_angie_helpers() {
|
|
||||||
sudo_ tee /usr/local/bin/angie-enable >/dev/null <<'EOF'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
# angie-enable <name> — symlink targets/<name>.conf into _on/ and validate.
|
|
||||||
set -euo pipefail
|
|
||||||
name="${1:-}"
|
|
||||||
[ -n "$name" ] || { echo "usage: angie-enable <name>" >&2; exit 2; }
|
|
||||||
target="/etc/angie/targets/${name}.conf"
|
|
||||||
link="/etc/angie/_on/${name}.conf"
|
|
||||||
[ -f "$target" ] || { echo "no such target: $target" >&2; exit 1; }
|
|
||||||
ln -sfn "../targets/${name}.conf" "$link"
|
|
||||||
echo "enabled: $name -> $link"
|
|
||||||
if command -v angie >/dev/null 2>&1; then
|
|
||||||
sudo angie -t || { echo "config invalid — review before reload" >&2; exit 1; }
|
|
||||||
sudo systemctl reload angie && echo "reloaded"
|
|
||||||
fi
|
|
||||||
EOF
|
|
||||||
sudo_ tee /usr/local/bin/angie-disable >/dev/null <<'EOF'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
# angie-disable <name> — remove the _on/<name>.conf symlink and reload.
|
|
||||||
set -euo pipefail
|
|
||||||
name="${1:-}"
|
|
||||||
[ -n "$name" ] || { echo "usage: angie-disable <name>" >&2; exit 2; }
|
|
||||||
link="/etc/angie/_on/${name}.conf"
|
|
||||||
[ -L "$link" ] || [ -e "$link" ] || { echo "$name not enabled ($link missing)" >&2; exit 1; }
|
|
||||||
rm -f "$link"
|
|
||||||
echo "disabled: $name"
|
|
||||||
if command -v angie >/dev/null 2>&1; then
|
|
||||||
sudo angie -t || true
|
|
||||||
sudo systemctl reload angie && echo "reloaded"
|
|
||||||
fi
|
|
||||||
EOF
|
|
||||||
sudo_ chmod +x /usr/local/bin/angie-enable /usr/local/bin/angie-disable
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── angie built-in ACME (Let's Encrypt) ────────────────────────────────────
|
|
||||||
# No certbot: Angie ships http_acme and fetches/renews certs itself.
|
|
||||||
# Bootstrap can't know your domain/email, so we ship an example TLS server
|
|
||||||
# target + an `angie-issue` helper that creates the live acme_client on demand.
|
|
||||||
_h_angie_acme() {
|
|
||||||
if ! command -v angie >/dev/null 2>&1; then
|
|
||||||
echo " skip angie ACME (angie not installed)"; return
|
|
||||||
fi
|
|
||||||
if ! yn "Ship Angie built-in ACME template + angie-issue helper?" y; then
|
|
||||||
echo " skip angie ACME"; return
|
|
||||||
fi
|
|
||||||
|
|
||||||
local d=/etc/angie
|
|
||||||
# example TLS server target — NOT auto-enabled (.example, so angie-enable won't glob it)
|
|
||||||
if [[ ! -f "$d/targets/example-https.conf.example" ]]; then
|
|
||||||
sudo_ tee "$d/targets/example-https.conf.example" >/dev/null <<'EOF'
|
|
||||||
# Template for a TLS host using Angie built-in ACME.
|
|
||||||
# Use it via the helper: angie-issue <domain> [you@email]
|
|
||||||
# Or by hand: copy to targets/<domain>.conf, fill <DOMAIN>, then: angie-enable <domain>
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
listen [::]:80;
|
|
||||||
listen 443 ssl;
|
|
||||||
listen [::]:443 ssl;
|
|
||||||
http2 on;
|
|
||||||
server_name <DOMAIN>;
|
|
||||||
|
|
||||||
acme default; # add this server_name to the shared 'default' ACME certificate
|
|
||||||
ssl_certificate $acme_cert_default;
|
|
||||||
ssl_certificate_key $acme_cert_key_default;
|
|
||||||
|
|
||||||
# http -> https redirect
|
|
||||||
if ($scheme = http) { return 301 https://$host$request_uri; }
|
|
||||||
|
|
||||||
location / {
|
|
||||||
# replace with your app / proxy_pass upstream;
|
|
||||||
return 200 "angie + builtin ACME ok\n";
|
|
||||||
add_header Content-Type text/plain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
|
|
||||||
sudo_ tee /usr/local/bin/angie-issue >/dev/null <<'EOF'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
# angie-issue <domain> [email] — create a TLS host backed by Angie built-in ACME.
|
|
||||||
# First call (needs email) writes /etc/angie/modules/http/acme.conf with the
|
|
||||||
# shared 'default' acme_client; every call writes targets/<domain>.conf and enables it.
|
|
||||||
# All enabled domains using `acme default` share one cert covering all their server_names.
|
|
||||||
set -euo pipefail
|
|
||||||
domain="${1:-}"; email="${2:-}"
|
|
||||||
[ -n "$domain" ] || { echo "usage: angie-issue <domain> [email]" >&2; exit 2; }
|
|
||||||
acme_conf=/etc/angie/modules/http/acme.conf
|
|
||||||
|
|
||||||
if [ ! -f "$acme_conf" ]; then
|
|
||||||
[ -n "$email" ] || { echo "first-time: also pass your email for Let's Encrypt" >&2; exit 2; }
|
|
||||||
cat > "$acme_conf" <<ACME
|
|
||||||
# managed by angie-issue — Angie built-in ACME (Let's Encrypt)
|
|
||||||
# resolver is required by the acme_client directive (http context).
|
|
||||||
resolver 1.1.1.1 8.8.8.8 valid=300s ipv6=off;
|
|
||||||
acme_client default https://acme-v02.api.letsencrypt.org/directory email=${email};
|
|
||||||
ACME
|
|
||||||
echo "wrote $acme_conf (email=${email})"
|
|
||||||
else
|
|
||||||
echo "_existing $acme_conf (shared default client)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
target=/etc/angie/targets/${domain}.conf
|
|
||||||
sed -e "s|<DOMAIN>|${domain}|g" /etc/angie/targets/example-https.conf.example > "$target"
|
|
||||||
echo "wrote $target"
|
|
||||||
ln -sfn "../targets/${domain}.conf" /etc/angie/_on/${domain}.conf
|
|
||||||
echo "enabled: $domain"
|
|
||||||
sudo angie -t
|
|
||||||
sudo systemctl reload angie && echo "angie reloaded — certificate is obtained automatically."
|
|
||||||
EOF
|
|
||||||
sudo_ chmod +x /usr/local/bin/angie-issue
|
|
||||||
|
|
||||||
ok "angie ACME ready: example host at $d/targets/example-https.conf.example"
|
|
||||||
echo " issue a cert: angie-issue <domain> you@email"
|
|
||||||
echo " then reload picks it up; Angie renews automatically before expiry."
|
|
||||||
}
|
|
||||||
32
setup.sh
32
setup.sh
@ -4,20 +4,13 @@
|
|||||||
# Detects distro/arch, lists modular steps, asks y/N per step, runs them.
|
# Detects distro/arch, lists modular steps, asks y/N per step, runs them.
|
||||||
#
|
#
|
||||||
# Flags:
|
# Flags:
|
||||||
# --yes run all steps without prompting
|
# --yes run all steps without prompting
|
||||||
# --only NAME run only the step matching NAME
|
# --only NAME run only the step matching NAME
|
||||||
# --skip NAME run all steps except NAME
|
# --skip NAME run all steps except NAME (repeatable in future)
|
||||||
# --create-user force creating a sudo user (otherwise Arch auto-creates, Debian asks)
|
# -h|--help usage
|
||||||
# --username NAME override the sudo user name (default: mk)
|
|
||||||
# -h|--help usage
|
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Default config target = whoever is running this. The create-user step
|
|
||||||
# (05-create-user) may override these to point at a freshly-created sudoer.
|
|
||||||
export TARGET_USER="$USER"
|
|
||||||
export TARGET_HOME="$HOME"
|
|
||||||
|
|
||||||
BOOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
BOOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
@ -25,25 +18,18 @@ source "$BOOT_DIR/lib/detect.sh"
|
|||||||
source "$BOOT_DIR/lib/log.sh"
|
source "$BOOT_DIR/lib/log.sh"
|
||||||
source "$BOOT_DIR/lib/prompt.sh"
|
source "$BOOT_DIR/lib/prompt.sh"
|
||||||
|
|
||||||
# helper: resolve the target user's real home even when we're root operating
|
ALL=0; ONLY=""; SKIP=""
|
||||||
# on a created user (whose home isn't $HOME).
|
|
||||||
target_home() { getent passwd "${TARGET_USER:-$USER}" | cut -d: -f6; }
|
|
||||||
|
|
||||||
ALL=0; ONLY=""; SKIP=""; CREATE_USER=0; USERNAME_ARG=""
|
|
||||||
while (( $# )); do
|
while (( $# )); do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--yes|-y) ALL=1; shift ;;
|
--yes|-y) ALL=1; shift ;;
|
||||||
--only) ONLY="${2:-}"; shift 2 ;;
|
--only) ONLY="${2:-}"; shift 2 ;;
|
||||||
--skip) SKIP="${2:-}"; shift 2 ;;
|
--skip) SKIP="${2:-}"; shift 2 ;;
|
||||||
--create-user) CREATE_USER=1; shift ;;
|
|
||||||
--username) USERNAME_ARG="${2:-}"; shift 2 ;;
|
|
||||||
-h|--help)
|
-h|--help)
|
||||||
grep -E '^# --' "$0" | sed 's/^# //'
|
echo "usage: setup.sh [--yes] [--only NAME] [--skip NAME]"
|
||||||
exit 0 ;;
|
exit 0 ;;
|
||||||
*) echo "unknown arg: $1" >&2; shift ;;
|
*) echo "unknown arg: $1" >&2; shift ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
export CREATE_USER USERNAME_ARG
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- collect steps
|
# ---------------------------------------------------------------- collect steps
|
||||||
NAMES=(); DESCS=(); RUNS=()
|
NAMES=(); DESCS=(); RUNS=()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user