Skip to content

Instantly share code, notes, and snippets.

@friz-xc
Last active October 19, 2026 12:24
Show Gist options
  • Select an option

  • Save friz-xc/c3423e0f918028fb6f4db5424b0a1297 to your computer and use it in GitHub Desktop.

Select an option

Save friz-xc/c3423e0f918028fb6f4db5424b0a1297 to your computer and use it in GitHub Desktop.
Termless installer
#!/usr/bin/env bash
# desc: one-line Termless install for designers: installs Apple's developer tools
# (for git) and GitHub's tool, logs in to GitHub, and puts the latest
# termless.app in Applications.
# usage: /bin/bash -c "$(curl -fsSL https://gist.githubusercontent.com/friz-xc/c3423e0f918028fb6f4db5424b0a1297/raw/termless-install.sh)"
# (`make installer-print-cmd` prints the real line.)
# Standalone on purpose: it's published as a public gist because this repo is
# private. The app itself comes from this repo's GitHub Releases, downloaded
# with gh, so the designer's GitHub account needs access to friz-xc/termless.
# Safe to run again: it updates the app in place.
set -euo pipefail
declare -r REPO="friz-xc/termless"
declare -r APP_NAME="termless.app"
green() { echo -e "\033[32m$*\033[0m"; }
yellow() { echo -e "\033[33m$*\033[0m"; }
red() { echo -e "\033[31m$*\033[0m"; }
dim() { echo -e "\033[2m$*\033[0m"; }
bold() { echo -e "\033[1m$*\033[0m"; }
fn_fail() {
red "✖ $1" >&2
[[ -n "${2:-}" ]] && dim "$2" >&2
dim "Nothing is broken. Fix the above and run the same command again." >&2
exit 1
}
fn_step() { echo; bold "[$1/4] $2"; }
fn_arch() {
case "$(uname -m)" in
arm64) echo "arm64" ;;
x86_64) echo "x86_64" ;;
*) fn_fail "This Mac's processor type isn't supported." "Ask an engineer for help." ;;
esac
}
# Termless runs the Mac's own git, which comes with Apple's developer tools.
fn_install_dev_tools() {
fn_step 1 "Apple's developer tools"
if xcode-select -p >/dev/null 2>&1; then
green "✔ Already installed."
return
fi
xcode-select --install >/dev/null 2>&1 || true
yellow "● A pop-up opened. Click Install and wait for it to finish."
dim " This window will carry on by itself when it's done."
until xcode-select -p >/dev/null 2>&1; do sleep 5; done
green "✔ Installed."
}
fn_install_gh() {
fn_step 2 "GitHub's tool (gh)"
export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"
if command -v gh >/dev/null 2>&1; then
green "✔ Already installed."
return
fi
local arch zip_url work_dir
case "$(fn_arch)" in
arm64) arch="arm64" ;;
x86_64) arch="amd64" ;;
esac
zip_url=$(curl -fsSL https://api.github.com/repos/cli/cli/releases/latest |
grep -o "https://[^\"]*macOS_${arch}\.zip" | head -1) || true
[[ -n "$zip_url" ]] ||
fn_fail "Couldn't find GitHub's tool for this Mac." "Check your internet connection."
work_dir=$(mktemp -d)
echo "Downloading…"
curl -fsSL -o "$work_dir/gh.zip" "$zip_url" || fn_fail "Download failed. Check your internet connection."
unzip -qq "$work_dir/gh.zip" -d "$work_dir" || fn_fail "Couldn't open the downloaded file."
# no sudo needed: it's just a binary in the designer's own folder. Termless
# looks in ~/.local/bin too, so it doesn't need to be on the shell's PATH.
mkdir -p "$HOME/.local/bin"
cp "$work_dir"/gh_*_macOS_"$arch"/bin/gh "$HOME/.local/bin/gh"
chmod +x "$HOME/.local/bin/gh"
rm -rf "$work_dir"
green "✔ Installed."
}
fn_login_github() {
fn_step 3 "Log in to GitHub"
if ! gh auth status --hostname github.com >/dev/null 2>&1; then
yellow "● Your browser will open GitHub."
dim " If asked \"Authenticate Git with your GitHub credentials?\", press Return."
dim " When asked, press Return, then paste the code in the browser (⌘ V),"
dim " click Continue, then Authorize."
echo
gh auth login --hostname github.com --git-protocol https --web --clipboard </dev/tty ||
fn_fail "GitHub login didn't finish."
fi
gh auth setup-git --hostname github.com >/dev/null 2>&1 || true
green "✔ Logged in as $(gh api user --jq .login 2>/dev/null || echo "you")."
}
fn_install_app() {
fn_step 4 "Install Termless"
local asset work_dir out dir
asset="termless-macos-$(fn_arch).zip"
work_dir=$(mktemp -d)
trap "rm -rf '$work_dir'" EXIT
echo "Downloading…"
out=$(gh release download --repo "$REPO" --pattern "$asset" --dir "$work_dir" 2>&1) ||
fn_fail "Couldn't download Termless." \
"If it says \"Not Found\", ask an engineer to give your GitHub account
access to ${REPO}. If it says \"no assets match\", there's no build for
this Mac yet: ask an engineer.
${out}"
ditto -x -k "$work_dir/$asset" "$work_dir" || fn_fail "Couldn't open the downloaded file."
# Same choice as `make install`: /Applications when it's writable (admin),
# otherwise ~/Applications. Syncing into the app folder avoids needing admin
# rights to replace an existing install.
if [[ -w "/Applications/$APP_NAME" ]] || [[ -w /Applications ]]; then
dir=/Applications
else
dir="$HOME/Applications"
fi
osascript -e 'quit app "termless"' >/dev/null 2>&1 || true
mkdir -p "$dir/$APP_NAME"
rsync -a --delete "$work_dir/$APP_NAME/" "$dir/$APP_NAME/" ||
fn_fail "Couldn't copy Termless into ${dir}."
xattr -dr com.apple.quarantine "$dir/$APP_NAME" 2>/dev/null || true
green "✔ Installed to ${dir}/${APP_NAME}"
open "$dir/$APP_NAME" || true
}
fn_termless_install() {
[[ "$(uname)" == "Darwin" ]] || fn_fail "Termless is for Macs only."
bold "Installing Termless on your Mac."
fn_install_dev_tools
fn_install_gh
fn_login_github
fn_install_app
echo
green "✔ All done. Termless is open. Run this same line again any time to update it."
}
# runs from `bash -c "$(curl …)"`, where there's no BASH_SOURCE to guard on
fn_termless_install "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment