Files
arthur-os/bin/omarchy-plugin-update
T
Ryan Hughes 1bb439476a Add third-party plugin sources and installer
Let users add trusted plugin source repos and add/update/remove plugins
from them, alongside the existing in-shell plugin commands:

  omarchy plugin source <add|list|remove|refresh>
  omarchy plugin available
  omarchy plugin add | update | remove | validate

Each command is interactive (gum/fzf pickers, confirmation, an update
diff) in a TTY and fully flag-driven with --yes for scripts and agents.
Sources live in ~/.config/omarchy/plugins/sources.json and clone into
~/.cache/omarchy/plugin-sources/. The installer only copies files,
validates manifests against the shell's schema, and toggles enabled
state over IPC -- it never runs plugin code, hooks, or sudo.

Also guard 'plugin bar add' so only known bar-widget ids enter the
layout (rejecting typos/non-widgets like a stray '--help').
2026-05-29 16:55:15 -04:00

216 lines
7.4 KiB
Bash
Executable File

#!/bin/bash
# omarchy:summary=Update installed Omarchy plugins from their sources
# omarchy:group=plugin
# omarchy:args=[plugin-id] [--all] [--force] [--review] [--no-refresh] [--yes]
# omarchy:examples=omarchy plugin update | omarchy plugin update orbit --review | omarchy plugin update --all --yes
# Re-pulls a plugin's folder from its source when the upstream manifest version
# is newer. Updates are just re-installs, so the same "review the code" rule
# applies — by default this prints a diff of exactly what changed before
# touching anything. --yes skips the diff and confirmation for scripts/agents.
set -o pipefail
PLUGINS_DIR="$HOME/.config/omarchy/plugins"
fail() {
echo "omarchy-plugin-update: $*" >&2
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || fail "$1 is required"
}
interactive() {
[[ -t 0 && -t 1 ]]
}
PLUGIN_ID=""
ALL=0
FORCE=0
DO_REVIEW=0
NO_REFRESH=0
ASSUME_YES=0
while (($# > 0)); do
case "$1" in
--all | -a) ALL=1; shift ;;
--force | -f) FORCE=1; shift ;;
--review) DO_REVIEW=1; shift ;;
--no-refresh) NO_REFRESH=1; shift ;;
--yes | -y) ASSUME_YES=1; shift ;;
-h | --help)
cat <<USAGE
Usage: omarchy plugin update [plugin-id] [--all] [--force] [--review] [--no-refresh] [--yes]
Updates installed plugins when their source has a newer version.
(no args) pick from the plugins that have an update available
<plugin-id> update one plugin
--all update every plugin with an available update
--force re-pull even when versions match (or local looks newer)
--review always show the file diff before updating (default in a TTY)
--no-refresh use cached clones, skip the network refresh
--yes skip the diff and confirmation
USAGE
exit 0
;;
-*) fail "unknown option: $1" ;;
*)
[[ -z $PLUGIN_ID ]] || fail "unexpected argument: $1"
PLUGIN_ID="$1"; shift ;;
esac
done
require_command jq
((ALL)) && [[ -n $PLUGIN_ID ]] && fail "pass either a plugin-id or --all, not both"
confirm() {
local prompt="$1"
((ASSUME_YES)) && return 0
if command -v gum >/dev/null 2>&1 && interactive; then gum confirm "$prompt"; return; fi
if interactive; then local reply; read -r -p "$prompt [y/N] " reply; [[ $reply == [yY]* ]]; return; fi
fail "refusing to update without confirmation; pass --yes"
}
if ((!NO_REFRESH)); then
omarchy-plugin-source refresh >/dev/null 2>&1 || true
fi
CATALOG=$(omarchy-plugin-scan 2>/dev/null || echo "[]")
# Map of currently enabled third-party plugins, so we can preserve state.
declare -A enabled_map
if shell_plugins=$(omarchy-shell shell listPlugins 2>/dev/null) && [[ -n $shell_plugins ]]; then
while IFS=$'\t' read -r pid pen; do
[[ -n $pid ]] && enabled_map["$pid"]="$pen"
done < <(jq -r '.[] | select(.firstParty == false) | [.id, (.enabled|tostring)] | @tsv' <<<"$shell_plugins" 2>/dev/null)
fi
# Echoes "<status>\t<source-id>\t<installed-version>\t<upstream-version>\t<cache-path>"
plugin_status() {
local id="$1" installed_version="$2"
local up
up=$(jq -c --arg id "$id" 'map(select(.manifest.id == $id)) | .[0] // empty' <<<"$CATALOG")
if [[ -z $up ]]; then
printf 'unmanaged\t\t%s\t\t\n' "$installed_version"
return
fi
local upstream_version src_id cache_path status
upstream_version=$(jq -r '.manifest.version // ""' <<<"$up")
src_id=$(jq -r '.sourceId' <<<"$up")
cache_path=$(jq -r '.path' <<<"$up")
if [[ -z $installed_version || -z $upstream_version ]]; then
status="unknown"
elif [[ $installed_version == "$upstream_version" ]]; then
status="up-to-date"
elif [[ $(printf '%s\n%s\n' "$installed_version" "$upstream_version" | sort -V | tail -n1) == "$upstream_version" ]]; then
status="update-available"
else
status="local-newer"
fi
printf '%s\t%s\t%s\t%s\t%s\n' "$status" "$src_id" "$installed_version" "$upstream_version" "$cache_path"
}
installed_version_of() {
local id="$1"
[[ -f "$PLUGINS_DIR/$id/manifest.json" ]] || return 1
jq -r '.version // ""' "$PLUGINS_DIR/$id/manifest.json" 2>/dev/null
}
# ---------------------------------------------------------------- pick targets
[[ -d $PLUGINS_DIR ]] || fail "no plugins installed"
declare -a targets=()
if ((ALL)); then
for d in "$PLUGINS_DIR"/*/; do
[[ -d $d ]] || continue
id=$(basename "$d"); [[ $id == .* ]] && continue
iv=$(installed_version_of "$id") || continue
IFS=$'\t' read -r status _ _ _ _ < <(plugin_status "$id" "$iv")
[[ $status == "update-available" ]] && targets+=("$id")
done
((${#targets[@]})) || { echo "No updates available."; exit 0; }
elif [[ -n $PLUGIN_ID ]]; then
installed_version_of "$PLUGIN_ID" >/dev/null || fail "plugin '$PLUGIN_ID' is not installed"
targets=("$PLUGIN_ID")
else
interactive || fail "a plugin-id is required (or pass --all)"
outdated=""
for d in "$PLUGINS_DIR"/*/; do
[[ -d $d ]] || continue
id=$(basename "$d"); [[ $id == .* ]] && continue
iv=$(installed_version_of "$id") || continue
IFS=$'\t' read -r status _ instv upv _ < <(plugin_status "$id" "$iv")
[[ $status == "update-available" ]] && outdated+="$id"$'\t'"$instv -> $upv"$'\n'
done
[[ -n $outdated ]] || { echo "No updates available."; exit 0; }
rows=$(awk -F '\t' 'NF{printf "%-28s %s\n", $1, $2}' <<<"$outdated")
if command -v gum >/dev/null 2>&1; then
pick=$(gum choose --no-limit --header="Update which plugins? (space toggles, enter confirms)" <<<"$rows") || fail "cancelled"
elif command -v fzf >/dev/null 2>&1; then
pick=$(fzf --multi --prompt "Update which plugins (tab to mark) > " <<<"$rows") || fail "cancelled"
else
fail "a plugin-id is required (install gum or fzf for an interactive picker, or pass --all)"
fi
[[ -n $pick ]] || { echo "Nothing selected."; exit 0; }
mapfile -t targets < <(awk '{print $1}' <<<"$pick")
fi
# ---------------------------------------------------------------- apply
show_diff() {
local installed="$1" cache="$2"
echo "Changes for $3 ($4 -> $5):"
if command -v delta >/dev/null 2>&1; then
diff -ruN "$installed" "$cache" | delta --paging=never
else
diff -ruN "$installed" "$cache"
fi
echo
}
any_enabled=0
rc=0
for id in "${targets[@]}"; do
iv=$(installed_version_of "$id") || { echo "Skipping $id: not installed." >&2; rc=1; continue; }
IFS=$'\t' read -r status src_id instv upv cache_path < <(plugin_status "$id" "$iv")
case "$status" in
unmanaged) echo "Skipping $id: not found in any configured source." >&2; rc=1; continue ;;
unknown) echo "Skipping $id: missing version metadata to compare." >&2; rc=1; continue ;;
up-to-date)
((FORCE)) || { echo "Skipping $id: already up to date ($instv). Use --force to re-pull."; continue; } ;;
local-newer)
((FORCE)) || { echo "Skipping $id: installed $instv is newer than upstream $upv. Use --force to overwrite." >&2; rc=1; continue; } ;;
esac
if ((!ASSUME_YES)); then
if ((DO_REVIEW)) || interactive; then
show_diff "$PLUGINS_DIR/$id" "$cache_path" "$id" "$instv" "$upv"
fi
confirm "Update $id ($instv -> $upv)?" || { echo "Skipped $id."; continue; }
fi
echo "Updating $id ..."
install_args=("$id" --from "$src_id" --no-refresh --yes)
case "${enabled_map[$id]:-}" in
true) install_args+=(--enable); any_enabled=1 ;;
false) install_args+=(--no-enable) ;;
esac
if ! omarchy-plugin-add "${install_args[@]}"; then
echo "Failed to update $id" >&2
rc=1
fi
done
if ((any_enabled)); then
echo
echo "Updated plugins that were enabled may need a shell restart to fully reload:"
echo " omarchy restart shell"
fi
exit $rc