Files
omarchycn/bin/omarchy-plugin
T
09b955dc75 Manage plugins from Setup > Plugins (#6420)
* Give built-in plugins an honest on/off state

Every built-in reported itself enabled no matter what. A bar widget said
"enabled" while sitting nowhere near the bar, and disabling a built-in service
silently did nothing, because enabled meant "listed in plugins[]" and a
built-in never is. Nothing surfaced that, since the only caller listing plugins
was the CLI.

For a widget, on and off is its place in the bar, so listPlugins reports layout
membership -- what enable/disable actually toggles. For everything else built
in, loading by default is the right behaviour to keep, so switching one off is
recorded the other way round, in disabledPlugins[]. shell.json still carries
only the deviation from the defaults: the key is dropped the moment nothing is
switched off, leaving a config that never disabled anything byte-identical.

isEnabled still answers a separate question -- whether the component loads at
all -- and deliberately does not follow a widget out of the bar. omarchy.menu
is both a widget and the menu itself, so tying the two together would let
taking its button off the bar lock the menu out of the shell, with no way back
that isn't the CLI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Manage plugins from Setup > Plugins

Plugins were CLI-only. Setup > Plugins now offers Enable, Disable, Add, and
Remove, each list living in the menu itself so picking a row acts on it.

Enable and Disable cover the built-ins as well as anything installed -- the bar
widgets you can put in the bar, the services and overlays you can switch off.
Remove is limited to plugins the user installed, since a built-in has no
checkout to delete, and stays hidden until there is one. Whole-bar
replacements are left out; those are chosen under Style.

Enabling a bar widget asks for a section first, because enabling alone drops it
on the right and the only way to move it was a follow-up bar plugin move. The
CLI asks the same question after its own add, so both paths place a widget the
same way. Add and Remove run in a terminal: one needs a git URL and shows the
trust warning before cloning, the other deletes a checkout and prints where it
backed it up.

Providers grew two hooks for this. placementFor turns a row into a submenu
instead of an action, and volatile re-runs the enumeration when its submenu is
entered -- picking from these lists is what changes them, and rows a provider
no longer returns now drop out instead of lingering forever.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Trim the plugin menu after review

Menu.qml carried its own shellQuote while already importing Util and calling
Util.shellQuote a few lines up; two copies of the same escaping is one place
for a future fix to miss. isDisabled walked the array by hand to compare values
it writes itself, and dropDisabled was an eight-line helper with one caller.

Two bugs came out of the same pass. A whole-bar replacement belongs under Style
rather than these lists, but the exclusion sat in the shared row builder, so a
third-party bar could be installed and never removed -- Remove would show an
empty list under a guard that said something was there. The exclusion now sits
on the two lists that mean it.

Rows are keyed by id, and distinct plugin ids can slugify alike: acme.foo,
acme_foo and acme-foo all give acme-foo. The merge keeps the first row per id,
so the rest simply vanished from the list with nothing to say why. Row ids are
now made distinct before merging.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Pick a plugin the way we pick a theme

Setup > Plugins listed plugins as menu rows, which needed three providers, a
placement submenu, a volatile-refresh hook and a row-swap in the merge. Only
Font and Apps are built that way. Theme, Background, Unlock, Timezone and
Keybindings all pipe a list into omarchy-menu-select instead, which is one
action string and a small script -- so that is what these use now.

The trade is search: a plugin name is no longer findable from the root prompt.
Neither is a theme name or a timezone, and Enable Plugin still is, so the loss
sits where the rest of the menu already puts it.

Two pieces of the row machinery stay, because they are worth having for the
lists that remain. A volatile provider re-runs when its submenu is entered, so
a font installed since the shell started now shows up without restarting it,
and rows a provider stops returning drop out. Row ids are still made distinct
before merging: Fira Code and Fira-Code both slug to fira-code, and a repeated
id was silently dropped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Let a picked option carry an icon

Moving the plugin lists onto omarchy-menu-select cost them their glyphs: the
select mode has always hardcoded an empty icon, which is why Timezone and
Keybindings have none either. An option may now lead with one, as
"<glyph><TAB><label>". The menu shows the glyph, filters on the label, and
hands the label back, so a caller never strips a glyph off its own selection
and a list of plain strings behaves exactly as before.

The plugin picker uses it for the puzzle glyph on each plugin and the align
glyphs on the sections, which also regain the capitals they lost when the
section names were passed through raw.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Switch bars by enabling one

A bar option was kept out of Enable and Disable on the grounds that picking
which bar to run belongs under Style -- but nothing under Style ever offered
it, so an installed bar could be added and removed and never actually put to
use. The menu was guarding a door to a room that was never built.

Enabling one is the switch. setEnabled already assigns bar.id for a bar
option, so a bar has always replaced the one before it; only the picker's
filter stood in the way. Dropping it costs nothing else, because enabled for a
bar option means active: the bar in use is the one row absent from Enable,
every other installed bar is one pick away, and the built-in is just another
entry, so going back to it is enabling Bar.

Disable keeps the exclusion. That is the one verb a bar cannot answer -- there
is no off, only a successor -- and offering it would have listed the built-in
bar on a stock system, where turning it off deletes a bar.id that was never
set and nothing happens.

A bar carries the bar glyph rather than the puzzle one, so a row that replaces
the whole bar does not read like one more widget to switch on, and enable now
says "Now using X as the bar" instead of "Enabled X", which understated a
whole-bar swap in both the enable and the freshly-added path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Refuse a plugin that declares a kind it cannot load

A kind is a promise to supply something to load, and the shell reads that
something from a fixed key: entryPoints.bar to draw a bar, entryPoints.menu to
open a menu. Nothing checked the promise. A manifest could claim kinds ["bar"]
with no bar entry point, pass validation, install, and enable -- and then the
bar would fall back to the built-in and the widget would be skipped, leaving a
plugin that does nothing, explained only by a console.warn nobody reads.

Our own plugins have been held to this table by plugins-test.sh all along.
This holds third-party ones to the same table, at add and update time, where
there is still someone to tell.

A kind outside the table is left alone rather than guessed at, so a shell that
learns a new kind does not need this list updated first. The cost is that a
misspelled kind still installs quietly.

omarchy-plugin-validate had no tests; it has some now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Act on the plugin whose row was picked

The picker showed a name and then looked that name up again across every
plugin, filtered set or not, taking the first match. Two plugins can share a
name: cloning one keeps the name it was cloned from, so the documented
`omarchy plugin clone omarchy.clock local.clock` leaves two plugins called
Clock. Enable listed the clone -- the built-in was already enabled, so only the
clone was eligible -- and then enabled omarchy.clock, moving the built-in
widget instead. Remove listed the clone and tried to delete a built-in that has
no checkout to delete.

A row now carries its id alongside its label, and the id is read back off the
row that was picked instead of being derived from the name a second time. Where
a name is not unique among the rows on offer, the label carries the id too, so
two rows that would both say Clock can be told apart at all -- which they could
not before, whichever one the pick resolved to.

The verb prompt only ever sees the first two fields, so the menu shows what it
always did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Never ask a bar where to sit in the bar

A manifest may declare both bar and bar-widget, and validation accepts it. The
picker saw bar-widget, asked for a section, and passed it to enable. setEnabled
takes bar as the dominant kind: it writes bar.id and returns, adding nothing to
any layout, so the move that followed had no widget to find and failed -- after
the bar had already been switched. A partial success with an error on the way
out.

Bar wins ahead of bar-widget now, in the picker and in the placement prompt
`plugin add --enable` asks, so a bar is enabled without a placement it cannot
use. The CLI refuses a placement on a bar outright, before the bar is switched
rather than after, since `omarchy plugin enable <bar> --section left` could
reach the same half-applied state without going through either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Only replacement, no off

* Add default placement for bar widgets

* Simplify plugin menu actions

* Document plugin placement behavior

* Allow dropping widgets in empty bar space

* Treat plugin dependencies as runtime invariants

* Reject duplicate plugin ids on add

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 18:11:32 -04:00

555 lines
16 KiB
Bash
Executable File

#!/bin/bash
# omarchy:summary=Manage Omarchy shell plugins and bar widgets
# omarchy:group=plugin
# omarchy:args=<list|rescan|enable|disable|add|update|remove|clone|edit|validate> [...]
# 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
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...]
Manage plugins:
list [--json] List discovered shell plugins
rescan Rescan ~/.config/omarchy/plugins
enable <id> [placement] Enable a plugin (a bar replaces the one in use)
disable <id> Disable a plugin
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
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 widgets are placed in the layout with 'omarchy bar plugin'.
Examples:
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 enable acme.weather --section right
USAGE
}
fail() {
echo "omarchy-plugin: $*" >&2
exit 1
}
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 != *..* ]]
}
is_bar_option() {
omarchy-plugin-catalog | jq -e --arg id "$1" 'any(.[]; .id == $id and (.kinds | index("bar")))' >/dev/null
}
# Only one bar runs at a time, so enabling one replaces the one before it. Say
# that rather than "Enabled", which reads like one more thing switched on next
# to everything already there.
enabled_message() {
local id="$1"
if is_bar_option "$id"; then
echo "Now using $id as the bar"
else
echo "Enabled $id"
fi
}
# A bar widget lands on the right when it is enabled, which is rarely where it
# belongs. Ask once, here, rather than leaving a follow-up 'omarchy bar plugin
# move' as the only way to place it.
place_bar_widget() {
local id="$1" section default_section
interactive || return 0
(( ASSUME_YES )) && return 0
# A plugin that is also a bar has no place in the layout to ask about.
jq -e '(.kinds // []) | (index("bar") | not) and (index("bar-widget") != null)' \
"$PLUGINS_DIR/$id/manifest.json" >/dev/null 2>&1 || return 0
default_section=$(jq -r '.barWidget.defaultSection // "center"' "$PLUGINS_DIR/$id/manifest.json")
section=$(printf '%s\n' left center right |
gum choose --header="Place $id in which bar section?" --selected "$default_section") || return 0
[[ -n $section ]] || return 0
omarchy-bar-plugin move "$id" --section "$section" >/dev/null && echo "Placed $id in the $section section"
}
installed_plugin_ids() {
find "$PLUGINS_DIR" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) ! -name '.*' -printf '%f\n' 2>/dev/null | sort
}
plugin_id_manifest() {
omarchy-plugin-catalog | jq -r --arg id "$1" '
map(select(.id == $id))[0].manifestPath // empty
'
}
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
}
print_plugins() {
local json="false"
while (( $# > 0 )); do
case "$1" in
--json)
json="true"
;;
-h | --help)
usage
exit 0
;;
*)
fail "unknown list option: $1"
;;
esac
shift
done
local plugins
plugins=$(omarchy-shell shell listPlugins) || fail "could not list plugins; is omarchy-shell running?"
if [[ $json == "true" ]]; then
printf '%s\n' "$plugins"
return
fi
jq -r '
sort_by(.id)[] |
[ .id,
(if .enabled then "enabled" else "disabled" end),
(if .firstParty then "first-party" else "third-party" end),
((.kinds // []) | join(",")),
(.name // "")
] | @tsv
' <<<"$plugins" | awk -F '\t' '
BEGIN { printf "%-32s %-9s %-11s %-18s %s\n", "ID", "STATE", "SOURCE", "KINDS", "NAME" }
{ printf "%-32s %-9s %-11s %-18s %s\n", $1, $2, $3, $4, $5 }
'
}
rescan_plugins() {
(( $# == 0 )) || fail "rescan does not take arguments"
omarchy-shell shell rescanPlugins || fail "could not rescan plugins; is omarchy-shell running?"
echo "Plugins rescanned"
}
plugin_enabled() {
local enabled="$1"
local id="${2:-}"
[[ -n $id ]] || fail "plugin id is required"
shift 2
if [[ $enabled != "true" ]] && (( $# > 0 )); then
fail "disable does not take placement options"
fi
# Enabling a bar writes bar.id and touches no layout, so a placement could
# only be applied to a widget that is not there. Refuse before the bar is
# switched, rather than switching it and failing on the move afterwards.
if [[ $enabled == "true" ]] && (( $# > 0 )) && is_bar_option "$id"; then
fail "'$id' is a bar; it replaces the bar in use rather than taking a place in one"
fi
omarchy-shell shell rescanPlugins >/dev/null 2>&1 || true
if [[ $enabled == "true" ]]; then
wait_for_plugin_discovery "$id" || fail "plugin '$id' was not discovered; is omarchy-shell running?"
fi
local result
result=$(omarchy-shell shell setPluginEnabled "$id" "$enabled") || fail "could not update plugin; is omarchy-shell running?"
[[ $result == "ok" ]] || fail "plugin '$id' is not known; run: omarchy plugin rescan"
if [[ $enabled == "true" ]] && (( $# > 0 )); then
omarchy-bar-plugin move "$id" "$@"
echo "Enabled and moved $id"
elif [[ $enabled == "true" ]]; then
enabled_message "$id"
else
echo "Disabled $id"
fi
}
# ---------------------------------------------------------------- add
plugin_add() {
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 existing_manifest
id=$(jq -r '.id' "$stage/manifest.json")
existing_manifest=$(plugin_id_manifest "$id") || {
rm -rf "$stage"
fail "could not inspect installed plugin ids"
}
if [[ -n $existing_manifest ]]; then
rm -rf "$stage"
fail "plugin id '$id' is already used by $existing_manifest"
fi
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
enabled_message "$id"
place_bar_widget "$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 omarchy-cmd-present delta; 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() {
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"
}
command="${1:-}"
case "$command" in
list | ls)
shift
print_plugins "$@"
;;
rescan)
shift
rescan_plugins "$@"
;;
enable)
shift
plugin_enabled true "$@"
;;
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 "$@"
;;
validate)
shift
exec omarchy-plugin-validate "$@"
;;
-h | --help | help | "")
usage
;;
*)
fail "unknown command: $command"
;;
esac