Replace the plugin package manager with plain git

A plugin is now just a git repo cloned into ~/.config/omarchy/plugins/<id>/.
That one idea replaces the entire homegrown package-manager half of the
plugin suite: trusted-source registry, clone cache, catalog scanning,
semver comparison, staging dirs, and timestamped backups — 1,025 lines
across five binaries whose jobs git already does.

Gone:

- omarchy-plugin-source: the trusted-repo registry (sources.json) and its
  clone cache under ~/.cache/omarchy/plugin-sources/. The trust decision
  now happens once, at add time, with the same unsandboxed-code warning.
- omarchy-plugin-scan + omarchy-plugin-available: the catalog machinery
  over cached clones. Discovery belongs on a web page, not in the CLI.
- omarchy-plugin-add: copying folders out of cached source clones with
  hand-rolled staging and .bak backups. Replaced by a git clone.
- omarchy-plugin-update: manifest version comparison via sort -V and
  re-installs. Replaced by fetch + diff + fast-forward; git is the version
  and git is the backup.
- omarchy-plugin-remove and omarchy-plugin-edit as separate binaries:
  folded into omarchy-plugin, much slimmer.

The consolidated omarchy-plugin now handles the full lifecycle:

- add <git-url>: warn, clone into a dot-prefixed staging dir (invisible
  to the plugin scanner), validate, then move into place named by the
  manifest id. Plugins land disabled — enabling is the single consent
  moment, replacing the old review-before-copy flow.
- update [id | --all]: fetch origin HEAD, show the diff (delta when
  available), confirm, fast-forward. Updates are code the shell will run,
  so the result is re-validated and rolled back to ORIG_HEAD if upstream
  turned invalid (e.g. smuggled a symlink).
- remove [id]: git checkouts are deleted outright since upstream keeps
  the history; hand-made plugin folders still get a backup, and dev
  symlinks are just unlinked.
- edit [id]: opens the user plugin directory in a shell.

All commands keep the interactive/unattended split: gum prompts in a
terminal, hard refusal without --yes otherwise, so scripts and agents
never hang on a hidden prompt.

Kept as siblings: omarchy-plugin-catalog (omarchy-bar reads it),
omarchy-plugin-validate (the security boundary, now pruning .git from its
symlink scan since installs are git checkouts), and omarchy-plugin-clone
(local development of built-in widgets, a separate concern).

Trade-offs accepted: one repo = one plugin (no more multi-plugin source
repos), and ref pinning or branch switching is no longer a CLI feature —
an installed plugin is a plain checkout, so that is ordinary git in the
plugin directory.

None of the removed machinery ever shipped: it existed only on this
branch, so there is no migration. The net effect is 11 scripts / 2,040
lines down to 4 scripts / 1,080 lines, and one less concept for users to
learn — everyone already knows what a git repo is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-07-02 21:47:33 -07:00
co-authored by Claude Fable 5
parent 60e0a2a0cd
commit 798d6af8b0
12 changed files with 390 additions and 1355 deletions
-207
View File
@@ -1,207 +0,0 @@
#!/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 interactive; then gum confirm "$prompt"; 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")
pick=$(gum choose --no-limit --header="Update which plugins? (space toggles, enter confirms)" <<<"$rows") || fail "cancelled"
[[ -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 enabled plugins were hot-reloaded by omarchy-shell."
fi
exit $rc