fix: install Claude plugin via marketplace + installer hardening#206
Conversation
📝 WalkthroughWalkthroughBash wrapper exports EVOLVE_REPO/EVOLVE_VERSION and runs embedded Python (now unbuffered). Python lazily downloads/extracts source when needed, introduces FileOps and per-platform installer classes, changes Claude to marketplace add+install, and updates dry-run and dispatch behavior. Changes
Sequence Diagram(s)sequenceDiagram
participant Bash as install.sh (wrapper)
participant Py as embedded Python
participant CurlTar as Downloader (`curl`/`tar`)
participant ClaudeCLI as claude CLI
Bash->>Py: invoke installer (env: EVOLVE_REPO/EVOLVE_VERSION)
Py->>Py: parse args (DRY_RUN), choose platform installer
Py->>Py: _ensure_source_dir()
alt no local source
Py->>CurlTar: download/extract evolve tarball to temp dir
CurlTar-->>Py: extracted SOURCE_DIR (cleanup via atexit)
end
Py->>Py: instantiate ClaudeInstaller / BobInstaller / CodexInstaller
alt Claude install path
Py->>ClaudeCLI: claude plugin marketplace add <local-or-EVOLVE_REPO>
ClaudeCLI-->>Py: ack
Py->>ClaudeCLI: claude plugin install evolve-lite@evolve-marketplace
ClaudeCLI-->>Py: install result
else Bob/Codex install path
Py->>Py: write files via FileOps (or DryRunFileOps)
end
Py-->>Bash: print outcomes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@platform-integrations/install.sh`:
- Around line 745-747: The code uses EVOLVE_REPO when computing
marketplace_source but EVOLVE_REPO is not defined, causing a NameError when
has_local_marketplace is False; define EVOLVE_REPO in the Python scope before
that expression (e.g., a constant string URL or environment-derived value) so
marketplace_source = str(marketplace_dir) if has_local_marketplace else
EVOLVE_REPO can safely fall back to the remote repo; update or add the
EVOLVE_REPO declaration near where target_dir/marketplace_dir is resolved
(referencing marketplace_dir, has_local_marketplace, and marketplace_source) and
ensure any imports or env reads used to populate EVOLVE_REPO run earlier.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3ad1798c-08da-4540-850e-35760758ef11
📒 Files selected for processing (1)
platform-integrations/install.sh
|
@illeatmyhat can you address the comments? |
|
yeah im trying a feature in claude to see if it can autofix it |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
platform-integrations/install.sh (1)
744-745: Info log is now slightly misleading.
plugin_sourceis no longer used to install the plugin — installation is driven by the marketplace entry — but the log still saysInstalling Claude plugin from {plugin_source}. Consider dropping the variable or clarifying the message (e.g., "Preparing Claude plugin install (marketplace flow)") so users aren't led to believe the install is sourced from that local path.✏️ Proposed tweak
-def install_claude(source_dir, target_dir): - plugin_source = Path(source_dir) / "platform-integrations" / "claude" / "plugins" / "evolve-lite" - info(f"Installing Claude plugin from {plugin_source}") +def install_claude(source_dir, target_dir): + info("Installing Claude plugin via marketplace")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@platform-integrations/install.sh` around lines 744 - 745, The info log is misleading because plugin_source (the variable assigned to Path(source_dir) / "platform-integrations" / "claude" / "plugins" / "evolve-lite") is no longer used for installation; update the code to either remove the unused plugin_source variable or change the info(...) call to a clarifying message such as "Preparing Claude plugin install (marketplace flow)" so it no longer implies installation is coming from that local path; locate the assignment to plugin_source and the info(...) invocation and apply one of these two fixes consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@platform-integrations/install.sh`:
- Around line 744-745: The info log is misleading because plugin_source (the
variable assigned to Path(source_dir) / "platform-integrations" / "claude" /
"plugins" / "evolve-lite") is no longer used for installation; update the code
to either remove the unused plugin_source variable or change the info(...) call
to a clarifying message such as "Preparing Claude plugin install (marketplace
flow)" so it no longer implies installation is coming from that local path;
locate the assignment to plugin_source and the info(...) invocation and apply
one of these two fixes consistently.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b5e6cec3-e28f-4669-84a8-b183da64cbc6
📒 Files selected for processing (1)
platform-integrations/install.sh
The `claude plugin install <path>` command requires the plugin to exist in a configured marketplace. This updates install_claude to first register the marketplace (local if .claude-plugin/marketplace.json exists, otherwise AgentToolkit/altk-evolve on GitHub), then install evolve-lite from it. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e fallback Export EVOLVE_REPO from bash and read it via os.environ in the Python heredoc so marketplace_source can fall back to the remote repo without a NameError. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7894e95 to
bfcff05
Compare
Claude installs via the marketplace CLI and never use SOURCE_DIR, so downloading the repo tarball upfront was unnecessary. Move the download logic into a Python-side _ensure_source_dir() called lazily at the top of install_bob and install_codex, so a Claude-only remote install triggers no network fetch at all. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
platform-integrations/install.sh (1)
165-168: Avoidshell=Truewith interpolated URL.
urlis built from theEVOLVE_REPO/EVOLVE_VERSIONenv vars and then spliced into a shell string. These are normally user-controlled, so this isn't an exploitable issue, but it is fragile (spaces, quotes, or a stray;in the env would break or misbehave). A small refactor using a pipe between twosubprocess.Popencalls orcurl | tarviashell=Falsewith explicit argv would be more robust.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@platform-integrations/install.sh` around lines 165 - 168, The subprocess.run call that executes f"curl -fsSL {url} | tar -xz -C {_tmpdir_download} --strip-components=1" should be rewritten to avoid shell=True and string interpolation of url; instead spawn curl and tar as separate processes and pipe curl's stdout into tar (use subprocess.Popen for curl with stdout=PIPE and subprocess.run or Popen for tar reading from that pipe), or call both commands with shell=False as argument lists (e.g., ["curl","-fsSL", url] piped into ["tar","-xz","-C", _tmpdir_download,"--strip-components=1"]); update the code around subprocess.run, url, and _tmpdir_download to use the piped processes and remove shell=True to make execution robust against spaces or malicious characters in EVOLVE_REPO/EVOLVE_VERSION.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@platform-integrations/install.sh`:
- Around line 160-163: The current logic treats EVOLVE_VERSION="latest" as an
alias for main, which fetches the branch instead of the newest release; update
the handling of EVOLVE_VERSION in the branch/tag selection (the conditional that
sets url using EVOLVE_VERSION and EVOLVE_REPO) so that when EVOLVE_VERSION ==
"latest" you resolve the latest published release via the GitHub Releases API
(GET https://api.github.com/repos/{EVOLVE_REPO}/releases/latest), extract the
release tag_name and use that in the tag URL, with a safe fallback to the main
branch or a clear error if the API call fails; ensure EVOLVE_VERSION and
EVOLVE_REPO are referenced exactly as in the diff and preserve the existing
branch/tag URL formats.
- Around line 770-779: The local-marketplace detection in install_claude
currently checks Path(target_dir) instead of the Evolve source checkout; update
the probe to use source_dir so has_local_marketplace =
(Path(source_dir).resolve() / ".claude-plugin" / "marketplace.json").is_file().
Adjust marketplace_dir/marketplace_source logic to use the resolved source_dir
when local marketplace exists (still falling back to EVOLVE_REPO otherwise) and
keep the existing info() messages and variable names (install_claude,
has_local_marketplace, marketplace_source) intact.
---
Nitpick comments:
In `@platform-integrations/install.sh`:
- Around line 165-168: The subprocess.run call that executes f"curl -fsSL {url}
| tar -xz -C {_tmpdir_download} --strip-components=1" should be rewritten to
avoid shell=True and string interpolation of url; instead spawn curl and tar as
separate processes and pipe curl's stdout into tar (use subprocess.Popen for
curl with stdout=PIPE and subprocess.run or Popen for tar reading from that
pipe), or call both commands with shell=False as argument lists (e.g.,
["curl","-fsSL", url] piped into ["tar","-xz","-C",
_tmpdir_download,"--strip-components=1"]); update the code around
subprocess.run, url, and _tmpdir_download to use the piped processes and remove
shell=True to make execution robust against spaces or malicious characters in
EVOLVE_REPO/EVOLVE_VERSION.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b4493cec-7e67-4bbd-93ac-6d549c0f6b4a
📒 Files selected for processing (1)
platform-integrations/install.sh
- Introduce FileOps class holding all write primitives (atomic_write_json, atomic_write_text, copy_tree, remove_dir, remove_file, run_subprocess) and higher-level JSON/YAML helpers as methods that compose them - DryRunFileOps subclass overrides only the primitives with dryrun() logs, so every helper and installer automatically gets dry-run behaviour for free — no more if DRY_RUN: sprinkled through installer logic - BobInstaller, ClaudeInstaller, CodexInstaller each receive an ops instance; install/uninstall/status are proper methods, Codex-specific hook/marketplace helpers are private static methods on CodexInstaller - Dispatch instantiates DryRunFileOps or FileOps once based on DRY_RUN, set a single time in main() via getattr(args, "dry_run", False) - PLATFORM_CLASSES dict replaces the per-platform if/elif chain - sys.exit() inside installers replaced with RuntimeError so the dispatcher can catch, report all failures, and exit once at the end Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
capture_output=True captured the spinner/progress \r sequences, causing garbled output when we printed them with an indentation prefix. Streaming directly lets the terminal handle the cursor movement cleanly. returncode is still checked; stdout/stderr scraping removed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Guard source dir is_dir() checks with `not self.ops.is_dry_run` so a remote dry-run (where _ensure_source_dir skips the download) doesn't raise RuntimeError on missing local paths — DryRunFileOps.copy_tree already handles missing source gracefully with a (source not found) note - Add -u to the python3 invocation so stdout is unbuffered; previously when piped, block-buffered stdout caused error() messages (stderr, unbuffered) to appear before info()/success() output in the terminal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
merge_yaml_custom_mode reads the source YAML file before writing, so it must be overridden in DryRunFileOps rather than relying on the atomic_write_text no-op. Similarly, Bob full mode reads mcp.json to extract the server config before calling upsert_json_key — skip the read and pass a placeholder in dry-run mode. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…emote tests - install.sh: iterate source skills/ dir instead of hardcoding each skill name; uninstall globs evolve-lite:* from target instead of hardcoding removals; status also derived from installed globs - Tests now derive expected skills from the source tree rather than naming them, so adding a skill requires no test or script edits - Add remote_install_runner fixture to conftest: copies install.sh to an isolated dir with no platform-integrations/ sibling, simulating curl | bash - Add test_dry_run.py covering local and remote dry-run for all platforms Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
platform-integrations/install.sh (2)
162-163:⚠️ Potential issue | 🟡 Minor
EVOLVE_VERSION=latestresolves tomain, not the newest release tag.Users typically interpret "latest" as the newest published release; silently tracking
mainwill surprise anyone pinningEVOLVE_VERSION=latestfor stability. Resolve via the GitHub Releases API, or drop the alias and documentmainexplicitly.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@platform-integrations/install.sh` around lines 162 - 163, The code treats EVOLVE_VERSION=="latest" as an alias for "main" which returns the branch instead of the newest release; update the logic around EVOLVE_VERSION, EVOLVE_REPO and the url construction so that when EVOLVE_VERSION == "latest" you query the GitHub Releases API for the repository's latest release tag (e.g., GET /repos/{EVOLVE_REPO}/releases/latest), use that release tag to build the download URL instead of using "main", and fall back to the current branch URL only if the API call fails; alternatively, remove the "latest" alias and document that EVOLVE_VERSION must be a branch or a tag, ensuring the url variable is set from the resolved tag when constructing the download URL.
584-586:⚠️ Potential issue | 🟠 MajorLocal marketplace probe still uses
target_dir, not the Evolve source tree.
marketplace.jsonlives in the Evolve source checkout, not the user's project. ProbingPath(target_dir)means the local path is only picked up whencwd == SOURCE_DIR; running with--dir /some/other/projectfrom a local checkout will silently fall back to GitHub even though a fresher local source is available. Also,ClaudeInstallerno longer receivessource_dir— consider threadingSOURCE_DIR(the resolved source) through and probing there.🐛 Proposed fix
- def install(self, target_dir): + def install(self, target_dir): info("Installing Claude plugin via marketplace") - - marketplace_dir = Path(target_dir).resolve() - has_local_marketplace = (marketplace_dir / ".claude-plugin" / "marketplace.json").is_file() - marketplace_source = str(marketplace_dir) if has_local_marketplace else EVOLVE_REPO + source_dir = SOURCE_DIR + has_local_marketplace = bool(source_dir) and ( + Path(source_dir) / ".claude-plugin" / "marketplace.json" + ).is_file() + marketplace_source = ( + str(Path(source_dir).resolve()) if has_local_marketplace else EVOLVE_REPO + )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@platform-integrations/install.sh` around lines 584 - 586, The local marketplace probe currently constructs marketplace_dir from target_dir; instead use the resolved Evolve source tree (SOURCE_DIR) and thread that through ClaudeInstaller (or add a source_dir parameter) so the probe checks (Path(SOURCE_DIR).resolve() / ".claude-plugin" / "marketplace.json").is_file(); if found set marketplace_source to the resolved SOURCE_DIR string, otherwise fall back to EVOLVE_REPO. Update the code that instantiates ClaudeInstaller to accept and pass the resolved SOURCE_DIR and replace references to marketplace_dir/has_local_marketplace/marketplace_source to use the new source_dir-based path resolution to ensure local check works regardless of cwd.
🧹 Nitpick comments (2)
tests/platform_integrations/test_preservation.py (1)
14-28: Consider moving_assert_all_bob_*helpers toconftest.py.These two helpers are duplicated verbatim in
test_idempotency.py. Promoting them toconftest.py(or extendingFileAssertions/BobFixtures) avoids drift if the source-layout contract changes.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/platform_integrations/test_preservation.py` around lines 14 - 28, Move the duplicate helper functions _assert_all_bob_skills_installed and _assert_all_bob_commands_installed out of tests/platform_integrations/test_preservation.py into the shared test helper (conftest.py) or attach them as methods on an existing fixture/object (e.g., FileAssertions or BobFixtures) so both test_preservation.py and test_idempotency.py can import/use the same implementations; update both test modules to import the helpers from conftest (or call the new FileAssertions/BobFixtures methods) and remove the duplicated definitions from each test file to avoid drift.platform-integrations/install.sh (1)
601-603: Consider aborting whenmarketplace addfails.If
claude plugin marketplace addreturns non-zero (e.g., unreachable GitHub source, invalid path), the subsequentplugin install evolve-lite@evolve-marketplacewill almost certainly also fail — but only the second failure prints manual-fallback instructions. Fail fast here (print manual commands and return) so users see a single actionable message instead of two consecutive warnings.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@platform-integrations/install.sh` around lines 601 - 603, The marketplace add step currently only warns on failure; change it to fail fast by detecting non-zero result from self.ops.run_subprocess([claude, "plugin", "marketplace", "add", marketplace_source]) and then print the same manual-fallback instructions that are printed for the later install failure and return/exit immediately (non-zero) so the script does not continue to attempt self.ops.run_subprocess to install "plugin install evolve-lite@evolve-marketplace". Ensure you reference the failing call (self.ops.run_subprocess for "plugin marketplace add"), use the existing warn/log method to emit the manual commands, and stop further execution (return or exit) when result.returncode != 0.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@platform-integrations/install.sh`:
- Around line 162-172: The current download uses shell=True with an f-string
including EVOLVE_REPO and EVOLVE_VERSION which allows shell injection; change to
a safe two-step subprocess call that avoids shell=True: run curl with
subprocess.run([...], stdout=subprocess.PIPE) and pipe its stdout into a
separate subprocess.run([...]) invocation of tar (passing _tmpdir_download and
--strip-components=1 as list args), check return codes or use check=True and
raise on failure (replace the current result variable logic). Additionally
validate EVOLVE_REPO and EVOLVE_VERSION with a strict regex (e.g.,
alphanumerics, dot, underscore, hyphen, slash) before building the URL to
further harden inputs.
- Around line 619-632: The uninstall path uses an unqualified plugin name
causing mismatch with install: update the CLAUDE_PLUGIN constant so it is
marketplace-qualified (e.g., "evolve-lite@evolve-marketplace") and ensure the
uninstall flow in uninstall(...) continues to pass that qualified CLAUDE_PLUGIN
to self.ops.run_subprocess([claude, "plugin", "uninstall", CLAUDE_PLUGIN]) so
the same identifier used by install(...) is used for uninstalling; verify any
references to CLAUDE_PLUGIN (and the install function) now match the qualified
value.
---
Duplicate comments:
In `@platform-integrations/install.sh`:
- Around line 162-163: The code treats EVOLVE_VERSION=="latest" as an alias for
"main" which returns the branch instead of the newest release; update the logic
around EVOLVE_VERSION, EVOLVE_REPO and the url construction so that when
EVOLVE_VERSION == "latest" you query the GitHub Releases API for the
repository's latest release tag (e.g., GET
/repos/{EVOLVE_REPO}/releases/latest), use that release tag to build the
download URL instead of using "main", and fall back to the current branch URL
only if the API call fails; alternatively, remove the "latest" alias and
document that EVOLVE_VERSION must be a branch or a tag, ensuring the url
variable is set from the resolved tag when constructing the download URL.
- Around line 584-586: The local marketplace probe currently constructs
marketplace_dir from target_dir; instead use the resolved Evolve source tree
(SOURCE_DIR) and thread that through ClaudeInstaller (or add a source_dir
parameter) so the probe checks (Path(SOURCE_DIR).resolve() / ".claude-plugin" /
"marketplace.json").is_file(); if found set marketplace_source to the resolved
SOURCE_DIR string, otherwise fall back to EVOLVE_REPO. Update the code that
instantiates ClaudeInstaller to accept and pass the resolved SOURCE_DIR and
replace references to marketplace_dir/has_local_marketplace/marketplace_source
to use the new source_dir-based path resolution to ensure local check works
regardless of cwd.
---
Nitpick comments:
In `@platform-integrations/install.sh`:
- Around line 601-603: The marketplace add step currently only warns on failure;
change it to fail fast by detecting non-zero result from
self.ops.run_subprocess([claude, "plugin", "marketplace", "add",
marketplace_source]) and then print the same manual-fallback instructions that
are printed for the later install failure and return/exit immediately (non-zero)
so the script does not continue to attempt self.ops.run_subprocess to install
"plugin install evolve-lite@evolve-marketplace". Ensure you reference the
failing call (self.ops.run_subprocess for "plugin marketplace add"), use the
existing warn/log method to emit the manual commands, and stop further execution
(return or exit) when result.returncode != 0.
In `@tests/platform_integrations/test_preservation.py`:
- Around line 14-28: Move the duplicate helper functions
_assert_all_bob_skills_installed and _assert_all_bob_commands_installed out of
tests/platform_integrations/test_preservation.py into the shared test helper
(conftest.py) or attach them as methods on an existing fixture/object (e.g.,
FileAssertions or BobFixtures) so both test_preservation.py and
test_idempotency.py can import/use the same implementations; update both test
modules to import the helpers from conftest (or call the new
FileAssertions/BobFixtures methods) and remove the duplicated definitions from
each test file to avoid drift.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 27d015e0-7c81-4f8b-bc44-cf3c9a0a18fc
📒 Files selected for processing (5)
platform-integrations/install.shtests/platform_integrations/conftest.pytests/platform_integrations/test_dry_run.pytests/platform_integrations/test_idempotency.pytests/platform_integrations/test_preservation.py
- Replace shell=True pipe in _ensure_source_dir with Popen so URL values can't break execution if they contain spaces or special characters - Fix marketplace local-source detection to check SOURCE_DIR (the repo root) rather than target_dir (the user's project dir), so local dev runs correctly find .claude-plugin/marketplace.json and fall back to EVOLVE_REPO remotely Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fail fast in ClaudeInstaller when 'marketplace add' fails instead of continuing to run 'plugin install' and getting a second confusing failure - Move _assert_all_bob_skills/commands_installed into FileAssertions in conftest.py so they're shared rather than duplicated across test files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ote context - Verify fallback manual instructions include both required commands - Verify local context uses local marketplace source - Verify remote context (no source tree) falls back to GitHub repo as source - Verify missing claude CLI exits 0 rather than failing the install Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…mands When the claude CLI is not on PATH, the previous fallback showed 'claude plugin marketplace add ...' instructions — which the user can't run without claude. Now we tell them to install it from claude.ai/download and re-run the script. Remove tests that asserted which marketplace source appeared in that message, since the source detail is irrelevant when the CLI isn't installed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Original fix:
claude plugin install <path>fails because the CLI requires the plugin to exist in a configured marketplaceclaude plugin marketplace add(local.claude-plugin/marketplace.jsonif present, otherwiseAgentToolkit/altk-evolveon GitHub), thenplugin install evolve-lite@evolve-marketplacemarketplace addfails rather than running a doomedplugin installclaudeCLI is not found, tell the user to install it fromclaude.ai/downloadand re-run — instead of showingclaude plugin ...commands they can't run yetInstaller refactor:
BobInstaller,ClaudeInstaller,CodexInstallerclasses backed byFileOps/DryRunFileOps— dry-run behavior is inherited automatically, noif DRY_RUNsprinkled throughout_ensure_source_dir()downloads on demand and only when Bob/Codex actually need source files. Claude installs need no download at all.skills/directory dynamically; uninstall globsevolve-lite:*from the target — adding a new skill requires no script changesmarketplace_dirdetection to probeSOURCE_DIR(the repo root) instead oftarget_dir(the user's project)shell=Truepipe for the tarball download withPopento avoid fragility on paths with spacesTests:
remote_install_runnerfixture: copiesinstall.shto an isolated dir with noplatform-integrations/sibling, simulatingcurl | bashtest_dry_run.pycovering local and remote dry-run for all platforms — verifies no files are written and exit code is 0 even with no local source treetest_claude.pycovering CLI-absent fallback behaviour_assert_all_bob_skills_installed/_assert_all_bob_commands_installedconsolidated intoFileAssertionsinconftest.pyTest plan
install --platform all --dry-runfrom repo root (local source)install --platform all --dry-runfrom isolated dir (remote simulation, no source tree)install --platform claudefrom repo root (local marketplace path used)install --platform claudefrom a remote context (GitHub marketplace used) — covered byTestDryRunRemoteclaudeCLI is absent, user is told to install it and re-run — covered bytest_cli_absent_prompts_download🤖 Generated with Claude Code