Skip to content

fix(install): close the GitHub-marketplace broken-install path#133

Merged
yyiilluu merged 3 commits into
mainfrom
fix/marketplace-install-guard
Jul 13, 2026
Merged

fix(install): close the GitHub-marketplace broken-install path#133
yyiilluu merged 3 commits into
mainfrom
fix/marketplace-install-guard

Conversation

@yyiilluu

@yyiilluu yyiilluu commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Problem

Installing via claude plugin marketplace add ReflexioAI/claude-smart produces a plugin whose backend can never start. plugin/vendor/reflexio is generated at pack time and gitignored, so the GitHub tree carries no Reflexio runtime. Users only find out later, from a backend log repeating bundled Reflexio import preflight failed.

What changed

Close the door at the front. .claude-plugin/marketplace.json is now generated at pack time from a committed template (via npm's prepack) instead of being committed. That manifest is what makes a directory installable as a Claude Code marketplace — with it absent from git, claude plugin marketplace add ReflexioAI/claude-smart fails immediately with Marketplace file not found instead of installing a plugin that cannot run. The supported path is untouched: npx claude-smart install registers the npm package root, which ships both the manifest and the vendor bundle. Version can't drift either — the generator stamps it from package.json, so make bump has nothing to bump.

Repair anyone already stuck, scoped to the cache dir matching the package's own version. The first cut compared against the highest-versioned cache dir; since each plugin version vendors a different Reflexio, their vendor payloads legitimately differ, so any downgrade install was diagnosed as "stale", force-reinstalled, and then failed outright (exit 1) when the repair couldn't converge. Reproduced, then fixed.

Preflight the bundled runtime before stopping or replacing a backend — in both the SessionStart hook and /claude-smart:restart. The check sits exactly on the boundary between accepting a healthy backend and tearing one down. This matters: a broken bundle in one plugin root no longer reports "backend is not running" over a live, compatible backend served by another root (a case the code explicitly supports).

Point users at the command that can actually fix it. The failure message said to run /claude-smart:restart, which preflights the same missing bundle and refuses — a dead end. It now says npx claude-smart update.

Fail Setup loudly when a host plugin cache (*/plugins/cache/*) has no vendor bundle. That can only mean a GitHub-marketplace install; smart-install.sh previously skipped silently. A source checkout legitimately has no bundle and is left alone.

Verification

Full end-to-end against a real make package tarball, installed into an isolated HOME on harness ports (production backend on 8071 untouched throughout):

Scenario Result
Clean install from tarball Backend boots healthy, serving from the vendored runtime
GitHub tree → marketplace add Refused: Marketplace file not found
npm package → marketplace add Succeeds
Bundle deleted, backend still serving Plain {"continue":true} — no false "backend is not running", backend survives
Bundle deleted, no backend Reports failure with the npx claude-smart update remedy
/claude-smart:restart, bundle deleted Aborts rc=1; same pid still serving 200 — nothing stopped
npx claude-smart update Restores the bundle; backend healthy; preflight passes

Tests: 202 passing across test_install_scripts.py, test_cli_restart.py, test_codex_support.py (7 new: manifest stays untracked, real pack ships a version-correct manifest, downgrade install doesn't touch a newer cache, dev-checkout exemption, Setup cache guard, multi-root acceptance, restart-preserves-services). Biome clean; bash -n clean.

Note for release notes

Anyone who previously added the GitHub marketplace will find claude plugin update stops working once this ships. That's intended — it pushes them onto npx claude-smart update, the only thing that can repair them — but it's a visible break.

Summary by CodeRabbit

  • Bug Fixes

    • Install and update flows now detect and repair incomplete or stale Claude Code plugin caches automatically (including missing bundled runtime payloads).
    • Backend startup and restart now run a preflight check for the required bundled runtime and won’t disrupt existing services if it’s missing.
    • Clear, actionable repair guidance is shown when the bundled runtime isn’t present.
  • Documentation

    • Updated installation, update/uninstall, and troubleshooting instructions to reflect the new npx claude-smart install flow and repair steps.
  • Packaging

    • Marketplace metadata is generated automatically during package creation and the generated manifest is no longer committed.

Installing claude-smart via `claude plugin marketplace add
ReflexioAI/claude-smart` produced a plugin whose backend could never start:
plugin/vendor/reflexio is generated at pack time and gitignored, so the
GitHub tree carries no Reflexio runtime. The failure surfaced late, as a
repeating `bundled Reflexio import preflight failed` in the backend log.

Prevent it at the front door, and make the runtime checks safe:

- Generate .claude-plugin/marketplace.json at pack time from a committed
  template (npm prepack) instead of committing it. The manifest is what makes
  a directory installable as a marketplace; absent from git,
  `claude plugin marketplace add ReflexioAI/claude-smart` now fails with
  "Marketplace file not found" rather than installing a plugin that cannot
  run. The npm path is unaffected: `npx claude-smart install` registers the
  package root, which ships both the manifest and the vendor bundle.
- Repair an existing broken cache from the npm package, scoped to the cache
  directory matching the package's own version. Comparing against the
  highest-versioned cache dir reported a spurious mismatch on any downgrade
  install (each version vendors a different Reflexio), forcing a pointless
  reinstall and then failing the install outright.
- Preflight the bundled runtime before stopping or replacing a backend, in
  both the SessionStart hook and `/claude-smart:restart`. The check sits on
  the boundary between accepting a healthy backend and tearing one down, so a
  broken bundle in this plugin root no longer reports "backend is not running"
  over a live, compatible backend served by another root.
- Point the failure message at `npx claude-smart update`, which can repair a
  missing bundle; `/claude-smart:restart` cannot, since it preflights the same
  bundle.
- Fail Setup loudly when a host plugin cache (*/plugins/cache/*) has no vendor
  bundle. That can only mean a GitHub-marketplace install; a source checkout
  legitimately has none and is left alone.

Verified end to end against a packed tarball in an isolated HOME: clean
install boots the backend from the vendored runtime; deleting the bundle makes
the hook accept a live backend without crying wolf, makes restart refuse
without stopping it, and `npx claude-smart update` restores the bundle and the
backend.
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR generates the Claude marketplace manifest during package creation, repairs incomplete cached plugin vendors, adds bundled Reflexio backend preflight checks, updates restart behavior, and documents npm-based installation and recovery flows with corresponding tests.

Changes

Marketplace packaging and runtime repair

Layer / File(s) Summary
Marketplace manifest packaging
.claude-plugin/*, scripts/generate-marketplace.js, package.json, Makefile, .gitignore, tests/integration/*, .github/workflows/*, tests/test_install_scripts.py
Marketplace metadata is generated from package.json during prepack, excluded from version bumps and git tracking, generated for integration setup, and verified in npm tarballs.
Claude cache vendor repair
bin/claude-smart.js, plugin/scripts/smart-install.sh, tests/test_install_scripts.py
Installation compares vendored Reflexio payloads for the shipped version, refreshes stale Claude cache entries, and distinguishes cache installs from source checkouts.
Backend runtime preflight
plugin/scripts/backend-service.sh, tests/test_install_scripts.py
Backend startup and preflight prepare the import context, validate bundled Reflexio imports, provide repair guidance, and avoid replacing working services after failures.
Restart preflight validation
plugin/src/claude_smart/cli.py, tests/test_cli_restart.py
Backend restart performs preflight before stopping services, with tests covering sequencing and failure preservation.
Installation and recovery guidance
DEVELOPER.md, README.md, TROUBLESHOOTING.md, plugin/README.md
Documentation describes npm-based installation, generated marketplace output, cache repair, update behavior, and backend recovery instructions.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: wang-henry4, wenchanghan

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Installer
  participant ClaudeCache
  participant BackendService
  participant ReflexioRuntime
  User->>Installer: npx claude-smart install or update
  Installer->>ClaudeCache: validate shipped-version vendor payload
  Installer->>ClaudeCache: refresh incomplete cache when needed
  Installer->>BackendService: bootstrap or restart backend
  BackendService->>ReflexioRuntime: run bundled import preflight
  ReflexioRuntime-->>BackendService: preflight result
  BackendService-->>User: start backend or report repair guidance
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: fixing the GitHub Marketplace install path that could produce broken installs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/marketplace-install-guard

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bin/claude-smart.js`:
- Line 2286: Update the missing-vendor recovery around
refreshClaudeCodeCacheIfVendorMissing in bin/claude-smart.js to distinguish
cache-refresh failures from ordinary bootstrap failures and recommend npx
claude-smart update. In tests/test_install_scripts.py, update the expected
recovery command and corresponding smart-install.sh message to npx claude-smart
update.
- Around line 829-845: Update claudeCodePackageVersion to read the version from
package.json first, matching the generated marketplace version used for Claude’s
cache key. Retain the existing plugin.json and pyproject.toml lookups only as
explicit fallback sources for development fixtures, preserving their current
order and behavior after the package version lookup.

In `@plugin/scripts/backend-service.sh`:
- Around line 821-825: Update the preflight branch to skip only when
claude_smart_reflexio_url_is_remote is true; remove the
CLAUDE_SMART_BACKEND_AUTOSTART check from this condition so explicit preflight
still validates the bundle when autostart is disabled.

In `@tests/test_install_scripts.py`:
- Line 2347: Update the marker_text assertion in the relevant install-script
test and the corresponding smart-install.sh guidance to use the supported
missing-bundle recovery command, “npx claude-smart update,” instead of “npx
claude-smart install.”
🪄 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 Plus

Run ID: b53abf12-7cc7-4162-9339-cb64eb169052

📥 Commits

Reviewing files that changed from the base of the PR and between 800343f and 53a5488.

📒 Files selected for processing (15)
  • .claude-plugin/marketplace.template.json
  • .gitignore
  • DEVELOPER.md
  • Makefile
  • README.md
  • TROUBLESHOOTING.md
  • bin/claude-smart.js
  • package.json
  • plugin/README.md
  • plugin/scripts/backend-service.sh
  • plugin/scripts/smart-install.sh
  • plugin/src/claude_smart/cli.py
  • scripts/generate-marketplace.js
  • tests/test_cli_restart.py
  • tests/test_install_scripts.py
💤 Files with no reviewable changes (1)
  • .claude-plugin/marketplace.template.json

Comment thread bin/claude-smart.js
Comment on lines +829 to +845
function claudeCodePackageVersion(sourceRoot = join(PACKAGE_ROOT, "plugin")) {
try {
const manifest = JSON.parse(
readFileSync(join(sourceRoot, ".claude-plugin", "plugin.json"), "utf8"),
);
if (typeof manifest.version === "string" && manifest.version) return manifest.version;
} catch {
// Fall through to pyproject.toml.
}
try {
const match = readFileSync(join(sourceRoot, "pyproject.toml"), "utf8").match(
/^version\s*=\s*"([^"]+)"/m,
);
if (match) return match[1];
} catch {
// No readable version.
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use package.json as the Claude cache-key source.

The generated marketplace version—and therefore Claude’s cache directory—is sourced from package.json, but this helper uses plugin-local metadata. If those versions drift, Line 1021 inspects the wrong directory and silently skips the required repair. Read the package version first, retaining these sources only as explicit fallbacks for development fixtures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bin/claude-smart.js` around lines 829 - 845, Update claudeCodePackageVersion
to read the version from package.json first, matching the generated marketplace
version used for Claude’s cache key. Retain the existing plugin.json and
pyproject.toml lookups only as explicit fallback sources for development
fixtures, preserving their current order and behavior after the package version
lookup.

Comment thread bin/claude-smart.js
}

try {
await refreshClaudeCodeCacheIfVendorMissing();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use npx claude-smart update consistently for missing-bundle recovery.

The affected paths currently direct users toward commands that cannot consistently restore the packaged vendor bundle.

  • bin/claude-smart.js#L2286-L2286: distinguish cache-refresh failures from ordinary bootstrap failures and recommend npx claude-smart update.
  • tests/test_install_scripts.py#L2347-L2347: expect npx claude-smart update and update the corresponding smart-install.sh message.
📍 Affects 2 files
  • bin/claude-smart.js#L2286-L2286 (this comment)
  • tests/test_install_scripts.py#L2347-L2347
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bin/claude-smart.js` at line 2286, Update the missing-vendor recovery around
refreshClaudeCodeCacheIfVendorMissing in bin/claude-smart.js to distinguish
cache-refresh failures from ordinary bootstrap failures and recommend npx
claude-smart update. In tests/test_install_scripts.py, update the expected
recovery command and corresponding smart-install.sh message to npx claude-smart
update.

Comment on lines +821 to +825
preflight)
if claude_smart_reflexio_url_is_remote \
|| [ "${CLAUDE_SMART_BACKEND_AUTOSTART:-1}" = "0" ]; then
exit 0
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Do not bypass explicit preflight when autostart is disabled.

CLAUDE_SMART_BACKEND_AUTOSTART=0 makes preflight report success without checking the bundle. Because restart uses this command before stopping services, it can cross that safety boundary with an unusable runtime. Only skip for remote Reflexio; callers that do not require preflight should avoid invoking it.

Proposed fix
-    if claude_smart_reflexio_url_is_remote \
-      || [ "${CLAUDE_SMART_BACKEND_AUTOSTART:-1}" = "0" ]; then
+    if claude_smart_reflexio_url_is_remote; then
       exit 0
     fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
preflight)
if claude_smart_reflexio_url_is_remote \
|| [ "${CLAUDE_SMART_BACKEND_AUTOSTART:-1}" = "0" ]; then
exit 0
fi
preflight)
if claude_smart_reflexio_url_is_remote; then
exit 0
fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugin/scripts/backend-service.sh` around lines 821 - 825, Update the
preflight branch to skip only when claude_smart_reflexio_url_is_remote is true;
remove the CLAUDE_SMART_BACKEND_AUTOSTART check from this condition so explicit
preflight still validates the bundle when autostart is disabled.

assert marker.exists()
marker_text = marker.read_text()
assert "bundled Reflexio runtime is missing" in marker_text
assert "npx claude-smart install" in marker_text

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the supported cache-repair command.

This test locks Setup guidance to npx claude-smart install, while the supported missing-bundle recovery command elsewhere in this PR is npx claude-smart update. Update both the assertion and smart-install.sh output.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_install_scripts.py` at line 2347, Update the marker_text assertion
in the relevant install-script test and the corresponding smart-install.sh
guidance to use the supported missing-bundle recovery command, “npx claude-smart
update,” instead of “npx claude-smart install.”

yyiilluu added 2 commits July 13, 2026 12:53
The harness installs the checkout itself as a marketplace, so removing the
committed .claude-plugin/marketplace.json broke it at `claude plugin
marketplace add` with "Marketplace file not found".

The manifest is a pack-time artifact, exactly like plugin/vendor/reflexio,
which the harness already generates in setup. Generate the manifest there too,
alongside prepare_vendored_reflexio, and add a guard test asserting it happens
before the marketplace is registered. Corrects the DEVELOPER.md claim that
local development never registers the checkout as a marketplace — it does.
plugin/vendor/reflexio is a pack-time artifact and is gitignored, so a fresh
CI checkout has none. Three install-flow tests depended on it implicitly and
passed locally only because a dev checkout has it lying around:

- Two copied REPO_ROOT/plugin/vendor into a fake cache and blew up with
  FileNotFoundError when it was absent.
- The tarball smoke test packed a vendorless tarball, which installs a plugin
  whose backend can never start (backend-service.sh requires the bundle) —
  a state Setup now correctly rejects. It was only "passing" before because
  smart-install.sh silently tolerated it.

Vendor the locked commit in the pytest job so those tests exercise what users
actually receive, and make the tests themselves resilient on an unvendored
checkout: drop a copytree that the version-scoped cache repair made
unnecessary, mirror the source payload only when the checkout has one, and
skip the tarball smoke with a clear reason rather than asserting a state that
cannot run.

Verified both ways: 202 passed + 1 skipped with no vendor bundle (fresh
clone), 203 passed with it (CI).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
.github/workflows/integration.yml (1)

63-64: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Inline JSON parsing lacks clear error messages.

If reflexio.lock.json is missing or lacks the repo/commit key, the inline python3 -c will fail with a raw KeyError traceback. The integration harness (tests/integration/integration.sh:195-237) uses read_reflexio_lock_field which raises a clear missing field: message. Consider adding a guard or reusing the same pattern for consistency and debuggability in CI logs.

♻️ Suggested improvement
           repo=$(python3 -c "import json,sys; d=json.load(open('reflexio.lock.json')); v=d.get('repo'); print(v) if v else sys.exit('missing field: repo')")
           commit=$(python3 -c "import json,sys; d=json.load(open('reflexio.lock.json')); v=d.get('commit'); print(v) if v else sys.exit('missing field: commit')")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/integration.yml around lines 63 - 64, Update the repo and
commit extraction commands in the workflow to validate reflexio.lock.json and
report clear missing-field errors, matching the read_reflexio_lock_field
behavior used by the integration harness. Ensure missing repo or commit keys
produce descriptive messages instead of raw KeyError tracebacks, while
preserving the existing variable assignments.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In @.github/workflows/integration.yml:
- Around line 63-64: Update the repo and commit extraction commands in the
workflow to validate reflexio.lock.json and report clear missing-field errors,
matching the read_reflexio_lock_field behavior used by the integration harness.
Ensure missing repo or commit keys produce descriptive messages instead of raw
KeyError tracebacks, while preserving the existing variable assignments.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 68b5e744-2a6b-4552-91e8-50ee695ab771

📥 Commits

Reviewing files that changed from the base of the PR and between 07852f4 and 07216fd.

📒 Files selected for processing (2)
  • .github/workflows/integration.yml
  • tests/test_install_scripts.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_install_scripts.py

@yyiilluu
yyiilluu merged commit 0a40ef7 into main Jul 13, 2026
16 of 17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant