feat!: rename Claude plugin to planbridge@contextbridge; refresh plugins on update#52
Conversation
0e51d6e to
9494b1f
Compare
| @@ -0,0 +1,12 @@ | |||
| { | |||
There was a problem hiding this comment.
This moves from github.com/contextbridge/claude-plugin (see contextbridge/claude-plugin#1). That repo is now just a catalog of all ContextBridge claude plugins (right now, just PlanBridge, but could expand to other products in the future).
I'm doing this migration now, which will make #51 easier to implement in a single place.
| ]); | ||
| expect(pluginInstall).toHaveLength(1); | ||
| expect(pluginInstall[0]?.args).toEqual(['plugin', 'install', 'cli@contextbridge', '--scope', 'user']); | ||
| expect(pluginInstall[0]?.args).toEqual(['plugin', 'install', 'planbridge@contextbridge', '--scope', 'user']); |
There was a problem hiding this comment.
I'm renaming this to match up. If we don't feel strongly, we can leave it as cli - feedback welcome.
There was a problem hiding this comment.
Think planbridge will make more sense given the marketing site and product name
| expect(spawnCall?.args).toEqual(['install', 'claude', '--scope', 'user']); | ||
| }); | ||
|
|
||
| it('refreshes Claude at project scope when the new plugin is installed at project scope', async () => { |
There was a problem hiding this comment.
Things could get a little bit weird if the old named plugin is installed in various projects, but that didn't feel like something I wanted to work around.
| ]); | ||
| await runPluginCommand(ctx, binaryName, 'install', ['install', PLUGIN_ID, '--scope', scope]); | ||
| if (hasCurrentAtScope) { | ||
| await runPluginCommand(ctx, binaryName, 'update', ['update', PLUGIN_ID, '--scope', scope]); |
There was a problem hiding this comment.
Claude will update plugins on some cadence, but we want to force it to be up-to-date (e.g., when we introduce new harness features).
| { | ||
| "type": "json", | ||
| "path": "harnessIntegrations/claude/.claude-plugin/plugin.json", | ||
| "jsonpath": "$.version" |
There was a problem hiding this comment.
This is a handy feature that'll keep the claude plugin released in-step with the binary.
| "homepage": "https://plan.contextbridge.ai", | ||
| "repository": "https://github.com/contextbridge/planbridge", | ||
| "license": "MIT", | ||
| "keywords": ["contextbridge", "planbridge", "planning", "review", "hooks", "ExitPlanMode"] |
There was a problem hiding this comment.
Is ExitPlanMode in the keywords intentional? Just curious since it sticks out against the others.
There was a problem hiding this comment.
Ah, just copied this over verbatim from the other repo - you're right though, not very helpful. Let me update.
| ]); | ||
| expect(pluginInstall).toHaveLength(1); | ||
| expect(pluginInstall[0]?.args).toEqual(['plugin', 'install', 'cli@contextbridge', '--scope', 'user']); | ||
| expect(pluginInstall[0]?.args).toEqual(['plugin', 'install', 'planbridge@contextbridge', '--scope', 'user']); |
There was a problem hiding this comment.
Think planbridge will make more sense given the marketing site and product name
| commandRunner | ||
| .on(CLAUDE_BINARY, ['plugin', 'list', '--json']) | ||
| .resolves(pluginListResult([{ id: 'cli@contextbridge', scope: 'user' }])); | ||
| .resolves(pluginListResult([{ id: 'planbridge@contextbridge', scope: 'user' }])); |
There was a problem hiding this comment.
Seems like we should use the constant that's defined elsewhere in this PR across these tests.
| // the user never wired up; never blocks update success on a refresh failure. | ||
| async function refreshInstalledHarnesses(ctx: CliContext): Promise<void> { | ||
| const { commandRunner, logger } = ctx; | ||
| for (const installer of ALL_INSTALLERS) { |
There was a problem hiding this comment.
Probably just me being me, but I'd flatMap here (using empty array in the failure case) as it'd avoid the let binding + re-assignment.
There was a problem hiding this comment.
Sure - I'll take a more functional approach throughout.
| continue; | ||
| } | ||
|
|
||
| for (const scope of refreshScopes) { |
There was a problem hiding this comment.
Noting that this is sequential, assuming this is intentional since concurrent install commands might cause issues?
There was a problem hiding this comment.
Yep, it's intentional. claude plugin install mutates shared config under ~/.claude (and Codex hook installs touch ~/.codex/hooks.json), so parallel installs across scopes/harnesses would race on those files. Sequential is also fine performance-wise — the post-update refresh fan-out is small (one per managed scope per installer), and stdio: 'inherit' already streams progress to the user.
- drop the `ExitPlanMode` keyword from the Claude plugin manifest - export CLAUDE_PLUGIN_ID / CLAUDE_LEGACY_PLUGIN_ID / CLAUDE_MARKETPLACE_* from ClaudeInstaller and use them in tests instead of literals - split refreshInstalledHarnesses into helpers so the status-failure path returns [] rather than a let/reassign + continue
9494b1f to
bd01fce
Compare
## Summary Fixes #63. After `brew upgrade --cask cli@alpha` (or `cli`), the post-update harness refresh introduced in #52 fails silently with `ENOENT` because `process.execPath` was captured at process start and now points at the cellar dir brew already purged. The refresh exists to keep harness state in sync across binary changes (renames, hook contracts) — silent no-op defeats the entire feature for every brew-installed user. Resolve the binary via `commandRunner.which('contextbridge')` instead. The PATH symlink (`/opt/homebrew/bin/contextbridge` for brew, `~/.local/bin/contextbridge` for install-script users) survives the upgrade. If `which` returns null, log `error` and skip — refusing to fall back to the broken `execPath` keeps us from silently masking the very bug we're fixing. Also bumped refresh-failure logs from `warn` → `error` so Sentry surfaces these in production.
🤖 I have created a release *beep* *boop* --- ## [0.3.0](v0.2.0...v0.3.0) (2026-05-08) ### ⚠ BREAKING CHANGES * rename Claude plugin to planbridge@contextbridge; refresh plugins on update ([#52](#52)) ### Features * add automatic release changelog with release-please ([#21](#21)) ([45a1bf1](45a1bf1)) * rename Claude plugin to planbridge@contextbridge; refresh plugins on update ([#52](#52)) ([2794ae6](2794ae6)) * **ui:** add GitHub link to header help menu ([#19](#19)) ([c36289b](c36289b)) ### Bug Fixes * emit plan review analytics from shared runner ([#48](#48)) ([5c769ff](5c769ff)) * **plan:** pre-scan src for transitive deps to stop vitest reload flake ([3b3cc0c](3b3cc0c)), closes [#12](#12) * **plan:** stop vitest reload flake from lazy zod optimization ([#15](#15)) ([3b3cc0c](3b3cc0c)) * refresh Claude marketplace cache during install ([#62](#62)) ([58be09a](58be09a)) * resolve contextbridge via PATH for post-update refresh ([#64](#64)) ([f658af4](f658af4)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --------- Co-authored-by: contextbridge-pr-automation[bot] <259134118+contextbridge-pr-automation[bot]@users.noreply.github.com> Co-authored-by: Ben Limmer <ben@benlimmer.com>
Summary
Move PlanBridge's Claude Code plugin assets (
plugin.json,hooks.json) fromcontextbridge/claude-plugininto this repo atharnessIntegrations/claude/, rename the plugin id fromcli@contextbridgetoplanbridge@contextbridge, and couple binary + plugin updates socontextbridge updatere-runs install for harnesses that already have PlanBridge wired up. Closes #49.Review focus
runUpdatepost-swap refresh inpackages/cli/src/commands/update.tswalksALL_INSTALLERS, skips harnesses withstatus.managed.length === 0, and spawns<process.execPath> install <harness-id>per harness with state. Refresh failure logs a warning and never blocks update success. Spawningprocess.execPath(not the oldrunUpdate's in-process logic) is deliberate — the child runs the new binary's installer code, which knows about the rename and updates/migrates Claude plugin state automatically.ClaudeInstaller.status()now surfaces bothplanbridge@contextbridgeandcli@contextbridgeentries inmanaged[], butinstalledstays gated on the new id only. This is what makes the refresh filter naturally migrate legacy users without a separate "do I have legacy state?" abstraction.runInstall/runUninstalllegacy cleanup at the target scope only — seeClaudeInstaller.ts. Tests cover the cross-scope edge case (legacy at project, install at user → leaves legacy alone). Existingplanbridge@contextbridgeinstalls now useclaude plugin updatesocontextbridge updatepulls the latest plugin version.release-please-config.jsonextra-fileskeepsharnessIntegrations/claude/.claude-plugin/plugin.json'sversionin sync with binary releases.Sequencing: depends on contextbridge/claude-plugin#1 merging first so the new plugin id resolves in the marketplace catalog. Then this PR; then the release-please release PR.
Commits
b2807dd— package the Claude Code plugin integration assets in this repoa382919— rename the Claude plugin toplanbridge@contextbridge9494b1f— refresh managed harness integrations aftercontextbridge update