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>
98 lines
4.2 KiB
Bash
Executable File
98 lines
4.2 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# omarchy:summary=Validate a plugin folder against the Omarchy plugin manifest schema
|
|
# omarchy:group=plugin
|
|
# omarchy:args=<plugin-folder>
|
|
# omarchy:examples=omarchy plugin validate ./my-plugin
|
|
|
|
# Mirrors the checks in shell/services/PluginRegistry.qml so the CLI refuses to
|
|
# install anything the running shell would silently reject (or worse, load
|
|
# unsafely). The shell never trusts manifests blindly; neither do we.
|
|
|
|
set -o pipefail
|
|
|
|
fail() {
|
|
echo "omarchy-plugin-validate: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
if [[ ${1:-} == -h || ${1:-} == --help ]]; then
|
|
cat <<USAGE
|
|
Usage: omarchy plugin validate <plugin-folder>
|
|
|
|
Checks a plugin folder's manifest.json against the schema the shell enforces:
|
|
schemaVersion, required fields, safe relative entry points that exist, no
|
|
symlinks, and an id that is not reserved. Exits 0 if valid — handy for plugin
|
|
authors before publishing.
|
|
USAGE
|
|
exit 0
|
|
fi
|
|
|
|
command -v jq >/dev/null 2>&1 || fail "jq is required"
|
|
|
|
PLUGIN_DIR="${1:-}"
|
|
[[ -n $PLUGIN_DIR && -d $PLUGIN_DIR ]] || fail "plugin folder not found: ${PLUGIN_DIR:-<none>}"
|
|
|
|
MANIFEST="$PLUGIN_DIR/manifest.json"
|
|
[[ -f $MANIFEST ]] || fail "missing manifest.json in $PLUGIN_DIR"
|
|
jq -e . "$MANIFEST" >/dev/null 2>&1 || fail "manifest.json is not valid JSON: $MANIFEST"
|
|
|
|
# schemaVersion must be exactly the JSON number 1 (the only version the registry
|
|
# understands). jq's == is type-aware, so the string "1" is correctly rejected
|
|
# just as the QML `schemaVersion !== 1` check rejects it.
|
|
jq -e '.schemaVersion == 1' "$MANIFEST" >/dev/null 2>&1 \
|
|
|| fail "unsupported or missing schemaVersion (expected 1) in $MANIFEST"
|
|
|
|
for field in id name version kinds entryPoints; do
|
|
jq -e --arg f "$field" 'has($f)' "$MANIFEST" >/dev/null 2>&1 \
|
|
|| fail "manifest missing required field '$field'"
|
|
done
|
|
|
|
ID=$(jq -r '.id // ""' "$MANIFEST")
|
|
[[ -n $ID ]] || fail "manifest 'id' is empty"
|
|
[[ $ID =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || fail "invalid plugin id '$ID'"
|
|
[[ $ID != *"/"* && $ID != *".."* ]] || fail "invalid plugin id '$ID'"
|
|
[[ $ID != omarchy.* ]] || fail "plugin id '$ID' uses the reserved omarchy.* namespace"
|
|
|
|
# kinds must be a non-empty array.
|
|
jq -e '(.kinds | type) == "array" and (.kinds | length) > 0' "$MANIFEST" >/dev/null 2>&1 \
|
|
|| fail "'kinds' must be a non-empty array"
|
|
|
|
# entryPoints must be an object and every value a safe relative path that exists.
|
|
jq -e '(.entryPoints | type) == "object"' "$MANIFEST" >/dev/null 2>&1 \
|
|
|| fail "'entryPoints' must be an object"
|
|
|
|
# Read each entry point as a JSON-encoded string (one per line), then decode it,
|
|
# so a value that itself contains a newline stays one literal path instead of
|
|
# being split into fragments that each pass the checks.
|
|
while IFS= read -r ep_json; do
|
|
[[ -n $ep_json ]] || continue
|
|
ep=$(jq -r '.' <<<"$ep_json")
|
|
[[ -n $ep ]] || fail "entry point path is empty"
|
|
[[ $ep != *$'\n'* ]] || fail "entry point may not contain a newline"
|
|
[[ $ep != /* ]] || fail "entry point must be a relative path: '$ep'"
|
|
[[ $ep != *".."* ]] || fail "entry point may not contain '..': '$ep'"
|
|
[[ -f "$PLUGIN_DIR/$ep" ]] || fail "entry point file not found: '$ep'"
|
|
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. 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.
|
|
# A third-party plugin claiming one of those ids would be rejected by the shell
|
|
# and could shadow built-in behaviour, so refuse it here too.
|
|
FIRST_PARTY_DIR="${OMARCHY_PATH:-$HOME/.local/share/omarchy}/shell/plugins"
|
|
if [[ -d $FIRST_PARTY_DIR ]]; then
|
|
while IFS= read -r fp_manifest; do
|
|
[[ -f $fp_manifest ]] || continue
|
|
fp_id=$(jq -r '.id // empty' "$fp_manifest" 2>/dev/null || true)
|
|
[[ $fp_id == "$ID" ]] && fail "plugin id '$ID' collides with a first-party Omarchy plugin"
|
|
done < <(find "$FIRST_PARTY_DIR" -type f \( -name manifest.json -o -name '*.manifest.json' \) 2>/dev/null)
|
|
fi
|
|
|
|
exit 0
|