Files
arthur-os/bin/omarchy-plugin-add

315 lines
10 KiB
Bash
Executable File

#!/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 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 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")
if command -v gum >/dev/null 2>&1; then
pick=$(gum choose --header="Add which plugin?" <<<"$rows") || fail "cancelled"
elif command -v fzf >/dev/null 2>&1; then
pick=$(fzf --prompt "Add which plugin > " <<<"$rows") || fail "cancelled"
else
fail "a plugin-id is required (gum or fzf needed for an interactive picker)"
fi
[[ -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 && command -v gum >/dev/null 2>&1; 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