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
-305
View File
@@ -1,305 +0,0 @@
#!/bin/bash
# omarchy:summary=Add an Omarchy shell plugin from a trusted source
# omarchy:group=plugin
# omarchy:args=[plugin-id] [--from <source-id>] [--enable] [--no-enable] [--review] [--no-refresh] [--yes]
# omarchy:examples=omarchy plugin add | omarchy plugin add orbit --enable | omarchy plugin add model-usage --from owner-plugins --yes
# Copies a plugin folder out of a trusted source's local clone into
# ~/.config/omarchy/plugins/<id>/. It never runs plugin code, never runs install
# hooks, and never uses sudo — it only copies files, validates the manifest, and
# toggles enabled state over shell IPC. Reviewing and trusting the code is the
# user's call; --yes opts out of every prompt for scripts/agents.
set -o pipefail
PLUGINS_DIR="$HOME/.config/omarchy/plugins"
OMARCHY_PATH="${OMARCHY_PATH:-$HOME/.local/share/omarchy}"
fail() {
echo "omarchy-plugin-add: $*" >&2
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || fail "$1 is required"
}
interactive() {
[[ -t 0 && -t 1 ]]
}
plugin_discovered() {
local id="$1" plugins
plugins=$(omarchy-shell shell listPlugins 2>/dev/null) || return 1
jq -e --arg id "$id" 'any(.[]; .id == $id)' <<<"$plugins" >/dev/null 2>&1
}
wait_for_plugin_discovery() {
local id="$1" attempt
for (( attempt = 0; attempt < 40; attempt++ )); do
plugin_discovered "$id" && return 0
sleep 0.05
done
return 1
}
PLUGIN_ID=""
FROM_SOURCE=""
ENABLE_AFTER="" # "", "true", or "false"
DO_REVIEW=0
NO_REFRESH=0
ASSUME_YES=0
while (($# > 0)); do
case "$1" in
--from) FROM_SOURCE="${2:-}"; shift 2 ;;
--enable) ENABLE_AFTER=true; shift ;;
--no-enable) ENABLE_AFTER=false; 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 add [plugin-id] [--from <source-id>] [--enable] [--no-enable] [--review] [--no-refresh] [--yes]
Adds a plugin from a trusted source into ~/.config/omarchy/plugins/<id>/.
Without a plugin-id, an interactive picker lists everything available.
--from disambiguate when the same id exists in multiple sources
--enable enable the plugin afterward (bar widgets land in the bar)
--no-enable add but leave disabled
--review open the plugin's files before adding (always in a TTY)
--no-refresh use the cached clones as-is, skip the network refresh
--yes skip every confirmation (review, add, enable)
USAGE
exit 0
;;
-*) fail "unknown option: $1" ;;
*)
[[ -z $PLUGIN_ID ]] || fail "unexpected argument: $1"
PLUGIN_ID="$1"; shift ;;
esac
done
require_command jq
confirm() {
local prompt="$1"
((ASSUME_YES)) && return 0
if interactive; then
gum confirm "$prompt"; return
fi
fail "refusing to add without confirmation; pass --yes"
}
if ((!NO_REFRESH)); then
omarchy-plugin-source refresh >/dev/null 2>&1 || true
fi
CATALOG=$(omarchy-plugin-scan)
if [[ $(jq 'length' <<<"$CATALOG") -eq 0 ]]; then
fail "no plugins available; add a source with: omarchy plugin source add <git-url>"
fi
# ---------------------------------------------------------------- resolve id
if [[ -z $PLUGIN_ID ]]; then
interactive || fail "a plugin-id is required (try: omarchy plugin available)"
choices=$(jq -r '.[] | "\(.manifest.id)\t\(.manifest.name // .manifest.id)\t\(.manifest.version // "?")\t\(.sourceId)"' <<<"$CATALOG")
rows=$(awk -F '\t' '{ printf "%-26s %-26s v%-8s [%s]\n", $1, $2, $3, $4 }' <<<"$choices")
pick=$(gum choose --header="Add which plugin?" <<<"$rows") || fail "cancelled"
[[ -n $pick ]] || fail "nothing selected"
PLUGIN_ID=$(awk '{print $1}' <<<"$pick")
fi
if [[ -n $FROM_SOURCE ]]; then
matches=$(jq --arg id "$PLUGIN_ID" --arg src "$FROM_SOURCE" '[.[] | select(.manifest.id == $id and .sourceId == $src)]' <<<"$CATALOG")
else
matches=$(jq --arg id "$PLUGIN_ID" '[.[] | select(.manifest.id == $id)]' <<<"$CATALOG")
fi
match_count=$(jq 'length' <<<"$matches")
if ((match_count == 0)); then
if [[ -n $FROM_SOURCE ]]; then
fail "no plugin '$PLUGIN_ID' in source '$FROM_SOURCE'"
fi
fail "no plugin '$PLUGIN_ID' in any configured source (try: omarchy plugin available)"
fi
if ((match_count > 1)); then
echo "omarchy-plugin-add: '$PLUGIN_ID' is available from multiple sources:" >&2
jq -r '.[] | " - " + .sourceId' <<<"$matches" >&2
fail "pick one with: --from <source-id>"
fi
entry=$(jq -c '.[0]' <<<"$matches")
src_id=$(jq -r '.sourceId' <<<"$entry")
src_url=$(jq -r '.sourceUrl' <<<"$entry")
src_path=$(jq -r '.path' <<<"$entry")
m_id=$(jq -r '.manifest.id' <<<"$entry")
m_name=$(jq -r '.manifest.name // .manifest.id' <<<"$entry")
m_version=$(jq -r '.manifest.version // "?"' <<<"$entry")
m_author=$(jq -r '.manifest.author // ""' <<<"$entry")
m_license=$(jq -r '.manifest.license // ""' <<<"$entry")
m_desc=$(jq -r '.manifest.description // ""' <<<"$entry")
m_kinds=$(jq -r '(.manifest.kinds // []) | join(", ")' <<<"$entry")
# ---------------------------------------------------------------- validate
omarchy-plugin-validate "$src_path" || fail "refusing to add: validation failed"
target="$PLUGINS_DIR/$m_id"
replacing=0
target_is_link=0
if [[ -L $target ]]; then
target_is_link=1
replacing=1
elif [[ -d $target ]]; then
replacing=1
fi
# Soft warning: a third-party id that matches a built-in's alias installs as a
# separate widget rather than replacing the built-in.
alias_owner=""
while IFS= read -r fp_manifest; do
[[ -f $fp_manifest ]] || continue
if jq -e --arg id "$m_id" '((.barWidget.aliases // []) | index($id)) != null' "$fp_manifest" >/dev/null 2>&1; then
alias_owner=$(jq -r '.id' "$fp_manifest")
break
fi
done < <(find "$OMARCHY_PATH/shell/plugins" -type f \( -name manifest.json -o -name '*.manifest.json' \) 2>/dev/null)
# Always surface an alias collision, even under --yes, since it changes what the
# added plugin actually shadows.
if [[ -n $alias_owner ]]; then
echo "Note: '$m_id' is also an alias of the built-in $alias_owner; it is added as a separate plugin and does not replace the built-in." >&2
fi
# ---------------------------------------------------------------- summary
print_summary() {
cat <<INFO
Plugin: $m_name ($m_id)
Version: $m_version${m_author:+
Author: $m_author}${m_license:+
License: $m_license}
Kinds: $m_kinds
Source: $src_id ($src_url)${m_desc:+
About: $m_desc}
INFO
if ((target_is_link)); then
echo " Target: $target (REPLACING a symlink -> $(readlink "$target"))"
elif ((replacing)); then
echo " Target: $target (REPLACING existing install)"
else
echo " Target: $target"
fi
echo
echo " Plugins run as unsandboxed code inside omarchy-shell. Review before enabling."
echo
}
review_files() {
echo "Files in $m_id:"
find "$src_path" -type f -printf '%P\n' 2>/dev/null | sort | sed 's/^/ /'
echo
# Only spawn an editor in a real terminal; a headless run just prints the path
# so it never launches a detached window.
if interactive && command -v omarchy-launch-editor >/dev/null 2>&1; then
omarchy-launch-editor "$src_path"
else
echo "Source folder: $src_path"
fi
}
if ((!ASSUME_YES)); then
print_summary
if ((DO_REVIEW)); then
review_files
confirm "Add $m_id $m_version now?" || fail "aborted"
elif interactive; then
action=$(gum choose --header="Add $m_id $m_version?" "Add" "Review files first" "Cancel") || fail "aborted"
case "$action" in
"Review files first")
review_files
confirm "Add $m_id $m_version now?" || fail "aborted"
;;
Add) ;;
*) fail "aborted" ;;
esac
else
confirm "Add $m_id $m_version?" || fail "aborted"
fi
fi
# ---------------------------------------------------------------- install files
mkdir -p "$PLUGINS_DIR"
stage="$PLUGINS_DIR/.${m_id}.tmp.$$"
rm -rf "$stage"
if ! cp -a "$src_path/." "$stage/"; then
rm -rf "$stage"
fail "failed to stage plugin into $stage"
fi
backup=""
if ((replacing)); then
if ((target_is_link)); then
# Don't drag a (possibly large) dev-symlink target into a backup; just drop
# the link. The real files it points at are untouched.
rm -f "$target"
else
base="$PLUGINS_DIR/.${m_id}.bak.$(date -u +%Y%m%d%H%M%S)"
backup="$base"
n=1
while [[ -e $backup ]]; do backup="${base}-${n}"; n=$((n + 1)); done
if ! mv "$target" "$backup"; then
rm -rf "$stage"
fail "failed to back up existing $target"
fi
fi
fi
if ! mv "$stage" "$target"; then
rm -rf "$stage"
[[ -n $backup && -d $backup ]] && mv "$backup" "$target"
fail "failed to move staged plugin into place"
fi
echo "Added $m_id $m_version into $target"
[[ -n $backup ]] && echo "Previous version backed up to: $backup"
omarchy-shell shell rescanPlugins >/dev/null 2>&1 || true
# ---------------------------------------------------------------- enable
if [[ -z $ENABLE_AFTER ]]; then
if ((ASSUME_YES)) || ! interactive; then
ENABLE_AFTER=false
elif confirm "Enable '$m_id' now?"; then
ENABLE_AFTER=true
else
ENABLE_AFTER=false
fi
fi
if [[ $ENABLE_AFTER == true ]]; then
if ! wait_for_plugin_discovery "$m_id"; then
echo "Could not enable $m_id because omarchy-shell did not discover it yet. Enable later with: omarchy plugin enable $m_id" >&2
elif result=$(omarchy-shell shell setPluginEnabled "$m_id" true) && [[ $result == "ok" ]]; then
echo "Enabled $m_id."
if [[ $m_kinds == *bar-widget* ]]; then
echo "It was added to the bar; arrange it with: omarchy plugin bar move $m_id --section <left|center|right>"
fi
else
echo "Could not enable $m_id over IPC (is omarchy-shell running?). Enable later with: omarchy plugin enable $m_id" >&2
fi
else
echo "Enable it later with: omarchy plugin enable $m_id"
fi
# Reaching here means the files are in place; never let a falsy trailing test
# leak a non-zero status to callers like `omarchy plugin update`.
exit 0