./install only ever adds. It copies over /, links dotfiles, installs packages -- and has no way to say "remove that file", "disable that unit", "that symlink points nowhere now". So a machine set up months ago keeps whatever this repository has since decided was wrong, and the only thing that ever fixes it is somebody reading a commit message. With a curl installer in the README, that stopped being hypothetical. A migration is one script that performs one repair, exactly once, on the machines that need it. Named by the commit timestamp that authored it, so glob order is chronological without a sequence number two branches could both pick. Marked in ~/.local/state on success and only on success, so a repair that failed stays pending rather than being recorded as done and hidden forever. Ordered, and stopped at the first failure, because a later repair may assume an earlier one landed. A fresh install marks everything without running it, the way Migrations.qml stamps a pre-versioning settings file at its baseline. The first real one removes the dangling ~/.config/forge symlink left behind when the GNOME session was cut: link-dotfiles could link it but never unlink it. Verified both ways -- a no-op on a machine that never had it, an actual repair on one that did. Root work goes through panama-sudo --reason so the password prompt names the repair, and the contract fails any migration reaching for bare sudo.
561 lines
20 KiB
Bash
Executable File
561 lines
20 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
#
|
||
# panama – Helper for managing the Panama dotfiles/repo
|
||
# Version 1.0
|
||
# Author: Gabriel Brown
|
||
#
|
||
# Commands:
|
||
# update Commit & sync local changes (or just pull if clean)
|
||
# edit Open the Panama repo in Neovim
|
||
# doctor Report what is actually running on this machine
|
||
# test Run every contract under tests/
|
||
# upgrade Re-run the installer from anywhere
|
||
# apps Choose applications to install, by category
|
||
# app Build and install an application that no repository packages
|
||
# help Show this help
|
||
#
|
||
# Designed to grow: add new subcommands as cmd_<name> functions and
|
||
# register them in the dispatcher / usage block below.
|
||
|
||
set -euo pipefail
|
||
|
||
PROGRAM=$(basename "$0")
|
||
VERSION="1.0"
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# Locate the Panama repo (this script lives in <repo>/bin/panama)
|
||
# ----------------------------------------------------------------------------
|
||
SCRIPT_PATH=$(readlink -f "${BASH_SOURCE[0]}")
|
||
PANAMA_DIR=$(cd "$(dirname "$SCRIPT_PATH")/.." && pwd)
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# Pretty output
|
||
# ----------------------------------------------------------------------------
|
||
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1 && [[ $(tput colors 2>/dev/null || echo 0) -ge 8 ]]; then
|
||
BOLD=$(tput bold); RESET=$(tput sgr0)
|
||
RED=$(tput setaf 1); GREEN=$(tput setaf 2); YELLOW=$(tput setaf 3)
|
||
BLUE=$(tput setaf 4); MAGENTA=$(tput setaf 5); CYAN=$(tput setaf 6)
|
||
else
|
||
BOLD=""; RESET=""; RED=""; GREEN=""; YELLOW=""; BLUE=""; MAGENTA=""; CYAN=""
|
||
fi
|
||
|
||
info() { printf '%s==>%s %s\n' "${BLUE}${BOLD}" "$RESET" "$*"; }
|
||
ok() { printf '%s✓%s %s\n' "${GREEN}${BOLD}" "$RESET" "$*"; }
|
||
warn() { printf '%s!%s %s\n' "${YELLOW}${BOLD}" "$RESET" "$*"; }
|
||
err() { printf '%s✗%s %s\n' "${RED}${BOLD}" "$RESET" "$*" >&2; }
|
||
header(){ printf '\n%s%s%s\n' "${MAGENTA}${BOLD}" "$*" "$RESET"; }
|
||
|
||
# Ask a yes/no question. Returns 0 for yes, 1 for no. Default = no.
|
||
confirm() {
|
||
local prompt="$1" reply
|
||
printf '%s?%s %s %s[y/N]%s ' "${CYAN}${BOLD}" "$RESET" "$prompt" "$BOLD" "$RESET"
|
||
read -r reply || true
|
||
[[ "$reply" =~ ^[Yy]([Ee][Ss])?$ ]]
|
||
}
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# Usage
|
||
# ----------------------------------------------------------------------------
|
||
usage() {
|
||
cat <<EOF
|
||
${BOLD}$PROGRAM${RESET} – manage the Panama repo (${PANAMA_DIR})
|
||
|
||
${BOLD}Usage:${RESET}
|
||
$PROGRAM <command> [options]
|
||
|
||
${BOLD}Commands:${RESET}
|
||
${GREEN}update${RESET} Review, commit & sync local changes. If the working tree is
|
||
clean it simply runs 'git pull'.
|
||
${GREEN}edit${RESET} Open the Panama repo in Neovim.
|
||
${GREEN}doctor${RESET} Report what is actually running on this machine, rather
|
||
than what was installed. Takes --summary for one line per check.
|
||
${GREEN}test${RESET} Run every contract under tests/. Give it a pattern to run
|
||
a subset: 'panama test dock' runs the ones matching 'dock'.
|
||
${GREEN}upgrade${RESET} Re-run ./install from anywhere. Safe: every stage is
|
||
idempotent and this is the documented upgrade path.
|
||
${GREEN}migrate${RESET} Apply repairs this machine has not had yet. The half of an
|
||
upgrade that ./install cannot do, because installing only ever
|
||
adds. Safe to re-run; nothing is applied twice.
|
||
${GREEN}apps${RESET} Choose applications to install: pick a category, then tick
|
||
what you want. The same catalog ./install offers, minus the
|
||
install.
|
||
${GREEN}app${RESET} Build and install an application that neither dnf nor
|
||
Flathub carries. With no name, lists what is available.
|
||
${GREEN}help${RESET} Show this help (also -h, --help).
|
||
|
||
${BOLD}Options:${RESET}
|
||
-h, --help Show this help and exit
|
||
--version Show version and exit
|
||
|
||
${BOLD}Examples:${RESET}
|
||
$PROGRAM update
|
||
$PROGRAM edit
|
||
$PROGRAM doctor --summary
|
||
$PROGRAM test dock
|
||
$PROGRAM upgrade
|
||
$PROGRAM apps
|
||
$PROGRAM app
|
||
$PROGRAM app chatgpt-desktop
|
||
EOF
|
||
}
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# Guard: make sure we're in a git repo
|
||
# ----------------------------------------------------------------------------
|
||
require_git_repo() {
|
||
if ! git -C "$PANAMA_DIR" rev-parse --git-dir >/dev/null 2>&1; then
|
||
err "'$PANAMA_DIR' is not a git repository."
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# Command: update
|
||
# ----------------------------------------------------------------------------
|
||
cmd_update() {
|
||
require_git_repo
|
||
cd "$PANAMA_DIR"
|
||
|
||
info "Panama repo: ${BOLD}${PANAMA_DIR}${RESET}"
|
||
|
||
# Any changes in the working tree? (modified, staged, or untracked)
|
||
if [[ -z "$(git status --porcelain)" ]]; then
|
||
info "Working tree is clean — pulling latest changes."
|
||
if git pull --ff-only; then
|
||
ok "Already in sync."
|
||
else
|
||
err "git pull failed."
|
||
exit 1
|
||
fi
|
||
return
|
||
fi
|
||
|
||
# Show what changed
|
||
header "Changed files"
|
||
git -c color.status=always status --short
|
||
|
||
header "Diff"
|
||
# Tracked changes (staged + unstaged) relative to the last commit.
|
||
git --no-pager -c color.diff=always diff HEAD
|
||
|
||
# Untracked files won't appear in 'git diff', so show them as new files.
|
||
local untracked
|
||
untracked=$(git ls-files --others --exclude-standard)
|
||
if [[ -n "$untracked" ]]; then
|
||
while IFS= read -r f; do
|
||
[[ -z "$f" ]] && continue
|
||
git --no-pager -c color.diff=always diff --no-index -- /dev/null "$f" || true
|
||
done <<< "$untracked"
|
||
fi
|
||
|
||
echo
|
||
if ! confirm "Commit these changes?"; then
|
||
warn "Aborted — no changes committed."
|
||
return
|
||
fi
|
||
|
||
# Commit message
|
||
local msg
|
||
printf '%s?%s Commit message: ' "${CYAN}${BOLD}" "$RESET"
|
||
read -r msg || true
|
||
if [[ -z "${msg// }" ]]; then
|
||
msg="Update $(date '+%Y-%m-%d %H:%M:%S')"
|
||
warn "No message given — using: ${BOLD}${msg}${RESET}"
|
||
fi
|
||
|
||
# Is the local branch up to date with its upstream?
|
||
info "Checking whether the repo is up to date..."
|
||
if git rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1; then
|
||
git fetch --quiet
|
||
local local_rev remote_rev base_rev
|
||
local_rev=$(git rev-parse @)
|
||
remote_rev=$(git rev-parse '@{u}')
|
||
base_rev=$(git merge-base @ '@{u}')
|
||
|
||
if [[ "$local_rev" == "$remote_rev" ]]; then
|
||
ok "Repo is up to date."
|
||
elif [[ "$local_rev" == "$base_rev" ]]; then
|
||
warn "Repo is behind upstream — stashing, pulling, then re-applying."
|
||
info "Stashing local changes..."
|
||
git stash push --include-untracked -m "panama-update-$(date +%s)" >/dev/null
|
||
|
||
if ! git pull --ff-only; then
|
||
err "git pull failed — restoring your changes."
|
||
git stash pop || true
|
||
exit 1
|
||
fi
|
||
|
||
info "Re-applying stashed changes..."
|
||
if ! git stash pop; then
|
||
err "Conflict while re-applying changes. Resolve it, then commit manually."
|
||
exit 1
|
||
fi
|
||
else
|
||
warn "Local branch has diverged from upstream — committing locally only."
|
||
fi
|
||
else
|
||
warn "No upstream configured for this branch — committing locally only."
|
||
fi
|
||
|
||
# Commit everything
|
||
info "Committing changes..."
|
||
git add -A
|
||
git commit -m "$msg"
|
||
ok "Committed: ${BOLD}${msg}${RESET}"
|
||
|
||
echo
|
||
if confirm "Push the changes now?"; then
|
||
info "Pushing..."
|
||
if git push; then
|
||
ok "Pushed to remote."
|
||
else
|
||
err "git push failed."
|
||
exit 1
|
||
fi
|
||
else
|
||
info "Done — changes committed locally but not pushed."
|
||
fi
|
||
}
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# Command: edit
|
||
# ----------------------------------------------------------------------------
|
||
cmd_edit() {
|
||
if ! command -v nvim >/dev/null 2>&1; then
|
||
err "Neovim (nvim) is not installed or not on PATH."
|
||
exit 1
|
||
fi
|
||
info "Opening ${BOLD}${PANAMA_DIR}${RESET} in Neovim."
|
||
cd "$PANAMA_DIR"
|
||
exec nvim .
|
||
}
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# Command: doctor
|
||
# ----------------------------------------------------------------------------
|
||
#
|
||
# The health check already exists and the installer already runs it; what it did
|
||
# not have was a way to reach it from a terminal. Everything is passed straight
|
||
# through, so --summary and anything added later work without this knowing about
|
||
# them.
|
||
cmd_doctor() {
|
||
local doctor="$PANAMA_DIR/config/dot/quickshell/scripts/panama-doctor"
|
||
if [[ ! -x "$doctor" ]]; then
|
||
err "panama-doctor is missing from $doctor"
|
||
exit 1
|
||
fi
|
||
exec "$doctor" "$@"
|
||
}
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# Command: test
|
||
# ----------------------------------------------------------------------------
|
||
#
|
||
# The contracts are the main safety net in this repository and had no entry
|
||
# point: 121 executables with no runner and no mention in the README, which is
|
||
# most of the way to not having them.
|
||
#
|
||
# Each runs in its own process and a failure does not stop the rest, because the
|
||
# useful output is the whole list of what is broken rather than the first thing
|
||
# that broke. The exit code is what a caller can act on.
|
||
cmd_test() {
|
||
local pattern="${1:-}"
|
||
local -a suite=()
|
||
|
||
# Executables, plus the Python suites. Those are unittest files rather than
|
||
# executables, and collecting only what has the executable bit would skip them
|
||
# without saying so -- which is how all three came to be run by nothing at all.
|
||
# A runner with a blind spot is worse than no runner, because it reports PASS.
|
||
while IFS= read -r path; do
|
||
[[ -x "$path" || "$path" == *_test.py ]] || continue
|
||
[[ -z "$pattern" || "$path" == *"$pattern"* ]] && suite+=("$path")
|
||
done < <(find "$PANAMA_DIR/tests" -type f -not -path '*/fixtures/*' -not -path '*__pycache__*' | sort)
|
||
|
||
if (( ${#suite[@]} == 0 )); then
|
||
err "No contracts match '${pattern}'"
|
||
exit 1
|
||
fi
|
||
|
||
info "Running ${#suite[@]} contract(s)"
|
||
local -a failed=()
|
||
local path name
|
||
local -a runner
|
||
for path in "${suite[@]}"; do
|
||
name="${path#"$PANAMA_DIR"/tests/}"
|
||
if [[ "$path" == *_test.py ]]; then
|
||
runner=(python3 "$path")
|
||
else
|
||
runner=("$path")
|
||
fi
|
||
if "${runner[@]}" >/dev/null 2>&1; then
|
||
ok "$name"
|
||
else
|
||
err "$name"
|
||
failed+=("$name")
|
||
fi
|
||
done
|
||
|
||
header "Result"
|
||
if (( ${#failed[@]} == 0 )); then
|
||
ok "${#suite[@]} contract(s) passed"
|
||
return 0
|
||
fi
|
||
err "${#failed[@]} of ${#suite[@]} failed:"
|
||
printf ' %s\n' "${failed[@]}" >&2
|
||
warn "Run one on its own to see why: ${BOLD}${PANAMA_DIR}/tests/<name>${RESET}"
|
||
return 1
|
||
}
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# Command: upgrade
|
||
# ----------------------------------------------------------------------------
|
||
#
|
||
# ./install is the upgrade path -- every stage is idempotent and re-running is
|
||
# the documented way to repair a machine. This only saves remembering where the
|
||
# repository lives.
|
||
cmd_upgrade() {
|
||
local installer="$PANAMA_DIR/install"
|
||
if [[ ! -x "$installer" ]]; then
|
||
err "The installer is missing from $installer"
|
||
exit 1
|
||
fi
|
||
info "Re-running ${BOLD}${installer}${RESET}"
|
||
cd "$PANAMA_DIR"
|
||
exec "$installer" "$@"
|
||
}
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# Command: migrate
|
||
# ----------------------------------------------------------------------------
|
||
#
|
||
# What ./install cannot do. The installer only ever adds -- it copies over /,
|
||
# links dotfiles, installs packages -- so a machine set up months ago keeps
|
||
# whatever this repository has since decided was wrong. Migrations are the one
|
||
# mechanism that can remove a file, disable a unit, or repair a symlink on a
|
||
# machine that already exists. See bin/panama-migrate.
|
||
cmd_migrate() {
|
||
local runner="$PANAMA_DIR/bin/panama-migrate"
|
||
if [[ ! -x "$runner" ]]; then
|
||
err "The migration runner is missing from $runner"
|
||
exit 1
|
||
fi
|
||
exec "$runner" "$@"
|
||
}
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# Command: app
|
||
# ----------------------------------------------------------------------------
|
||
#
|
||
# The applications that neither dnf nor Flathub carries, built from source into
|
||
# a package dnf can still own and remove.
|
||
#
|
||
# Deliberately NOT part of ./install. A source build is slow, wants the network
|
||
# for the whole of it, and depends on an upstream that moves -- which is exactly
|
||
# the failure the interview exists to prevent: twenty minutes in, a prompt or an
|
||
# error, with nobody at the keyboard. Asking for one of these is a thing you do
|
||
# on purpose, and it is also the rebuild path when a new version ships.
|
||
#
|
||
# Nothing is pinned. Each build takes the current default branch and the current
|
||
# upstream release, and says so when it fails. A recorded version is a 404
|
||
# waiting to happen -- sunhat proved that three times over.
|
||
APPS_DIR="$PANAMA_DIR/setup/apps"
|
||
APPS_WORK="${XDG_CACHE_HOME:-$HOME/.cache}/panama/apps"
|
||
|
||
cmd_app() {
|
||
local name="${1:-}"
|
||
|
||
if [[ -z "$name" ]]; then
|
||
header "Applications"
|
||
printf 'Built from source, because no repository carries them.\n\n'
|
||
local file
|
||
for file in "$APPS_DIR"/*; do
|
||
[[ -f "$file" ]] || continue
|
||
local description=""
|
||
# shellcheck source=/dev/null
|
||
source "$file"
|
||
printf ' %s%-18s%s %s\n' "$GREEN" "$(basename "$file")" "$RESET" "$description"
|
||
done
|
||
printf '\nBuild one with: %s%s app <name>%s\n' "$BOLD" "$PROGRAM" "$RESET"
|
||
return 0
|
||
fi
|
||
|
||
local definition="$APPS_DIR/$name"
|
||
if [[ ! -f "$definition" ]]; then
|
||
err "No such application: '$name'"
|
||
printf "Run '%s app' to see what is available.\n" "$PROGRAM" >&2
|
||
exit 1
|
||
fi
|
||
|
||
local description="" repo=""
|
||
# shellcheck source=/dev/null
|
||
source "$definition"
|
||
[[ -n "$repo" ]] || { err "$name declares no repository"; exit 1; }
|
||
|
||
# The checkout lives in the cache because it is entirely rebuildable and
|
||
# should never be mistaken for something to keep. Existing checkouts are
|
||
# reset to upstream rather than merged: a local edit in a build tree is not
|
||
# something to preserve silently.
|
||
local tree="$APPS_WORK/$name"
|
||
if [[ -d "$tree/.git" ]]; then
|
||
info "Updating $name"
|
||
git -C "$tree" fetch --depth 1 origin HEAD || { err "Could not reach $repo"; exit 1; }
|
||
git -C "$tree" reset --hard FETCH_HEAD >/dev/null
|
||
else
|
||
info "Cloning $name"
|
||
mkdir -p "$APPS_WORK"
|
||
rm -rf "$tree"
|
||
git clone --depth 1 "$repo" "$tree" || { err "Could not clone $repo"; exit 1; }
|
||
fi
|
||
|
||
info "Building ${BOLD}${name}${RESET} — this takes a while and needs the network"
|
||
if ( cd "$tree" && build ); then
|
||
ok "$name installed"
|
||
printf 'Built from %s\n' "$(git -C "$tree" rev-parse --short HEAD)"
|
||
else
|
||
err "$name failed to build"
|
||
printf 'The tree is left at %s so the failure can be read.\n' "$tree" >&2
|
||
printf 'This builds against upstream HEAD, so a break there breaks this.\n' >&2
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# Command: apps
|
||
# ----------------------------------------------------------------------------
|
||
# The optional applications, chosen a few at a time rather than all at once.
|
||
#
|
||
# ./install offers the same catalog as whole categories, because during a first
|
||
# install you want coarse and fast. This is the other end of it: pick a
|
||
# category, then tick the applications inside, and install just those. Both read
|
||
# setup/lib/extras-catalog, so neither can drift from the other.
|
||
#
|
||
# Source-built applications appear here too, as their own category. They are not
|
||
# part of ./install for good reason -- a build is slow, wants the network
|
||
# throughout, and can fail on an upstream that moved -- but "choose, then
|
||
# install" is exactly the right shape for them, which is what this is.
|
||
cmd_apps() {
|
||
if ! command -v gum >/dev/null 2>&1; then
|
||
err "gum is not installed. It is in setup/packages/initial-packages; run 'panama upgrade'."
|
||
exit 1
|
||
fi
|
||
|
||
# shellcheck source=../setup/lib/extras-catalog
|
||
source "$PANAMA_DIR/setup/lib/extras-catalog"
|
||
|
||
local extras_dir="$PANAMA_DIR/setup/packages/extras"
|
||
local apps_dir="$PANAMA_DIR/setup/apps"
|
||
|
||
local -a categories=()
|
||
mapfile -t categories < <(catalog_categories "$extras_dir")
|
||
[[ -d "$apps_dir" ]] && categories+=("built from source")
|
||
|
||
(( ${#categories[@]} > 0 )) || { err "No application categories found."; exit 1; }
|
||
|
||
local category
|
||
category="$(gum choose --header "Which kind of application?" "${categories[@]}")" || return 0
|
||
[[ -n "$category" ]] || return 0
|
||
|
||
if [[ "$category" == "built from source" ]]; then
|
||
cmd_apps_source "$apps_dir"
|
||
return
|
||
fi
|
||
|
||
local file="$extras_dir/$category"
|
||
local -a labels=() targets=()
|
||
local target label marker
|
||
while IFS=$'\t' read -r target label; do
|
||
[[ -n "$target" ]] || continue
|
||
# Marked, not hidden: reinstalling what is present is harmless, but a menu
|
||
# that silently omits it leaves you wondering where it went.
|
||
if catalog_installed "$target"; then marker=" (installed)"; else marker=""; fi
|
||
targets+=("$target")
|
||
labels+=("$label$marker")
|
||
done < <(catalog_entries "$file")
|
||
|
||
local chosen
|
||
chosen="$(gum choose --no-limit --header "$category — space to select, enter to accept" "${labels[@]}")" || return 0
|
||
[[ -n "$chosen" ]] || { info "Nothing selected."; return 0; }
|
||
|
||
# Back from labels to install targets, then out to everything each one carries.
|
||
local -a install_targets=()
|
||
local pick i
|
||
while IFS= read -r pick; do
|
||
[[ -n "$pick" ]] || continue
|
||
for i in "${!labels[@]}"; do
|
||
if [[ "${labels[$i]}" == "$pick" ]]; then
|
||
mapfile -t -O "${#install_targets[@]}" install_targets < <(catalog_targets "$file" "${targets[$i]}")
|
||
break
|
||
fi
|
||
done
|
||
done <<< "$chosen"
|
||
|
||
local -a dnf_packages=() flatpak_ids=()
|
||
for target in "${install_targets[@]}"; do
|
||
if [[ "$target" == flatpak:* ]]; then flatpak_ids+=("${target#flatpak:}"); else dnf_packages+=("$target"); fi
|
||
done
|
||
|
||
header "About to install"
|
||
(( ${#dnf_packages[@]} > 0 )) && printf ' dnf %s\n' "${dnf_packages[*]}"
|
||
(( ${#flatpak_ids[@]} > 0 )) && printf ' flatpak %s\n' "${flatpak_ids[*]}"
|
||
echo
|
||
confirm "Install these?" || { warn "Nothing installed."; return 0; }
|
||
|
||
if (( ${#dnf_packages[@]} > 0 )); then
|
||
info "Installing ${#dnf_packages[@]} package(s) with dnf"
|
||
sudo dnf install -y "${dnf_packages[@]}" || warn "Some packages did not install"
|
||
fi
|
||
if (( ${#flatpak_ids[@]} > 0 )); then
|
||
info "Installing ${#flatpak_ids[@]} flatpak(s)"
|
||
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo >/dev/null
|
||
sudo flatpak install -y flathub "${flatpak_ids[@]}" || warn "Some flatpaks did not install"
|
||
fi
|
||
ok "Done."
|
||
}
|
||
|
||
# The source-built applications, offered by the same two-step flow and handed to
|
||
# the existing `panama app` so there is one build path, not two.
|
||
cmd_apps_source() {
|
||
local apps_dir="$1"
|
||
local -a names=()
|
||
mapfile -t names < <(for f in "$apps_dir"/*; do [[ -f "$f" ]] && basename "$f"; done)
|
||
(( ${#names[@]} > 0 )) || { err "No applications in $apps_dir."; return 1; }
|
||
|
||
local chosen
|
||
chosen="$(gum choose --no-limit --header "Built from source — these take a while" "${names[@]}")" || return 0
|
||
[[ -n "$chosen" ]] || { info "Nothing selected."; return 0; }
|
||
|
||
local name
|
||
while IFS= read -r name; do
|
||
[[ -n "$name" ]] || continue
|
||
header "Building $name"
|
||
cmd_app "$name" || warn "$name did not build"
|
||
done <<< "$chosen"
|
||
}
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# Dispatcher
|
||
# ----------------------------------------------------------------------------
|
||
main() {
|
||
local cmd="${1:-}"
|
||
case "$cmd" in
|
||
update) shift; cmd_update "$@" ;;
|
||
edit) shift; cmd_edit "$@" ;;
|
||
doctor) shift; cmd_doctor "$@" ;;
|
||
test) shift; cmd_test "$@" ;;
|
||
upgrade) shift; cmd_upgrade "$@" ;;
|
||
migrate) shift; cmd_migrate "$@" ;;
|
||
app) shift; cmd_app "$@" ;;
|
||
apps) shift; cmd_apps "$@" ;;
|
||
help|-h|--help|"") usage ;;
|
||
--version) printf '%s %s\n' "$PROGRAM" "$VERSION" ;;
|
||
*)
|
||
err "Unknown command: '$cmd'"
|
||
echo
|
||
usage
|
||
exit 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
main "$@"
|