omalocal: track upstream OMATERM installer, fix first-login tmux + Once

Brings the first-login server setup back to fully working on a fresh install:

- OMATERM: call install-native.sh directly. omaterm.org/install is now an
  interactive native/docker dispatcher that would stall the unattended
  first-run; the native branch is what a headless Arch box wants. Verified the
  closing `exec bash -l` and bundled AI agents (mise.packages) still hold.

- Rebrand the user-facing "OMATERM + Once" headline to "omalocal" (banner,
  setup prompt, closing reboot message); body still names the installed tools.

- Fix tmux "can't use /dev/tty" on first login. OMATERM's installer closes with
  `exec bash -l </dev/tty >/dev/tty 2>&1`; with fds on /dev/tty, tmux's client
  ttyname is the literal "/dev/tty", which tmux's server_client_open refuses.
  Strip that redirect from the downloaded installer and run it without
  redirecting our stdin, so the closing shell inherits the login pts
  (/dev/pts/N) — which tmux accepts. (script(1) did not work: OMATERM
  re-redirects to /dev/tty after it.)

- Fix Once not installing. Once installs before OMATERM (which used to provide
  Docker); Once requires Docker and can't install it on Arch (get.docker.com is
  unsupported there). Provision Docker in base: pacstrap docker, enable
  docker.service in the chroot, and add the install user to the docker group at
  install time, so the first-run Once install finds a running daemon and
  once/once-add need no sudo. OMATERM's later docker setup no-ops.

- gitignore dated build-*.log files.

Full chain confirmed working on a clean install: Welcome to omalocal -> Once
installs -> Omarchy-Send (optional) -> OMATERM -> lands in tmux.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
28allday 2026-05-31 13:29:29 +01:00
parent b1af2fe02d
commit 023b9394d3
2 changed files with 76 additions and 17 deletions

1
.gitignore vendored
View file

@ -6,3 +6,4 @@ omaterm-arch-*.iso
*.ppm
*.png
build.log
build-*.log

View file

@ -240,7 +240,7 @@ echo "==> Pacstrap (this is the slow bit)..."
pacstrap -K /mnt \
base linux linux-firmware intel-ucode amd-ucode \
networkmanager openssh sudo git curl vim less \
base-devel ufw avahi nss-mdns \
base-devel ufw avahi nss-mdns docker \
kitty-terminfo foot-terminfo ghostty-terminfo rxvt-unicode-terminfo
echo "==> Generating fstab..."
@ -263,6 +263,13 @@ systemctl enable sshd.service
# hosts: NSS line per the Arch wiki recommendation.
systemctl enable avahi-daemon.service
sed -i '/^hosts:/ s/\bresolve\b/mdns_minimal [NOTFOUND=return] resolve/' /etc/nsswitch.conf
# Docker is in base (not just from OMATERM) because the first-run flow installs
# Once BEFORE OMATERM, and Once requires Docker. Its installer's fallback is
# `curl get.once.com ... | get.docker.com | sh`, but the Docker convenience
# script does NOT support Arch and aborts — so Once must find Docker already
# present. Enable it here so the daemon is up by first login. OMATERM's later
# `pacman --needed docker` + enable are then no-ops.
systemctl enable docker.service
bootctl install
cat > /boot/loader/loader.conf <<LOADER
default arch.conf
@ -300,6 +307,12 @@ HOSTS
echo "==> Creating user $USERNAME..."
arch-chroot /mnt useradd -m -G wheel -s /bin/bash "$USERNAME"
printf 'root:%s\n%s:%s\n' "$USER_PW" "$USERNAME" "$USER_PW" | arch-chroot /mnt chpasswd
# Put the user in the docker group at install time (effective from first login),
# so the first-run Once install sees a usable daemon (docker info succeeds) and
# once/once-add work without sudo. groupadd -f is idempotent if the docker
# package already created the group.
arch-chroot /mnt groupadd -f docker
arch-chroot /mnt usermod -aG docker "$USERNAME"
# ----------------------------------------------------------- ssh handoff ----
if [ -f /root/master.pub ]; then
@ -375,7 +388,7 @@ clear
cat <<'BANNER'
============================================================
=== Welcome to OMATERM + Once ===
=== Welcome to omalocal ===
============================================================
This is a fresh Arch Linux server installed by omalocal.
@ -393,20 +406,20 @@ cat <<'BANNER'
and messaging (the firewall already opens 53317).
Installers (run any of these later if you skip):
curl -fsSL https://omaterm.org/install | bash
curl -fsSL https://raw.githubusercontent.com/omacom-io/omaterm/refs/heads/master/install-native.sh | bash
curl https://get.once.com | ONCE_INTERACTIVE=false sh
curl -fsSL https://raw.githubusercontent.com/28allday/omarchy-send/main/install.sh | bash
BANNER
setup_server=0
read -rp " Set up this server (OMATERM + Once) now? [Y/n] " ans </dev/tty
read -rp " Set up this server (omalocal) now? [Y/n] " ans </dev/tty
case "$ans" in
[Nn]*)
echo
echo " Skipped. To set up later, run:"
echo " curl https://get.once.com | ONCE_INTERACTIVE=false sh"
echo " curl -fsSL https://omaterm.org/install | bash"
echo " curl -fsSL https://raw.githubusercontent.com/omacom-io/omaterm/refs/heads/master/install-native.sh | bash"
echo
;;
*)
@ -415,6 +428,9 @@ case "$ans" in
# because its installer finishes with `exec bash -l`, hijacking this shell
# and dropping the user straight into their new tmux session — nothing after
# that call would ever run. So Once (and Omarchy-Send below) go first.
# Docker is provisioned in base (pacstrap + enabled in chroot) precisely so
# Once — which runs here, before OMATERM — finds a working daemon; Once can't
# install Docker itself on Arch (its get.docker.com fallback is unsupported).
echo
echo " [1/2] Installing Once..."
if curl https://get.once.com | ONCE_INTERACTIVE=false sh; then
@ -464,21 +480,58 @@ case "$ans" in
;;
esac
# OMATERM goes LAST. Its installer (omaterm.org/install) ends with `exec bash -l`,
# which replaces this shell and drops the user into their configured tmux session
# — so nothing after this call would run, which is exactly why it's the closer.
# OMATERM goes LAST. Its installer ends with `exec bash -l`, which replaces this
# shell and drops the user into their configured tmux session — so nothing after
# this call would run, which is exactly why it's the closer.
# OMATERM now also installs the AI agents itself (node, opencode, claude-code,
# codex, gemini are in its mise.packages), so there's no separate agent step.
#
# We call install-native.sh directly rather than omaterm.org/install: the vanity
# URL is now a dispatcher that interactively asks "native or Docker?" — which would
# stall this otherwise unattended first-run flow. install-native.sh is the native
# branch of that dispatcher, is self-contained (it clones the omaterm repo into its
# own tmpdir), and is exactly what a headless Arch box wants. Same URL pattern the
# dispatcher itself uses; override the branch with OMATERM_REF if needed.
#
# OMATERM's installer ends by exec'ing `bash -l`, whose .bashrc auto-starts tmux
# (omadots aliases `t`='tmux attach || tmux new -s Work'). Its exact closer is
# exec bash -l </dev/tty >/dev/tty 2>&1
# and THAT `</dev/tty >/dev/tty` is what breaks tmux here. With the final shell's
# fds opened on /dev/tty, tmux's client reports its terminal name (ttyname of
# STDIN, client.c) as the literal "/dev/tty" — and tmux's server_client_open
# (server-client.c) explicitly refuses a client whose ttyname is "/dev/tty",
# dying with "open terminal failed: can't use /dev/tty". (script(1)'s fresh PTY
# did NOT help: OMATERM re-redirects to /dev/tty *after* it, re-poisoning the
# name.) A relog works because the login shell's fds are a real /dev/pts/N, whose
# ttyname tmux accepts.
#
# Fix: strip that redirect from the downloaded installer so its closing
# `exec bash -l` inherits OUR fds instead — which are the login shell's real pts
# (this firstrun runs straight off .bash_profile with the pts on 0/1/2, and we do
# NOT redirect below). ttyname then resolves to /dev/pts/N and tmux starts cleanly.
# The sed is narrow and forward-compatible: if OMATERM ever drops the redirect
# itself, it no-ops and the inherited-pts behaviour still holds.
if [ "$setup_server" = 1 ]; then
echo
echo " [2/2] Installing OMATERM (toolkit + AI agents: claude-code, codex,"
echo " opencode, gemini)... when it finishes you'll be dropped straight"
echo " into your new shell."
echo
curl -fsSL https://omaterm.org/install | bash || {
echo " OMATERM install did not complete — retry later with:"
echo " curl -fsSL https://omaterm.org/install | bash"
}
omaterm_installer="$(mktemp)"
if curl -fsSL https://raw.githubusercontent.com/omacom-io/omaterm/refs/heads/master/install-native.sh -o "$omaterm_installer"; then
# Neutralise the /dev/tty redirect on OMATERM's final `exec bash -l`.
sed -i 's@exec bash -l </dev/tty >/dev/tty 2>&1@exec bash -l@' "$omaterm_installer"
# No </dev/tty here: let OMATERM (and its closing exec) inherit our pts fds.
bash "$omaterm_installer" || {
echo " OMATERM install did not complete — retry later with:"
echo " curl -fsSL https://raw.githubusercontent.com/omacom-io/omaterm/refs/heads/master/install-native.sh | bash"
}
rm -f "$omaterm_installer"
else
rm -f "$omaterm_installer"
echo " Could not download the OMATERM installer — retry later with:"
echo " curl -fsSL https://raw.githubusercontent.com/omacom-io/omaterm/refs/heads/master/install-native.sh | bash"
fi
fi
OMALOCAL_FIRSTRUN
@ -653,7 +706,7 @@ description: Context and shortcuts for an Arch Linux server provisioned by omalo
Installed by `omalocal.sh`, a single-script patcher that takes a stock Arch ISO and produces a headless install. The host was provisioned with:
- **Base packages**: `base linux linux-firmware intel-ucode amd-ucode networkmanager openssh sudo git curl vim less base-devel ufw avahi nss-mdns kitty-terminfo foot-terminfo ghostty-terminfo rxvt-unicode-terminfo`
- **Base packages**: `base linux linux-firmware intel-ucode amd-ucode networkmanager openssh sudo git curl vim less base-devel ufw avahi nss-mdns docker kitty-terminfo foot-terminfo ghostty-terminfo rxvt-unicode-terminfo` (Docker is in base + enabled so the first-run Once install — which runs before OMATERM — has a working daemon; the install user is in the `docker` group).
- **SSH**: enabled, **key-only** (`/etc/ssh/sshd_config.d/10-key-only.conf` sets `PasswordAuthentication no`, `KbdInteractiveAuthentication no`, `PermitRootLogin no`). The master machine's public key was baked into `~/.ssh/authorized_keys` at install time.
- **Firewall**: ufw enabled with default deny incoming, allow outgoing. Open: `22/tcp` (SSH), `80/tcp` + `443/tcp` (web / Once), `5353/udp` (mDNS), `53317/tcp` + `53317/udp` (LocalSend).
- **mDNS**: avahi-daemon enabled and `mdns_minimal` wired into `/etc/nsswitch.conf`, so the box is reachable LAN-wide as `<hostname>.local` (try `ssh <user>@<hostname>.local`). The `once-mdns-sync.service` daemon (`/usr/local/bin/once-mdns-sync`) auto-publishes an mDNS alias for every single-label `*.local` host Once is serving — so apps deployed via `once-add` (as `<name>.local`) are reachable LAN-wide. See the "Deploying a Once app reachable on the LAN" recipe for the deploy side.
@ -664,7 +717,7 @@ Installed by `omalocal.sh`, a single-script patcher that takes a stock Arch ISO
On the user's first interactive login a self-removing `~/.bash_profile` hook offers to set up the server. Once is installed first; OMATERM is installed last because its installer ends with `exec bash -l` and drops the user straight into their new shell:
- **Once** (https://once.com)`curl https://get.once.com | ONCE_INTERACTIVE=false sh` — Basecamp's self-hosted app deployment platform.
- **Omaterm** (https://omaterm.org)`curl -fsSL https://omaterm.org/install | bash` — terminal-first toolkit: starship, neovim, tmux, mise, docker, lazygit, yay, plus AI agents (claude-code, codex, opencode, gemini), etc. OMATERM installs the AI agents itself (they're in its `mise.packages`), so there's no separate agent step.
- **Omaterm** (https://omaterm.org)`curl -fsSL https://raw.githubusercontent.com/omacom-io/omaterm/refs/heads/master/install-native.sh | bash` — terminal-first toolkit: starship, neovim, tmux, mise, docker, lazygit, yay, plus AI agents (claude-code, codex, opencode, gemini), etc. OMATERM installs the AI agents itself (they're in its `mise.packages`), so there's no separate agent step. We call `install-native.sh` directly because `omaterm.org/install` is now a dispatcher that interactively prompts native-vs-Docker; the native branch is what a headless Arch box wants.
If the user declined, install them manually with those same two commands.
@ -1166,7 +1219,12 @@ cat > "/mnt/home/$USERNAME/.config/omarchy-send/config.json" <<'OMS_CONFIG'
}
OMS_CONFIG
arch-chroot /mnt chmod 600 "/home/$USERNAME/.config/omarchy-send/config.json"
arch-chroot /mnt chown -R "$USERNAME:$USERNAME" "/home/$USERNAME/.local" "/home/$USERNAME/.config/omarchy-send"
# chown the WHOLE ~/.config (not just .config/omarchy-send): the host-side
# `install -d` above created the ~/.config PARENT as root, and a -R chown of the
# omarchy-send subdir alone leaves ~/.config itself root-owned. That blocks the
# user from creating anything else under ~/.config later — e.g. OMATERM's
# LazyVim step (`git clone ~/.config/nvim`) fails with "Permission denied".
arch-chroot /mnt chown -R "$USERNAME:$USERNAME" "/home/$USERNAME/.local" "/home/$USERNAME/.config"
echo "==> Writing pre-login console banner (hostname + IPv4 + ssh hint)..."
# agetty expands these escapes when it prints /etc/issue:
@ -1237,9 +1295,9 @@ echo
echo " Then from your master machine:"
echo " ssh $USERNAME@<this-host-ip> (or ssh $USERNAME@$HOSTNAME)"
echo
echo " On your first login you'll be offered OMATERM + Once. To set"
echo " On your first login you'll be offered the omalocal setup. To set"
echo " them up manually at any time:"
echo " curl -fsSL https://omaterm.org/install | bash"
echo " curl -fsSL https://raw.githubusercontent.com/omacom-io/omaterm/refs/heads/master/install-native.sh | bash"
echo " curl https://get.once.com | ONCE_INTERACTIVE=false sh"
echo "=================================================================="
echo