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:
co-authored by
Claude Fable 5
parent
60e0a2a0cd
commit
798d6af8b0
+356
-37
@@ -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
|
||||
;;
|
||||
|
||||
@@ -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
|
||||
@@ -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>"
|
||||
@@ -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 "$@"
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user