From 0fc5725ec481bddce6b47b1d182104c4fd445e02 Mon Sep 17 00:00:00 2001 From: Julian lechner Date: Thu, 24 Sep 2026 16:24:49 +0200 Subject: [PATCH] fix(menu): parse Bash plugin inventory safely --- TODO.md | 2 +- scripts/lib/plugins.sh | 28 ++++++++++++++++++++++------ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/TODO.md b/TODO.md index f233d2d..ed5413c 100644 --- a/TODO.md +++ b/TODO.md @@ -21,7 +21,7 @@ - [ ] Reconcile resources whose ownership record remains but whose installation was removed manually. `ensure_cli()` and `ensure_mcporter()` return early from a ledger record without checking the actual CLI or server; normal sync cannot restore the missing resource. (`scripts/lib/managed-extensions.py:342`, `scripts/lib/managed-extensions.py:395`) - [ ] Update owned MCPorter itself on `--update`. `ensure_cli()` returns whenever its ledger record exists, so a selected MCPorter package never receives the requested update. (`scripts/lib/managed-extensions.py:342`) - [x] Keep terminal newlines in Bash-managed files that need substitution. Command substitution strips trailing newlines before Bash hashes and writes the content, while PowerShell retains them; switching installers causes needless file changes and backups. (`scripts/lib/manifest.sh:70`, `scripts/lib/manifest.ps1:75`) -- [ ] Parse the Bash plugin inventory as JSON and stop when listing fails. `plugin_installed()` searches for one exact spacing pattern and suppresses CLI errors; a minified response or listing failure marks installed plugins unchecked, allowing an update to remove owned plugins. PowerShell parses JSON and fails on a CLI error. (`scripts/lib/plugins.sh:4`, `scripts/lib/plugins.ps1:36`) +- [x] Parse the Bash plugin inventory as JSON and stop when listing fails. `plugin_installed()` searches for one exact spacing pattern and suppresses CLI errors; a minified response or listing failure marks installed plugins unchecked, allowing an update to remove owned plugins. PowerShell parses JSON and fails on a CLI error. (`scripts/lib/plugins.sh:4`, `scripts/lib/plugins.ps1:36`) - [x] Serialize Bash manifest entries without shell word splitting. `write_managed_manifest()` loops over command substitution; a managed filename containing spaces becomes multiple invalid entries and can no longer be reconciled correctly. (`scripts/lib/manifest.sh:47`) - [ ] Allow uninstall to reconcile owned extensions when file manifests are missing. Both uninstall commands stop with “Not installed” based only on destination manifests, even if `extensions.json` still owns plugins, skills, or MCP servers. (`scripts/commands/uninstall.ps1:21`, `scripts/commands/uninstall.sh:34`) diff --git a/scripts/lib/plugins.sh b/scripts/lib/plugins.sh index 73b9a32..16ec9ed 100755 --- a/scripts/lib/plugins.sh +++ b/scripts/lib/plugins.sh @@ -3,11 +3,22 @@ set -euo pipefail plugin_installed() { local client=$1 plugin=$2 - if [ "$client" = claude ]; then - claude plugin list --json 2>/dev/null | grep -Fq "\"id\": \"$plugin\"" - else - codex plugin list --json 2>/dev/null | grep -Fq "\"pluginId\": \"$plugin\"" - fi + local inventory + inventory=$("$client" plugin list --json) || return 2 + printf '%s' "$inventory" | python3 -c ' +import json, sys +try: + client, plugin = sys.argv[1:] + data = json.load(sys.stdin) + if client == "codex": + data = data["installed"] + if not isinstance(data, list) or any(not isinstance(item, dict) for item in data): + raise ValueError("Invalid plugin inventory") + sys.exit(0 if any(item.get("id" if client == "claude" else "pluginId") == plugin for item in data) else 1) +except (ValueError, KeyError, TypeError) as error: + print("Invalid plugin inventory: " + str(error), file=sys.stderr) + sys.exit(2) +' "$client" "$plugin" } run_plugin_command() { @@ -121,7 +132,12 @@ select_configured_plugins() { local skill_name skill_name=$(plugin_field "$root" "$name" codex_skill) if [ -d "$HOME/.agents/skills/$skill_name" ] || [ -d "$HOME/.codex/skills/$skill_name" ]; then checked+=(true); else checked+=(false); fi - elif plugin_installed "$reference_client" "$selector"; then checked+=(true); else checked+=(false); fi + elif plugin_installed "$reference_client" "$selector"; then checked+=(true) + else + local status=$? + [ "$status" -eq 1 ] || return "$status" + checked+=(false) + fi done read_plugin_toggle_selection toggle_names checked