#!/bin/sh # # plow-here.sh — plow ground that's already broken: run AS the account the # nerd will live in, clone the stock into its home, and hand # off to install.sh. The no-sudo sibling of the plows. # # This script contains no sudo and refuses to run as root — it cannot ask # for privileges it was built not to have. Read it and check: "sudo" is # never run here; it appears only in prose and one group-name check. # # Usage (as the service user — never root): # ./plow-here.sh [-repo ] [-branch ] [-dir ] # [-no-install] [-- ] # # curl -fsSL https://plow.smolnerd.farm | sh # the whole thing # curl -fsSL https://plow.smolnerd.farm | less # read it first # # © Fork Development Corp. Fetching and running this plow is free — the # licensed part is the nerd, and install.sh is that gate (see LICENSE). # # The fence is the requirement, the plow is one way to dig: if you made # the dedicated account yourself (System Settings, adduser, your MDM), # the ground is broken. This script does the sequence from step 2: move # the software in and boot. Nothing here needs root, so nothing here asks. # # -repo accepts: owner/repo | https://github.com/owner/repo(.git) # git@github.com:owner/repo.git | ssh://... # set -u SCRIPT_NAME=$(basename "${0:-}") case "$SCRIPT_NAME" in sh|bash|zsh|dash|tmp.*|"") SCRIPT_NAME="plow-here.sh" ;; esac die() { printf '%s: error: %s\n' "$SCRIPT_NAME" "$*" >&2; exit 1; } info() { printf '==> %s\n' "$*"; } warn() { printf -- '--> warning: %s\n' "$*" >&2; } usage() { cat <] [-branch ] [-dir ] [-no-install] [-- ] Runs as the current (non-root) user: 1. clones the stock into ~/ 2. hands off to ./install.sh (the license gate; interactive) -repo repo to clone; owner/repo is fine (default: git@github.com:Fork-Development-Corp/smol-nerd.git) -branch branch or tag to check out (default: repo default) -dir clone into ~/ (default: the repo name) -no-install stop after the clone — plant when ready -this-account skip the dedicated-account question (scripted runs) -- everything after it goes to install.sh (e.g. -- --name mynerd --yes) -h, --help this text Contains no sudo; refuses to run as root. No account yet? Make one with your own system's tools — a Standard user on macOS (System Settings), adduser on Linux — named for your nerd. Then run this as that user. EOF } # The fence: a dedicated NON-admin account, dug with the tools you already # trust. We instruct rather than script it — the OS vendor maintains that # UI forever; sysadminctl across macOS versions is somebody else's bug. fence_guide() { printf '\n' >&2 printf ' Make the account on your own system — no script needed:\n' >&2 case "$(uname -s)" in Darwin) printf ' 1. System Settings -> Users & Groups -> Add User...\n' >&2 printf ' a STANDARD account (not admin), named for your nerd\n' >&2 printf ' 2. log in as it once, so the home folder exists\n' >&2 printf ' 3. as that user, run this same command again\n' >&2 ;; *) printf ' 1. sudo adduser yournerd # your distro'\''s ordinary user creation\n' >&2 printf ' 2. sudo -iu yournerd # become the new user\n' >&2 printf ' 3. run this same command again\n' >&2 ;; esac printf '\n' >&2 } # The whole body lives in main() for one reason: every advertised way to # run this feeds the script to a shell's STDIN (curl | sh, and the two # plows' `sh -s -- "$@" < plow-here.sh` handoffs). A shell reading a # script from stdin reads it command by command, so a top-level redirect # of fd 0 — which the tty reattach below must do, so install.sh can ask # its questions — would make the shell read its next command from the # terminal and silently abandon the rest of the file, exit 0. Parsing the # body as a function consumes it whole before any of it runs. # Regression test: tests/plow-here-delivery.test.sh main() { # ------------------------------------------------------------- arg parsing --- repo="git@github.com:Fork-Development-Corp/smol-nerd.git" branch="" clone_dir="" no_install=0 this_account=0 while [ $# -gt 0 ]; do case "$1" in -repo) [ $# -ge 2 ] || die "-repo requires a URL"; repo=$2; shift 2 ;; -branch) [ $# -ge 2 ] || die "-branch requires a ref"; branch=$2; shift 2 ;; -dir) [ $# -ge 2 ] || die "-dir requires a name"; clone_dir=$2; shift 2 ;; -no-install) no_install=1; shift ;; -this-account) this_account=1; shift ;; --) shift; break ;; -h|-help|--help) usage; exit 0 ;; *) usage >&2; die "unknown argument: $1 (install.sh flags go after --)" ;; esac done # "$@" now holds the install.sh pass-through args. # --------------------------------------------------------------- preflight --- if [ "$(id -u)" = "0" ]; then warn "refusing to run as root — this plow contains no sudo, and the nerd" warn "does not live in root's house." fence_guide exit 1 fi if ! command -v git >/dev/null 2>&1; then case "$(uname -s)" in Darwin) die "git not found — install the Command Line Tools first (xcode-select --install)" ;; *) die "git not found — install it first (e.g. apt install git / dnf install git)" ;; esac fi # curl | sh friendliness: when stdin is the pipe (the script itself), # reattach it to the terminal so questions work — including the fence # question just below. Headless runs (no tty) are unaffected. if [ ! -t 0 ] && (exec < /dev/tty) 2>/dev/null; then exec < /dev/tty fi # The fence question. Running on an admin account is almost always the # "I ran it as myself first" mistake — so instruct, default to No, and # let the deliberate say y (or pass -this-account). install.sh keeps its # own hard check either way. if id -Gn 2>/dev/null | tr ' ' '\n' | grep -qx -e admin -e sudo -e wheel; then if [ "$this_account" = 1 ]; then warn "admin account accepted (-this-account); install.sh will question it again" else warn "'$(id -un)' has admin rights — a nerd belongs behind a fence: a dedicated NON-admin account." fence_guide if [ -t 0 ]; then printf 'Continue on this admin account anyway? [y/N]: ' read -r _fence_ok || _fence_ok="" case "$_fence_ok" in y|Y|yes|YES) ;; *) info "good call — make the account, log in as it, run this same command."; exit 1 ;; esac else warn "no terminal to ask on — proceeding; install.sh will question it again" fi fi fi # Fence reachability — can an operator ssh IN to this account? Wrong # answers here cost a field visit: the first channel install found the # account healthy (su worked) while ssh bounced, because macOS Remote # Login was in "only these users" mode and the account wasn't in the # com.apple.access_ssh SACL group. Every remedy needs sudo, which this # plow has sworn off — so it complains with the exact command and MOVES # ON. Warn-only by doctrine: a loopback nerd is a complete nerd. _lb=$(ssh -o BatchMode=yes -o ConnectTimeout=3 -o StrictHostKeyChecking=accept-new \ "$(id -un)@127.0.0.1" true 2>&1) || true case "$_lb" in *"Connection refused"*) warn "sshd is not reachable on this box — remote tending and ssh -A planting won't work." case "$(uname -s)" in Darwin) warn " enable it: sudo systemsetup -setremotelogin on" ;; *) warn " enable it: sudo systemctl enable --now ssh (or sshd, per distro)" ;; esac ;; *) # Reachable. On macOS, Remote Login may still be restricted to a # SACL group — membership is checkable without sudo. if [ "$(uname -s)" = "Darwin" ] \ && dscl . -read /Groups/com.apple.access_ssh >/dev/null 2>&1 \ && ! dseditgroup -o checkmember -m "$(id -un)" com.apple.access_ssh >/dev/null 2>&1; then warn "Remote Login is restricted ('only these users') and '$(id -un)' is not on the allow-list —" warn "ssh to this account bounces until an operator runs:" warn " sudo dseditgroup -o edit -a $(id -un) -t user com.apple.access_ssh" fi ;; esac # GitHub auth probe — announce fate up front instead of failing mid-plant. # Informational only: some component repos are release-gated (private) # during the opening; with access (a key here, or a forwarded agent via # ssh -A) every clone completes today. Without it, public repos clone and # gated ones stop at the gate — which this makes known BEFORE any work. # Evaporates in relevance as the repos open. _probe=$(ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new \ -o ConnectTimeout=5 -T git@github.com 2>&1) || true case "$_probe" in *successfully\ authenticated*) _who=$(printf '%s' "$_probe" | sed -n 's/^Hi \([^!]*\)!.*/\1/p') info "GitHub access detected${_who:+ (authenticated as $_who)} — private components will clone" ;; *) info "no GitHub access detected — release-gated (private) components will stop at the gate." # The remedy is a credential this account owns, and minting one is # inside the no-sudo covenant: ssh-keygen writes only in this home. # Registration is the one step that stays human, so the contract is # mint -> show -> "run this same command again". A fresh key is # certainly unregistered, so no clone is attempted after minting. _farmkey="$HOME/.ssh/id_ed25519" if [ -f "$_farmkey.pub" ]; then info "this farm already has a key — it just isn't recognized. Its public half:" printf '\n'; cat "$_farmkey.pub"; printf '\n' info "register it (licensed installs: licensing@smolnerd.farm attaches it to the" info "machine seat; or add it to a GitHub account with access), or ssh -A instead." elif [ -t 0 ] && command -v ssh-keygen >/dev/null 2>&1; then printf 'Mint this farm its own key now? It never leaves this account. [Y/n]: ' read -r _mint || _mint="" case "$_mint" in n|N|no|NO) info "continuing without one — licensed installs: ssh -A, or request access" ;; *) umask 077 mkdir -p "$HOME/.ssh" || die "could not create $HOME/.ssh" # The cue, same as the sudo plow scaffolds: an empty # authorized_keys says "put an operator key here to ssh in". [ -e "$HOME/.ssh/authorized_keys" ] || : > "$HOME/.ssh/authorized_keys" ssh-keygen -q -t ed25519 -N "" \ -C "$(id -un)@$(uname -n) farm key" -f "$_farmkey" \ || die "ssh-keygen failed — nothing was created" info "key minted. Its public half (the private half never leaves this account):" printf '\n'; cat "$_farmkey.pub"; printf '\n' info "register it — licensed installs: send it to licensing@smolnerd.farm to" info "attach to the machine seat, or add it to a GitHub account with access." info "Then run this same command again; the clone will authenticate as this farm." exit 1 ;; esac else info "licensed installs: connect with a forwarded agent (ssh -A), add a key, or request access" fi ;; esac # owner/repo -> https://github.com/owner/repo.git case "$repo" in *://*|*@*:*) : ;; */*) repo="https://github.com/${repo%.git}.git" ;; *) die "unrecognized repo spec: '$repo'" ;; esac if [ -z "$clone_dir" ]; then clone_dir=$(basename "$repo") clone_dir=${clone_dir%.git} fi case "$clone_dir" in ""|.|..|*/*) die "invalid -dir '$clone_dir' — must be a single path component" ;; esac [ -n "${HOME:-}" ] && [ -d "$HOME" ] || die "\$HOME is not a directory — log in as the user properly (su - , or ssh)" target="$HOME/$clone_dir" [ -e "$target" ] && die "$target already exists" # -------------------------------------------------------------------- plan --- printf '\n' info "plowing here (ground already broken)" printf ' user : %s (uid %s)\n' "$(id -un)" "$(id -u)" printf ' home : %s\n' "$HOME" printf ' repo : %s%s\n' "$repo" "${branch:+ (branch: $branch)}" printf ' clone : %s\n' "$target" if [ "$no_install" = 1 ]; then printf ' then : nothing (-no-install) — plant when ready\n' else printf ' then : ./install.sh %s\n' "$*" fi printf '\n' # ------------------------------------------------------------------- clone --- # Forwarded ssh agent? (you ran `ssh -A nerd@host`.) Then git authenticates # private clones as your key — no credential is stored on this box. Announce # it so the operator-provisioning path (ssh-able fence, then plant over the # tunnel) is legible. BatchMode below doesn't block agent auth — it only # suppresses interactive password/passphrase prompts. if [ -n "${SSH_AUTH_SOCK:-}" ] && ssh-add -l >/dev/null 2>&1; then info "ssh agent forwarded — private clones authenticate as your key (nothing stored here)" fi # GIT_TERMINAL_PROMPT=0 and ssh BatchMode make a repo this account can't # reach fail fast instead of blocking on a prompt it can't answer. clone_rc=0 clone_log=$( env GIT_TERMINAL_PROMPT=0 \ GIT_SSH_COMMAND='ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new' \ GIT_CONFIG_NOSYSTEM=1 \ git clone ${branch:+--branch "$branch"} -- "$repo" "$target" 2>&1 ) || clone_rc=$? printf '%s\n' "$clone_log" | sed 's/^/ /' if [ "$clone_rc" != 0 ]; then printf '\n' warn "clone failed (exit $clone_rc) — nothing was changed except an empty attempt" case "$clone_log" in *"could not read Username"*|*"Authentication failed"*|*"terminal prompts disabled"*) printf '%s\n' " over HTTPS that means private — or no such repo (GitHub answers both the" >&2 printf '%s\n' " same way). Check the URL; if it's private, this account has no credentials" >&2 printf '%s\n' " yet: run 'gh auth login' (or switch to the SSH URL), then retry:" >&2 ;; *"Permission denied (publickey)"*) printf '%s\n' " that repo needs an SSH key this account does not have yet." >&2 printf '%s\n' " if you ssh'd in: reconnect with 'ssh -A' so git can borrow your key." >&2 printf '%s\n' " otherwise: run 'ssh-keygen -t ed25519', register the key, then retry:" >&2 ;; *"not found"*) printf '%s\n' " repo not found — check the URL, or it is private (see above). then retry:" >&2 ;; *) printf '%s\n' " retry with:" >&2 ;; esac printf '%s\n' " git clone ${branch:+--branch $branch }$repo $target" >&2 exit 1 fi info "clone ok: $(git -C "$target" log -1 --format='%h %s' 2>/dev/null)" # ----------------------------------------------------------------- handoff --- if [ "$no_install" = 1 ]; then printf '\n' info "done (clone only — plant when ready)" printf ' cd %s && ./install.sh\n\n' "$target" exit 0 fi cd "$target" || die "could not cd into $target" [ -x ./install.sh ] || die "no executable install.sh in $target — is -repo the stock?" printf '\n' info "handing off to install.sh (the license gate)" exec ./install.sh "$@" } main "$@"