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
+356 -37
View File
@@ -2,49 +2,50 @@
# omarchy:summary=Manage Omarchy shell plugins and bar widgets
# omarchy:group=plugin
# omarchy:args=<list|rescan|enable|disable|clone|edit|bar> [...]
# omarchy:examples=omarchy plugin list | omarchy plugin clone omarchy.clock local.clock | omarchy plugin edit local.clock | omarchy plugin enable acme.weather --section right | omarchy plugin bar settings
# omarchy:args=<list|rescan|enable|disable|add|update|remove|clone|edit|validate|bar> [...]
# omarchy:examples=omarchy plugin list | omarchy plugin add https://github.com/acme/omarchy-weather.git | omarchy plugin update --all | omarchy plugin clone omarchy.clock local.clock | omarchy plugin bar settings
set -euo pipefail
PLUGINS_DIR="$HOME/.config/omarchy/plugins"
# Never let git block an unattended run on a credential or host-key prompt; fail
# fast instead so error paths can handle it.
export GIT_TERMINAL_PROMPT=0
export GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -oBatchMode=yes}"
usage() {
cat <<USAGE
Usage: omarchy-plugin <command> [args...]
Plugin commands:
list [--json] List discovered shell plugins
rescan Rescan ~/.config/omarchy/plugins
enable <id> [placement] Enable a plugin
disable <id> Disable a plugin
clone [source] [new-id] [options] Clone a built-in or user plugin
edit [id] Open a user plugin directory in a shell
Manage plugins:
list [--json] List discovered shell plugins
rescan Rescan ~/.config/omarchy/plugins
enable <id> [placement] Enable a plugin
disable <id> Disable a plugin
Add from sources (see 'omarchy plugin <command> --help'):
source <add|list|remove|refresh> Manage trusted plugin source repos
available List plugins offered by your sources
add [id] [--from <src>] [--enable] [--review] [--yes]
Add a plugin from a trusted source
update [id] [--all] [--review] [--yes]
Update added plugins (shows a diff)
remove [id] [--yes] Remove an installed plugin
validate <plugin-folder> Check a plugin's manifest (for authors)
Install from git (a plugin is a git repo):
add [git-url] [--enable] [--yes] Clone a plugin repo into your plugins
update [id | --all] [--yes] Fetch, review the diff, fast-forward
remove [id] [--yes] Disable and delete an installed plugin
Source/add commands run their own binaries; the rest are handled here.
Plugins are unsandboxed code — review what you add and enable.
Make your own:
clone [source] [new-id] [options] Clone a built-in or user plugin
edit [id] Open a user plugin directory in a shell
validate <plugin-folder> Check a plugin's manifest (for authors)
Bar commands (delegate to 'omarchy bar'; run 'omarchy bar --help' for details):
bar settings Open the inline bar config panel
bar settings Open the inline bar config panel
bar list|layout|options|use|reset|add|move|remove|set|position|transparent
widget|widgets Alias of 'bar list'
widget|widgets Alias of 'bar list'
Examples:
omarchy plugin list
omarchy plugin rescan
omarchy plugin add https://github.com/acme/omarchy-weather.git --enable
omarchy plugin update --all
omarchy plugin clone omarchy.clock local.clock --name "My Clock" --replace
omarchy plugin edit local.clock
omarchy plugin enable acme.weather --section right
omarchy plugin bar add acme.weather --section right
omarchy plugin bar set omarchy.clock format HH:mm
USAGE
}
@@ -61,6 +62,33 @@ require_command() {
command -v "$1" >/dev/null 2>&1 || fail "$1 is required"
}
interactive() {
[[ -t 0 && -t 1 ]]
}
# Yes/no prompt. Honours ASSUME_YES, and refuses in a non-interactive context so
# an agent must pass --yes deliberately rather than hang on a prompt.
ASSUME_YES=0
confirm() {
local prompt="$1"
(( ASSUME_YES )) && return 0
if interactive; then
gum confirm "$prompt"
return
fi
fail "refusing to continue without confirmation; pass --yes"
}
# Plugin ids become paths under PLUGINS_DIR that we mv/rm, so reject anything
# that could escape it (matches the id rules in omarchy-plugin-validate).
valid_plugin_id() {
[[ $1 =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ && $1 != *..* ]]
}
installed_plugin_ids() {
find "$PLUGINS_DIR" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) ! -name '.*' -printf '%f\n' 2>/dev/null | sort
}
plugin_discovered() {
local id="$1" plugins
plugins=$(omarchy-shell shell listPlugins 2>/dev/null) || return 1
@@ -154,6 +182,290 @@ plugin_enabled() {
fi
}
# ---------------------------------------------------------------- add
plugin_add() {
require_command git
require_command jq
local url="" enable_after=""
while (( $# > 0 )); do
case "$1" in
--enable) enable_after=true; shift ;;
--no-enable) enable_after=false; shift ;;
--yes | -y) ASSUME_YES=1; shift ;;
-h | --help) usage; return 0 ;;
-*) fail "unknown add option: $1" ;;
*)
[[ -z $url ]] || fail "unexpected argument: $1"
url="$1"; shift ;;
esac
done
if [[ -z $url ]]; then
interactive || fail "a git URL is required (e.g. omarchy plugin add https://github.com/acme/omarchy-weather.git)"
url=$(gum input --prompt "Git URL of the plugin repo: ") || fail "cancelled"
[[ -n $url ]] || fail "a git URL is required"
fi
if (( ! ASSUME_YES )); then
cat >&2 <<WARN
⚠️ Plugins run as arbitrary, unsandboxed code inside your long-lived
omarchy-shell process. Only add repos you trust, and review the code
before you enable it.
URL: $url
WARN
confirm "Clone and add this plugin?" || fail "aborted"
fi
mkdir -p "$PLUGINS_DIR"
# Clone into a dot-prefixed staging dir (invisible to the plugin scanner) so
# a plugin only ever appears under its manifest id, fully validated.
local stage="$PLUGINS_DIR/.add.tmp.$$"
rm -rf "$stage"
if ! git clone -- "$url" "$stage"; then
rm -rf "$stage"
fail "failed to clone $url"
fi
if ! omarchy-plugin-validate "$stage"; then
rm -rf "$stage"
fail "refusing to add: validation failed"
fi
local id
id=$(jq -r '.id' "$stage/manifest.json")
local target="$PLUGINS_DIR/$id"
if [[ -e $target || -L $target ]]; then
rm -rf "$stage"
fail "plugin '$id' is already installed; update it with: omarchy plugin update $id"
fi
mv "$stage" "$target"
echo "Added $id into $target"
omarchy-shell shell rescanPlugins >/dev/null 2>&1 || true
if [[ -z $enable_after ]]; then
if (( ASSUME_YES )) || ! interactive; then
enable_after=false
elif confirm "Enable '$id' now?"; then
enable_after=true
else
enable_after=false
fi
fi
if [[ $enable_after == true ]]; then
if wait_for_plugin_discovery "$id" && [[ $(omarchy-shell shell setPluginEnabled "$id" true) == "ok" ]]; then
echo "Enabled $id"
else
echo "Could not enable $id (is omarchy-shell running?). Enable later with: omarchy plugin enable $id" >&2
fi
else
echo "Enable it later with: omarchy plugin enable $id"
fi
}
# ---------------------------------------------------------------- update
UPDATED_ANY=0
update_one() {
local id="$1"
local dir="$PLUGINS_DIR/$id"
if ! git -C "$dir" fetch --quiet origin HEAD; then
echo "omarchy-plugin: fetch failed for '$id'" >&2
return 1
fi
if [[ $(git -C "$dir" rev-parse HEAD) == $(git -C "$dir" rev-parse FETCH_HEAD) ]]; then
echo "$id is up to date."
return 0
fi
if (( ! ASSUME_YES )); then
echo "Changes for $id:"
if command -v delta >/dev/null 2>&1; then
git -C "$dir" diff HEAD FETCH_HEAD | delta --paging=never
else
git -C "$dir" diff HEAD FETCH_HEAD
fi
echo
confirm "Update $id?" || { echo "Skipped $id."; return 0; }
fi
if ! git -C "$dir" merge --ff-only FETCH_HEAD >/dev/null 2>&1; then
echo "omarchy-plugin: cannot fast-forward '$id'; you have local changes in $dir" >&2
return 1
fi
# An update is code the shell will run, same as an add: re-validate, and roll
# back to the pre-merge commit if upstream turned invalid.
if ! omarchy-plugin-validate "$dir"; then
git -C "$dir" reset --hard ORIG_HEAD >/dev/null
echo "omarchy-plugin: update of '$id' failed validation; rolled back" >&2
return 1
fi
echo "Updated $id."
UPDATED_ANY=1
}
plugin_update() {
require_command git
local id="" all=0
while (( $# > 0 )); do
case "$1" in
--all | -a) all=1; shift ;;
--yes | -y) ASSUME_YES=1; shift ;;
-h | --help) usage; return 0 ;;
-*) fail "unknown update option: $1" ;;
*)
[[ -z $id ]] || fail "unexpected argument: $1"
id="$1"; shift ;;
esac
done
if (( all )) && [[ -n $id ]]; then
fail "pass either a plugin-id or --all, not both"
fi
[[ -d $PLUGINS_DIR ]] || fail "no plugins installed"
local -a targets=()
if [[ -n $id ]]; then
valid_plugin_id "$id" || fail "invalid plugin id '$id'"
[[ -d "$PLUGINS_DIR/$id" ]] || fail "plugin '$id' is not installed"
[[ -d "$PLUGINS_DIR/$id/.git" ]] || fail "plugin '$id' is not a git checkout, so there is nothing to pull from"
targets=("$id")
else
local dir
for dir in "$PLUGINS_DIR"/*/; do
[[ -d $dir/.git ]] || continue
targets+=("$(basename "$dir")")
done
if (( ${#targets[@]} == 0 )); then
echo "No git-managed plugins installed."
return 0
fi
fi
local rc=0
for id in "${targets[@]}"; do
update_one "$id" || rc=1
done
if (( UPDATED_ANY )); then
omarchy-shell shell rescanPlugins >/dev/null 2>&1 || true
fi
return $rc
}
# ---------------------------------------------------------------- remove
plugin_remove() {
local id=""
while (( $# > 0 )); do
case "$1" in
--yes | -y) ASSUME_YES=1; shift ;;
-h | --help) usage; return 0 ;;
-*) fail "unknown remove option: $1" ;;
*)
[[ -z $id ]] || fail "unexpected argument: $1"
id="$1"; shift ;;
esac
done
[[ -d $PLUGINS_DIR ]] || fail "no plugins installed"
if [[ -z $id ]]; then
interactive || fail "a plugin-id is required"
id=$(installed_plugin_ids | gum choose --header="Remove which plugin?") || fail "cancelled"
[[ -n $id ]] || fail "nothing selected"
fi
valid_plugin_id "$id" || fail "invalid plugin id '$id'"
local target="$PLUGINS_DIR/$id"
[[ -e $target || -L $target ]] || fail "plugin '$id' is not installed"
local was_enabled=""
local shell_plugins
if shell_plugins=$(omarchy-shell shell listPlugins 2>/dev/null) && [[ -n $shell_plugins ]]; then
was_enabled=$(jq -r --arg id "$id" '.[] | select(.id == $id) | .enabled' <<<"$shell_plugins" 2>/dev/null) || true
fi
if [[ -L $target ]]; then
confirm "Unlink '$id' (symlink -> $(readlink "$target"))?" || fail "aborted"
elif [[ -d $target/.git ]]; then
confirm "Delete '$id'? Its git repo remains upstream." || fail "aborted"
else
confirm "Remove '$id'? The folder will be backed up." || fail "aborted"
fi
[[ $was_enabled == "true" ]] && omarchy-shell shell setPluginEnabled "$id" false >/dev/null 2>&1 || true
if [[ -L $target ]]; then
# A dev symlink is just unlinked; the files it points at are left alone.
rm -f "$target"
echo "Unlinked $id."
elif [[ -d $target/.git ]]; then
rm -rf "$target"
echo "Removed $id."
else
# A hand-made plugin may be the user's only copy, so keep a backup.
local base="$PLUGINS_DIR/.${id}.bak.$(date -u +%Y%m%d%H%M%S)"
local backup="$base" n=1
while [[ -e $backup ]]; do backup="${base}-${n}"; n=$((n + 1)); done
mv "$target" "$backup" || fail "failed to move $target to backup"
echo "Removed $id. Backup at: $backup"
fi
omarchy-shell shell rescanPlugins >/dev/null 2>&1 || true
if [[ $was_enabled == "true" ]]; then
echo "Plugin was enabled and was unloaded from omarchy-shell."
fi
}
# ---------------------------------------------------------------- edit
plugin_edit() {
local id=""
while (( $# > 0 )); do
case "$1" in
-h | --help) usage; return 0 ;;
-*) fail "unknown edit option: $1" ;;
*)
[[ -z $id ]] || fail "unexpected argument: $1"
id="$1"; shift ;;
esac
done
if [[ -z $id ]]; then
interactive || fail "a plugin id is required"
id=$(installed_plugin_ids | gum choose --header="Edit which plugin?") || fail "cancelled"
[[ -n $id ]] || fail "nothing selected"
fi
valid_plugin_id "$id" || fail "invalid plugin id '$id'"
[[ $id != omarchy.* ]] || fail "$id is built in; clone it first with: omarchy plugin clone $id"
local dir="$PLUGINS_DIR/$id"
[[ -f $dir/manifest.json ]] || fail "no user plugin at $dir"
if interactive; then
cd "$dir"
exec "${SHELL:-bash}"
fi
printf '%s\n' "$dir"
}
# ---------------------------------------------------------------- bar
bar_command() {
local command="${1:-list}"
[[ $# -gt 0 ]] && shift || true
@@ -190,27 +502,34 @@ disable)
shift
plugin_enabled false "$@"
;;
add | install)
shift
plugin_add "$@"
;;
update)
shift
plugin_update "$@"
;;
remove | rm)
shift
plugin_remove "$@"
;;
edit)
shift
plugin_edit "$@"
;;
clone)
shift
exec omarchy-plugin-clone "$@"
;;
edit)
validate)
shift
exec omarchy-plugin-edit "$@"
exec omarchy-plugin-validate "$@"
;;
bar | widget | widgets)
shift
bar_command "$@"
;;
source | available | add | update | remove | validate)
# These live in sibling binaries (omarchy-plugin-<command>). The `omarchy`
# dispatcher normally routes straight to them; delegate here too so invoking
# this base binary directly matches the commands its --help advertises.
shift
sibling="$(dirname -- "${BASH_SOURCE[0]}")/omarchy-plugin-$command"
[[ -x $sibling ]] || fail "missing helper: omarchy-plugin-$command"
exec "$sibling" "$@"
;;
-h | --help | help | "")
usage
;;
-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
-98
View File
@@ -1,98 +0,0 @@
#!/bin/bash
# omarchy:summary=List plugins available across trusted sources and their install status
# omarchy:group=plugin
# omarchy:args=[--json] [--no-refresh]
# omarchy:examples=omarchy plugin available | omarchy plugin available --json
set -o pipefail
PLUGINS_DIR="$HOME/.config/omarchy/plugins"
fail() {
echo "omarchy-plugin-available: $*" >&2
exit 1
}
command -v jq >/dev/null 2>&1 || fail "jq is required"
JSON=0
NO_REFRESH=0
while (($# > 0)); do
case "$1" in
--json) JSON=1; shift ;;
--no-refresh) NO_REFRESH=1; shift ;;
-h | --help)
cat <<USAGE
Usage: omarchy plugin available [--json] [--no-refresh]
Lists every plugin found across your trusted sources, with whether it is
installed and whether an update is available. Add one with:
omarchy plugin add <plugin-id>
USAGE
exit 0
;;
*) fail "unknown option: $1" ;;
esac
done
if ((!NO_REFRESH)); then
omarchy-plugin-source refresh >/dev/null 2>&1 || true
fi
CATALOG=$(omarchy-plugin-scan)
# Annotate each catalogue entry with a stable status token (available |
# installed | update-available | local-newer, matching omarchy-plugin-update's
# vocabulary) plus the installed version, by comparing the installed manifest
# version (if any) against the upstream version. Entries are read as compact
# JSON objects so an empty version field never shifts the parse.
rows='[]'
while IFS= read -r row; do
[[ -n $row ]] || continue
id=$(jq -r '.id' <<<"$row")
name=$(jq -r '.name' <<<"$row")
version=$(jq -r '.version' <<<"$row")
source=$(jq -r '.source' <<<"$row")
installed=""
[[ -f "$PLUGINS_DIR/$id/manifest.json" ]] && installed=$(jq -r '.version // ""' "$PLUGINS_DIR/$id/manifest.json" 2>/dev/null)
if [[ -z $installed ]]; then
status="available"
elif [[ $installed == "$version" ]]; then
status="installed"
elif [[ $(printf '%s\n%s\n' "$installed" "$version" | sort -V | tail -n1) == "$version" ]]; then
status="update-available"
else
status="local-newer"
fi
entry=$(jq -n --arg id "$id" --arg name "$name" --arg version "$version" \
--arg installed "$installed" --arg source "$source" --arg status "$status" \
'{id:$id, name:$name, version:$version, installedVersion:$installed, source:$source, status:$status}')
rows=$(jq --argjson e "$entry" '. + [$e]' <<<"$rows")
done < <(jq -c '.[] | {id: .manifest.id, name: (.manifest.name // .manifest.id), version: (.manifest.version // ""), source: .sourceId}' <<<"$CATALOG")
if ((JSON)); then
echo "$rows"
exit 0
fi
if [[ $(jq 'length' <<<"$rows") -eq 0 ]]; then
echo "No plugins available. Add a source with: omarchy plugin source add <git-url>"
exit 0
fi
# Render the human status from the token + versions (ASCII arrow, matching update).
jq -r '.[] | [.id, .name, .version, .source, .status, .installedVersion] | @tsv' <<<"$rows" |
awk -F '\t' '
function human(st, inst, ver) {
if (st == "update-available") return "update (" inst " -> " ver ")"
if (st == "local-newer") return "local-newer (" inst ")"
return st
}
BEGIN { printf "%-26s %-26s %-10s %-30s %s\n", "ID", "NAME", "VERSION", "SOURCE", "STATUS" }
{ printf "%-26s %-26s %-10s %-30s %s\n", $1, $2, ("v" ($3=="" ? "?" : $3)), $4, human($5, $6, $3) }'
echo
echo "Add with: omarchy plugin add <id>"
-157
View File
@@ -1,157 +0,0 @@
#!/bin/bash
# omarchy:summary=Open a user Omarchy shell plugin directory in a shell
# omarchy:group=plugin
# omarchy:args=[plugin-id]
# omarchy:examples=omarchy plugin edit | omarchy plugin edit local.clock
set -euo pipefail
PLUGINS_DIR="$HOME/.config/omarchy/plugins"
fail() {
echo "omarchy-plugin-edit: $*" >&2
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || fail "$1 is required"
}
interactive() {
[[ -t 0 && -t 1 ]]
}
canonical_widget_id() {
printf '%s' "$1"
}
slug_id() {
tr '[:upper:]' '[:lower:]' <<<"$1" | sed -E 's/^omarchy\.//; s/[^a-z0-9]+/-/g; s/^-+//; s/-+$//'
}
require_omarchy_path() {
[[ -n ${OMARCHY_PATH:-} ]] || fail "OMARCHY_PATH is not set"
}
# A built-in id is one the catalog knows as first-party (or an omarchy.* id).
builtin_id_known() {
local id="$1"
require_command jq
require_omarchy_path
omarchy-plugin-catalog | jq -e --arg id "$id" \
'any(.[]; .id == $id and .firstParty)' >/dev/null 2>&1
}
validate_user_plugin_dir() {
local dir="$1"
local requested_id="${2:-}"
local root="$PLUGINS_DIR"
local dir_abs root_abs manifest_id
require_command jq
require_command python3
[[ -d $dir ]] || fail "plugin directory not found: $dir"
dir_abs=$(python3 -c 'from pathlib import Path; import sys; print(Path(sys.argv[1]).expanduser().resolve(strict=False))' "$dir")
root_abs=$(python3 -c 'from pathlib import Path; import sys; print(Path(sys.argv[1]).expanduser().resolve(strict=False))' "$root")
[[ $dir_abs == $root_abs/* ]] || fail "plugin edit only opens user plugins under $root"
[[ -f $dir_abs/manifest.json ]] || fail "plugin directory missing manifest.json: $dir_abs"
manifest_id=$(jq -r '.id // empty' "$dir_abs/manifest.json" 2>/dev/null || true)
[[ -n $manifest_id ]] || fail "plugin manifest missing id: $dir_abs/manifest.json"
[[ $manifest_id != omarchy.* && $(canonical_widget_id "$manifest_id") == $manifest_id ]] \
|| fail "$manifest_id is built in; clone it first with: omarchy plugin clone $manifest_id local.$(slug_id "$manifest_id")"
if [[ -n $requested_id && $manifest_id != $requested_id ]]; then
fail "plugin id mismatch: requested $requested_id but manifest declares $manifest_id"
fi
printf '%s' "$dir_abs"
}
plugin_dir_for_id() {
local id="$1"
if [[ $id == */* ]]; then
validate_user_plugin_dir "$id"
return
fi
id=$(canonical_widget_id "$id")
if builtin_id_known "$id" || [[ $id == omarchy.* ]]; then
fail "$id is built in; clone it first with: omarchy plugin clone $id local.$(slug_id "$id")"
fi
validate_user_plugin_dir "$PLUGINS_DIR/$id" "$id"
}
edit_source_options() {
[[ -d $PLUGINS_DIR ]] || return 0
find "$PLUGINS_DIR" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) 2>/dev/null | sort | while IFS= read -r dir; do
[[ -f $dir/manifest.json ]] || continue
id=$(jq -r '.id // empty' "$dir/manifest.json" 2>/dev/null || true)
name=$(jq -r '.name // .id // empty' "$dir/manifest.json" 2>/dev/null || true)
[[ -n $id ]] && printf '%s\t%s\t%s\n' "$id" "${name:-$id}" "$dir"
done
}
choose_edit_source() {
local selected
selected=$(edit_source_options | awk -F '\t' '{ printf "%-32s %s\n", $2, $1 }' \
| gum filter --header "Edit plugin" --placeholder "Search user plugins..." --limit 1) || return 1
awk '{ print $NF }' <<<"$selected"
}
usage() {
cat <<USAGE
Usage: omarchy plugin edit [plugin-id]
Opens a user plugin directory (~/.config/omarchy/plugins/<id>) in a shell so you
can edit it. Built-in plugins must be cloned first with 'omarchy plugin clone'.
Without an id in a terminal, an interactive picker lists your user plugins.
Examples:
omarchy plugin edit
omarchy plugin edit local.clock
USAGE
}
edit_command() {
local id=""
if (( $# > 0 )) && [[ $1 != --* ]]; then
id="$1"
shift
fi
while (( $# > 0 )); do
case "$1" in
-h | --help)
usage
return
;;
*)
fail "unknown edit option: $1"
;;
esac
done
if [[ -z $id ]]; then
if interactive; then
id=$(choose_edit_source) || fail "edit cancelled"
else
fail "plugin id is required"
fi
fi
local dir
dir=$(plugin_dir_for_id "$id")
if interactive; then
cd "$dir"
exec "${SHELL:-bash}"
fi
printf '%s\n' "$dir"
}
edit_command "$@"
-99
View File
@@ -1,99 +0,0 @@
#!/bin/bash
# omarchy:summary=Remove an installed Omarchy shell plugin
# omarchy:group=plugin
# omarchy:args=[plugin-id] [--yes]
# omarchy:examples=omarchy plugin remove | omarchy plugin remove orbit --yes
# Disables a plugin and removes it from ~/.config/omarchy/plugins/. A real
# install is moved to a timestamped backup so it can be restored; a dev symlink
# is just unlinked (the files it points at are left alone).
set -o pipefail
PLUGINS_DIR="$HOME/.config/omarchy/plugins"
fail() {
echo "omarchy-plugin-remove: $*" >&2
exit 1
}
interactive() {
[[ -t 0 && -t 1 ]]
}
PLUGIN_ID=""
ASSUME_YES=0
while (($# > 0)); do
case "$1" in
--yes | -y) ASSUME_YES=1; shift ;;
-h | --help)
cat <<USAGE
Usage: omarchy plugin remove [plugin-id] [--yes]
Disables the plugin and removes it from ~/.config/omarchy/plugins/.
A real install is backed up to .<id>.bak.<timestamp>/; a symlink is unlinked.
USAGE
exit 0
;;
-*) fail "unknown option: $1" ;;
*)
[[ -z $PLUGIN_ID ]] || fail "unexpected argument: $1"
PLUGIN_ID="$1"; shift ;;
esac
done
confirm() {
local prompt="$1"
((ASSUME_YES)) && return 0
if interactive; then gum confirm "$prompt"; return; fi
fail "refusing to remove without confirmation; pass --yes"
}
[[ -d $PLUGINS_DIR ]] || fail "no plugins installed"
if [[ -z $PLUGIN_ID ]]; then
mapfile -t ids < <(find "$PLUGINS_DIR" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) ! -name '.*' -printf '%f\n' 2>/dev/null | sort)
((${#ids[@]})) || fail "no plugins installed"
interactive || fail "a plugin-id is required"
PLUGIN_ID=$(printf '%s\n' "${ids[@]}" | gum choose --header="Remove which plugin?") || fail "cancelled"
[[ -n $PLUGIN_ID ]] || fail "nothing selected"
fi
# The id becomes a path under PLUGINS_DIR that we mv/rm, so reject anything that
# could escape it (matches the id rules in omarchy-plugin-validate).
[[ $PLUGIN_ID =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ && $PLUGIN_ID != *"/"* && $PLUGIN_ID != *".."* ]] \
|| fail "invalid plugin id '$PLUGIN_ID'"
target="$PLUGINS_DIR/$PLUGIN_ID"
[[ -e $target || -L $target ]] || fail "plugin '$PLUGIN_ID' is not installed"
was_enabled=""
if shell_plugins=$(omarchy-shell shell listPlugins 2>/dev/null) && [[ -n $shell_plugins ]]; then
was_enabled=$(jq -r --arg id "$PLUGIN_ID" '.[] | select(.id == $id) | .enabled' <<<"$shell_plugins" 2>/dev/null)
fi
if [[ -L $target ]]; then
confirm "Unlink '$PLUGIN_ID' (symlink -> $(readlink "$target"))?" || fail "aborted"
else
confirm "Remove '$PLUGIN_ID'? The folder will be backed up." || fail "aborted"
fi
[[ $was_enabled == "true" ]] && omarchy-shell shell setPluginEnabled "$PLUGIN_ID" false >/dev/null 2>&1 || true
if [[ -L $target ]]; then
rm -f "$target" || fail "failed to unlink $target"
echo "Unlinked $PLUGIN_ID."
else
base="$PLUGINS_DIR/.${PLUGIN_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
mv "$target" "$backup" || fail "failed to move $target to backup"
echo "Removed $PLUGIN_ID. Backup at: $backup"
fi
omarchy-shell shell rescanPlugins >/dev/null 2>&1 || true
if [[ $was_enabled == "true" ]]; then
echo "Plugin was enabled and was unloaded from omarchy-shell."
fi
-60
View File
@@ -1,60 +0,0 @@
#!/bin/bash
# omarchy:summary=List every plugin available across configured sources (JSON)
# omarchy:group=plugin
# omarchy:hidden=true
# Walks the locally cached clones of every trusted source and emits one JSON
# entry per plugin folder it finds. This is the shared catalogue that
# `available`, `install`, and `update` all read; it never touches the network
# (run `omarchy plugin source refresh` first to pull fresh clones).
set -o pipefail
SOURCES_FILE="$HOME/.config/omarchy/plugins/sources.json"
CACHE_ROOT="$HOME/.cache/omarchy/plugin-sources"
command -v jq >/dev/null 2>&1 || { echo "omarchy-plugin-scan: jq is required" >&2; exit 1; }
if [[ ! -f $SOURCES_FILE ]]; then
echo "[]"
exit 0
fi
results='[]'
# Read each source as a compact JSON object (one per line) rather than a
# delimiter-joined string — no separator to get wrong, and empty fields survive.
while IFS= read -r src_json; do
[[ -n $src_json ]] || continue
src_id=$(jq -r '.id // ""' <<<"$src_json")
src_url=$(jq -r '.url // ""' <<<"$src_json")
src_ref=$(jq -r '.ref // ""' <<<"$src_json")
[[ -n $src_id ]] || continue
cache_dir="$CACHE_ROOT/$src_id"
[[ -d $cache_dir ]] || continue
for folder in "$cache_dir"/*/; do
[[ -d $folder ]] || continue
manifest="$folder/manifest.json"
[[ -f $manifest ]] || continue
jq -e . "$manifest" >/dev/null 2>&1 || continue
folder_name=$(basename "$folder")
abs_path=$(cd "$folder" && pwd)
entry=$(jq -n \
--arg sourceId "$src_id" \
--arg sourceUrl "$src_url" \
--arg ref "$src_ref" \
--arg folder "$folder_name" \
--arg path "$abs_path" \
--slurpfile manifest "$manifest" \
'{sourceId:$sourceId, sourceUrl:$sourceUrl, ref:$ref, folder:$folder, path:$path, manifest:$manifest[0]}') || continue
results=$(jq --argjson e "$entry" '. + [$e]' <<<"$results")
done
done < <(jq -c '.sources[]?' "$SOURCES_FILE")
echo "$results"
-354
View File
@@ -1,354 +0,0 @@
#!/bin/bash
# omarchy:summary=Manage trusted git repos that Omarchy plugins are installed from
# omarchy:group=plugin
# omarchy:args=<add|list|remove|refresh> [...]
# omarchy:examples=omarchy plugin source add | omarchy plugin source list | omarchy plugin source add https://github.com/owner/omarchy-plugins.git --as owner-plugins | omarchy plugin source refresh
set -o pipefail
# Never let git block an unattended run on a credential or host-key prompt; fail
# fast instead so callers' `|| true` / error paths can handle it.
export GIT_TERMINAL_PROMPT=0
export GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -oBatchMode=yes}"
SOURCES_FILE="$HOME/.config/omarchy/plugins/sources.json"
CACHE_ROOT="$HOME/.cache/omarchy/plugin-sources"
fail() {
echo "omarchy-plugin-source: $*" >&2
exit 1
}
# A source id becomes a cache directory name and a sources.json key, so it must
# never contain path separators or traversal. Used for --as, derived ids, and
# (defensively) any id read back before an rm -rf.
valid_source_id() {
[[ $1 =~ ^[a-z0-9][a-z0-9._-]*$ && $1 != *..* ]]
}
require_command() {
command -v "$1" >/dev/null 2>&1 || fail "$1 is required"
}
interactive() {
[[ -t 0 && -t 1 ]]
}
# Yes/no prompt. Honours a caller-set ASSUME_YES, and refuses in a
# non-interactive context so an agent must pass --yes deliberately rather than
# hang on a prompt.
confirm() {
local prompt="$1"
[[ ${ASSUME_YES:-0} == 1 ]] && return 0
if interactive; then
gum confirm "$prompt"
return
fi
fail "refusing to continue without confirmation; pass --yes"
}
ask_input() {
local prompt="$1"
gum input --prompt "$prompt " || return 1
}
ensure_sources_file() {
mkdir -p "$(dirname "$SOURCES_FILE")"
if [[ ! -f $SOURCES_FILE ]]; then
# Subshell the umask so it does not leak into the rest of this process.
(umask 077; echo '{"version":1,"sources":[]}' >"$SOURCES_FILE")
fi
}
# Persist a jq transform of the sources file, then keep it owner-only.
write_sources() {
local filter="$1"
shift
local tmp
tmp=$(mktemp)
if ! jq "$@" "$filter" "$SOURCES_FILE" >"$tmp"; then
rm -f "$tmp"
fail "failed to update $SOURCES_FILE"
fi
mv "$tmp" "$SOURCES_FILE"
chmod 600 "$SOURCES_FILE" 2>/dev/null || true
}
# owner-repo, lowercased and sanitized, from any git URL form (scp-like or URL).
derive_source_id() {
local url="$1"
url="${url%.git}"
url="${url%/}"
local path
if [[ $url =~ ^[^/]+@[^/]+:(.+)$ ]]; then
path="${BASH_REMATCH[1]}"
else
path="${url#*://}"
path="${path#*/}"
fi
local repo="${path##*/}"
local owner_part="${path%/*}"
local owner="${owner_part##*/}"
local id
if [[ -n $owner && $owner != "$path" ]]; then
id="${owner}-${repo}"
else
id="$repo"
fi
id="${id,,}"
id="${id//[^a-z0-9._-]/-}"
while [[ $id == -* ]]; do id="${id#-}"; done
while [[ $id == *- ]]; do id="${id%-}"; done
printf '%s' "$id"
}
source_exists() {
jq -e --arg id "$1" '.sources | map(.id) | index($id) != null' "$SOURCES_FILE" >/dev/null 2>&1
}
# ---------------------------------------------------------------- add
source_add() {
require_command jq
require_command git
local url="" ref="" name=""
ASSUME_YES=0
while (($# > 0)); do
case "$1" in
--ref) ref="${2:-}"; shift 2 ;;
--as) name="${2:-}"; shift 2 ;;
--yes | -y) ASSUME_YES=1; shift ;;
-h | --help) usage; return 0 ;;
--) shift; break ;;
-*) fail "unknown option: $1" ;;
*)
[[ -z $url ]] || fail "unexpected argument: $1"
url="$1"; shift ;;
esac
done
if [[ -z $url ]]; then
interactive || fail "a git URL is required (e.g. omarchy plugin source add https://github.com/owner/repo.git)"
url=$(ask_input "Git URL of the plugin source repo:") || fail "cancelled"
[[ -n $url ]] || fail "a git URL is required"
fi
[[ -n $name ]] || name=$(derive_source_id "$url")
[[ -n $name ]] || fail "could not derive a source id from '$url'; re-run with --as <name>"
valid_source_id "$name" || fail "invalid source id '$name' (lowercase letters, digits, '.', '_', '-'; no '/' or '..')"
ensure_sources_file
if source_exists "$name"; then
local existing
existing=$(jq -r --arg id "$name" '.sources[] | select(.id == $id) | .url' "$SOURCES_FILE")
fail "source '$name' already exists (url: $existing); remove it first with: omarchy plugin source remove $name"
fi
cat >&2 <<WARN
⚠️ You are about to trust a new plugin source.
Name: $name
URL: $url
Ref: ${ref:-<default branch>}
Plugins from this repo run as arbitrary code inside your long-lived
omarchy-shell process. They are not sandboxed. Only add sources you
trust, and review a plugin's code before you install and enable it.
WARN
confirm "Trust this source?" || fail "aborted"
local cache_dir="$CACHE_ROOT/$name"
rm -rf "$cache_dir"
mkdir -p "$CACHE_ROOT"
local clone_args=(--depth 1)
[[ -n $ref ]] && clone_args+=(--branch "$ref")
echo "Cloning $url ..."
if ! git clone "${clone_args[@]}" -- "$url" "$cache_dir"; then
rm -rf "$cache_dir"
fail "failed to clone $url"
fi
[[ -n $ref ]] || ref=$(git -C "$cache_dir" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
local ts
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
write_sources '.sources += [{id:$id, url:$url, ref:$ref, trustedAt:$ts}]' \
--arg id "$name" --arg url "$url" --arg ref "$ref" --arg ts "$ts"
echo "Added source '$name'${ref:+ (ref: $ref)}."
echo "Browse its plugins with: omarchy plugin available"
}
# ---------------------------------------------------------------- list
source_list() {
require_command jq
local json=0
while (($# > 0)); do
case "$1" in
--json) json=1; shift ;;
-h | --help) usage; return 0 ;;
*) fail "unknown list option: $1" ;;
esac
done
if [[ ! -f $SOURCES_FILE ]]; then
if ((json)); then echo '{"version":1,"sources":[]}'; else echo "No plugin sources configured. Add one with: omarchy plugin source add <git-url>"; fi
return 0
fi
if ((json)); then
cat "$SOURCES_FILE"
return 0
fi
if [[ $(jq '.sources | length' "$SOURCES_FILE") -eq 0 ]]; then
echo "No plugin sources configured. Add one with: omarchy plugin source add <git-url>"
return 0
fi
jq -r '.sources[] | [.id, .url, (.ref // "")] | @tsv' "$SOURCES_FILE" |
while IFS=$'\t' read -r id url ref; do
local cache="missing"
[[ -d "$CACHE_ROOT/$id/.git" ]] && cache="cached"
printf '%s\t%s\t%s\t%s\n' "$id" "$url" "${ref:-?}" "$cache"
done |
awk -F '\t' '
BEGIN { printf "%-34s %-46s %-14s %s\n", "ID", "URL", "REF", "CACHE" }
{ printf "%-34s %-46s %-14s %s\n", $1, $2, $3, $4 }'
}
# ---------------------------------------------------------------- remove
source_remove() {
require_command jq
local id=""
ASSUME_YES=0
while (($# > 0)); do
case "$1" in
--yes | -y) ASSUME_YES=1; shift ;;
-h | --help) usage; return 0 ;;
-*) fail "unknown option: $1" ;;
*)
[[ -z $id ]] || fail "unexpected argument: $1"
id="$1"; shift ;;
esac
done
[[ -f $SOURCES_FILE ]] || fail "no plugin sources configured"
if [[ -z $id ]]; then
interactive || fail "a source id is required"
id=$(jq -r '.sources[].id' "$SOURCES_FILE" | gum choose --header="Remove which plugin source?") || fail "cancelled"
[[ -n $id ]] || fail "nothing selected"
fi
source_exists "$id" || fail "no such source: $id"
confirm "Remove source '$id'? (installed plugins are left in place)" || fail "aborted"
write_sources '.sources |= map(select(.id != $id))' --arg id "$id"
# Defense in depth: never rm -rf a cache path built from an id that could
# contain traversal, even though valid_source_id gates ids on the way in.
if valid_source_id "$id"; then
rm -rf "${CACHE_ROOT:?}/$id"
fi
echo "Removed source '$id'."
}
# ---------------------------------------------------------------- refresh
refresh_one() {
local id="$1"
valid_source_id "$id" || { echo "omarchy-plugin-source: invalid source id: $id" >&2; return 1; }
local url ref cache_dir
url=$(jq -r --arg id "$id" '.sources[] | select(.id == $id) | .url' "$SOURCES_FILE")
ref=$(jq -r --arg id "$id" '.sources[] | select(.id == $id) | .ref // ""' "$SOURCES_FILE")
cache_dir="$CACHE_ROOT/$id"
[[ -n $url ]] || { echo "omarchy-plugin-source: no such source: $id" >&2; return 1; }
if [[ ! -d "$cache_dir/.git" ]]; then
echo "Cache missing for '$id'; re-cloning ..."
rm -rf "$cache_dir"
mkdir -p "$CACHE_ROOT"
local clone_args=(--depth 1)
[[ -n $ref ]] && clone_args+=(--branch "$ref")
git clone "${clone_args[@]}" -- "$url" "$cache_dir" || return 1
return 0
fi
echo "Refreshing '$id' (${ref:-HEAD}) ..."
if ! git -C "$cache_dir" fetch --depth 1 origin "${ref:-HEAD}" >/dev/null 2>&1; then
echo "omarchy-plugin-source: fetch failed for '$id'" >&2
return 1
fi
git -C "$cache_dir" reset --hard FETCH_HEAD >/dev/null 2>&1 || return 1
}
source_refresh() {
require_command jq
require_command git
local id=""
while (($# > 0)); do
case "$1" in
-h | --help) usage; return 0 ;;
-*) fail "unknown option: $1" ;;
*)
[[ -z $id ]] || fail "unexpected argument: $1"
id="$1"; shift ;;
esac
done
[[ -f $SOURCES_FILE ]] || fail "no plugin sources configured"
if [[ -n $id ]]; then
refresh_one "$id"
return $?
fi
local rc=0
mapfile -t ids < <(jq -r '.sources[].id' "$SOURCES_FILE")
if ((${#ids[@]} == 0)); then
echo "No plugin sources configured."
return 0
fi
for sid in "${ids[@]}"; do
refresh_one "$sid" || rc=1
done
return $rc
}
usage() {
cat <<USAGE
Usage: omarchy plugin source <command> [args...]
add [<git-url>] [--ref <ref>] [--as <name>] [--yes]
Trust a git repo as a plugin source and clone it locally.
list [--json]
List trusted plugin sources and whether each is cached.
remove [<source-id>] [--yes]
Forget a source and drop its cache (installed plugins stay).
refresh [<source-id>]
Pull the latest clone for one source, or all sources.
Sources are stored in ~/.config/omarchy/plugins/sources.json and cloned
into ~/.cache/omarchy/plugin-sources/<source-id>/.
USAGE
}
command="${1:-list}"
[[ $# -gt 0 ]] && shift || true
case "$command" in
add) source_add "$@" ;;
list | ls) source_list "$@" ;;
remove | rm) source_remove "$@" ;;
refresh | update) source_refresh "$@" ;;
-h | --help | help) usage ;;
*) fail "unknown command: $command (use add, list, remove, or refresh)" ;;
esac
-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
+3 -2
View File
@@ -77,8 +77,9 @@ done < <(jq -c '.entryPoints | to_entries[] | .value' "$MANIFEST")
# Refuse any symlink anywhere inside the plugin folder. Symlinks could point a
# copied plugin back at arbitrary files on disk after it lands in the trusted
# plugins directory.
link=$(find "$PLUGIN_DIR" -type l -print -quit 2>/dev/null)
# plugins directory. The .git dir is skipped: installed plugins are git
# checkouts, and git's internals are never loaded by the shell.
link=$(find "$PLUGIN_DIR" -name .git -prune -o -type l -print -quit 2>/dev/null)
[[ -z $link ]] || fail "symlinks are not allowed inside a plugin folder: $link"
# The whole omarchy.* namespace plus every shipped first-party id is reserved.
+1 -1
View File
@@ -45,7 +45,7 @@ defaults must use the resync command.
Current generated theme state lives under
`~/.local/state/omarchy/current/`. Keep `~/.config/omarchy/` for files a user
may intentionally version in a dotfile manager, such as user themes, hooks,
shell layout, plugin sources, and themed template overrides.
shell layout, plugins, and themed template overrides.
## Build-time map (repo → installed paths)
+13 -15
View File
@@ -42,26 +42,24 @@ Full schema: [`shell/services/PluginRegistry.qml`](../shell/services/PluginRegis
## Installing a third-party plugin
Plugins come from **source repos** — a git repo where every top-level folder is
a plugin with its own `manifest.json`. Trust a repo, then install from it:
A plugin is a **git repo** with a `manifest.json` at its root. Adding one
clones it straight into `~/.config/omarchy/plugins/<id>/`; updating is a
fast-forward pull:
```bash
omarchy plugin source add https://github.com/owner/omarchy-plugins.git
omarchy plugin available # what your sources offer
omarchy plugin add some-widget # validate, copy, offer to enable
omarchy plugin update --all # shows a diff before applying
omarchy plugin remove some-widget
omarchy plugin add https://github.com/acme/omarchy-weather.git
omarchy plugin update --all # fetches, shows a diff, fast-forwards
omarchy plugin remove acme.weather
```
Plugins run as **unsandboxed code** inside `omarchy-shell`. Adding a source and
installing both warn you and let you review the manifest, the files, and (on
update) a diff before anything is copied or enabled. Commands prompt when run
bare in a terminal and run unattended when given arguments — add `--yes` to skip
every prompt (the path for scripts and agents).
Plugins run as **unsandboxed code** inside `omarchy-shell`. Adding warns you
before cloning, plugins land disabled so you can review the code before
`omarchy plugin enable`, and updates show a diff before touching anything.
Commands prompt when run bare in a terminal and run unattended when given
arguments — add `--yes` to skip every prompt (the path for scripts and agents).
Sources are recorded in `~/.config/omarchy/plugins/sources.json` and cloned into
`~/.cache/omarchy/plugin-sources/`. You can still install by hand: drop a plugin
into `~/.config/omarchy/plugins/<id>/`, run `omarchy plugin rescan`, then
You can still install by hand: drop a plugin into
`~/.config/omarchy/plugins/<id>/`, run `omarchy plugin rescan`, then
`omarchy plugin enable <id>` (bar widgets also need `omarchy bar add <id>`;
full bar replacements are selected with `omarchy bar use <id>`).
The lower-level IPC methods remain available through `omarchy-shell shell ...`.
+17 -20
View File
@@ -93,43 +93,40 @@ The full schema lives in `services/PluginRegistry.qml`.
## Installing a third-party plugin
Plugins are distributed as **source repos**: a git repo where every top-level
folder is one plugin with its own `manifest.json`. Trust a repo, then install
individual plugins from it. Everything lands in `~/.config/omarchy/plugins/<id>/`.
A plugin is a **git repo** with a `manifest.json` at its root. Adding one
clones it straight into `~/.config/omarchy/plugins/<id>/` (named by the
manifest id); updating is a fast-forward pull of that checkout.
```bash
omarchy plugin source add https://github.com/owner/omarchy-plugins.git
omarchy plugin available # list what your sources offer
omarchy plugin add some-widget # validate, copy, offer to enable
omarchy plugin update some-widget # shows a diff of changes first
omarchy plugin add https://github.com/acme/omarchy-weather.git
omarchy plugin update acme.weather # fetches, shows a diff, fast-forwards
omarchy plugin update --all
omarchy plugin remove some-widget
omarchy plugin remove acme.weather
```
> ⚠️ **Plugins run as unsandboxed code inside `omarchy-shell`.** Adding a source
> and installing both warn you and let you review the manifest, the files, and
> (on update) a diff of the changes before anything is copied or enabled. Only
> trust sources and plugins whose code you are willing to run.
> ⚠️ **Plugins run as unsandboxed code inside `omarchy-shell`.** Adding warns
> you before cloning, plugins land disabled so you can review the code before
> enabling, and updates show a diff of the changes before touching anything.
> Only add repos whose code you are willing to run.
Each command is **interactive** when run bare in a terminal (gum/fzf pickers,
Each command is **interactive** when run bare in a terminal (gum pickers,
confirmation, a diff to review) and fully **non-interactive** when given
arguments. Pass `--yes` to skip every prompt — this is the path for scripts and
AI agents:
```bash
omarchy plugin source add <url> --as acme --yes
omarchy plugin add acme-weather --from acme --enable --yes
omarchy plugin add https://github.com/acme/omarchy-weather.git --enable --yes
omarchy plugin update --all --yes
```
Sources live in `~/.config/omarchy/plugins/sources.json`; their clones are
cached under `~/.cache/omarchy/plugin-sources/`. The installer never runs
plugin code, install hooks, or sudo — it only copies files and toggles enabled
state over shell IPC.
The installer never runs plugin code, install hooks, or sudo — it only clones
files, validates the manifest, and toggles enabled state over shell IPC. Since
an installed plugin is a plain git checkout, anything beyond add/update
(pinning a ref, switching branches) is ordinary git in the plugin directory.
### Installing by hand
You can still drop a plugin in without a source:
You can still drop a plugin in without git:
1. Put it in `~/.config/omarchy/plugins/<plugin-id>/` with a `manifest.json`
plus the QML referenced from its `entryPoints`.