Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/installer/src/__tests__/harnesses.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,27 @@ describe("claude harness", () => {
expect(system.run).not.toHaveBeenCalledWith(expect.stringContaining("marketplace remove"));
});

it("removes the copy installed from our own marketplace", async () => {
const system = fakeSystem({
run: (cmd) => (isList(cmd) ? claudeList(["sentry@sentry-plugin-marketplace"]) : ok),
});
const removed = await createClaude(system).cleanup!();

expect(system.run).toHaveBeenCalledWith(
"claude plugin uninstall sentry@sentry-plugin-marketplace",
);
expect(removed).toContain("sentry@sentry-plugin-marketplace");
});

it("leaves cleanup a no-op when only the official plugin is installed", async () => {
const system = fakeSystem({
run: (cmd) => (isList(cmd) ? claudeList(["sentry@claude-plugins-official"]) : ok),
});

expect(await createClaude(system).cleanup!()).toBeNull();
expect(system.run).not.toHaveBeenCalledWith(expect.stringContaining("uninstall"));
});

it("surfaces stderr when install fails", async () => {
const harness = createClaude(
fakeSystem({ run: () => ({ ok: false, stderr: "boom", message: "exit 1" }) }),
Expand Down
22 changes: 19 additions & 3 deletions packages/installer/src/harnesses/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ const INSTALL_COMMAND = `claude plugin install ${PLUGIN_ID}`;
const UPDATE_COMMAND = `claude plugin update ${PLUGIN_ID}`;
const UNINSTALL_COMMAND = `claude plugin uninstall ${PLUGIN_ID}`;

// Our plugin reaches Claude two ways: Anthropic's official catalog above, which is
// what the installer uses, and our own catalog under the marketplace name it
// declares. Both resolve to the same repository, so a machine carrying both runs
// two plugins serving the same skills.
const OUR_MARKETPLACE = "sentry-plugin-marketplace";
const OUR_PLUGIN_ID = `sentry@${OUR_MARKETPLACE}`;

// `claude plugin list --json` emits an array of installed plugins. We only care
// about the marketplace-qualified id of each entry.
interface ClaudePlugin {
Expand All @@ -21,9 +28,9 @@ interface ClaudeMarketplace {
name?: string;
}

async function isSentryInstalled(system: SystemDeps): Promise<boolean> {
async function hasPlugin(system: SystemDeps, pluginId: string): Promise<boolean> {
const plugins = await runJson<ClaudePlugin[]>(system, "claude plugin list --json");
return Array.isArray(plugins) && plugins.some((plugin) => plugin.id === PLUGIN_ID);
return Array.isArray(plugins) && plugins.some((plugin) => plugin.id === pluginId);
}

async function isMarketplaceRegistered(system: SystemDeps): Promise<boolean> {
Expand Down Expand Up @@ -52,10 +59,19 @@ export function createClaude(system: SystemDeps): Harness {

detect: async () => detectOnPath(system, "claude"),

isInstalled: async () => isSentryInstalled(system),
isInstalled: async () => hasPlugin(system, PLUGIN_ID),

canInstall: async () => ({ ok: true }),

cleanup: async (output) => {
if (!(await hasPlugin(system, OUR_PLUGIN_ID))) {
return null;
}

await runCommand(system, `claude plugin uninstall ${OUR_PLUGIN_ID}`, output);
return `Removed conflicting plugin ${OUR_PLUGIN_ID}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cleanup can leave Sentry disabled

High Severity

When the official plugin is installed but disabled, cleanup removes the enabled marketplace copy and then the installer selects update. Activation state is ignored, so a successful run can leave no active Sentry plugin.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c0f5a3e. Configure here.

},

install: async (output): Promise<InstallOutcome> => {
await ensureMarketplace(system, output);
await runCommand(system, INSTALL_COMMAND, output);
Expand Down
Loading