can1357/oh-my-pi · error · Error

Plugin "${pluginId}" is installed in both user and project s

Error message

Plugin "${pluginId}" is installed in both user and project scope. Use --scope user or --scope project to specify which to upgrade.

What it means

When a plugin with the same name@marketplace id is installed in BOTH the user-scope and project-scope registries, upgradePlugin() cannot decide which copy to re-install, so it throws unless the caller explicitly passes scope. The error text mirrors the CLI's --scope flag, telling you how to disambiguate.

Source

Thrown at packages/coding-agent/src/extensibility/plugins/marketplace/manager.ts:698

	async upgradePlugin(pluginId: string, scope?: "user" | "project"): Promise<InstalledPluginEntry> {
		const parsed = parsePluginId(pluginId);
		if (!parsed) {
			throw new Error(`Invalid plugin ID: "${pluginId}". Expected "name@marketplace".`);
		}

		const { userEntries, projectEntries } = await this.#findInBothRegistries(pluginId);

		const inUser = userEntries && userEntries.length > 0;
		const inProject = projectEntries && projectEntries.length > 0;

		if (!inUser && !inProject) {
			throw new Error(`Plugin "${pluginId}" is not installed`);
		}

		let resolvedScope: "user" | "project";
		if (inUser && inProject) {
			if (!scope) {
				throw new Error(
					`Plugin "${pluginId}" is installed in both user and project scope. Use --scope user or --scope project to specify which to upgrade.`,
				);
			}
			resolvedScope = scope;
		} else if (inProject) {
			if (scope === "user") throw new Error(`Plugin "${pluginId}" is not installed in user scope`);
			resolvedScope = "project";
		} else {
			if (scope === "project") throw new Error(`Plugin "${pluginId}" is not installed in project scope`);
			resolvedScope = "user";
		}

		return this.installPlugin(parsed.name, parsed.marketplace, { force: true, scope: resolvedScope });
	}

	// Upgrade a plugin across all scopes where it is installed.
	// Returns one entry per scope upgraded (0–2 entries).
	async upgradePluginAcrossScopes(pluginId: string): Promise<InstalledPluginEntry[]> {

View on GitHub (pinned to 9690622007)

Solutions

  1. Pass the desired scope: upgradePlugin(id, "user") or upgradePlugin(id, "project").
  2. Decide which copy you actually want to upgrade — usually the project one for repo-pinned plugins.
  3. If only one copy should exist, uninstall the redundant scope entry, then upgrade the remaining one.
  4. In CLI usage, add --scope user or --scope project to the upgrade command.

Example fix

// before
await manager.upgradePlugin("my-plugin@official"); // installed in both scopes
// after
await manager.upgradePlugin("my-plugin@official", "project");
Defensive patterns

Strategy: validation

Validate before calling

const { userEntries, projectEntries } = /* look up both scopes */;
const inUser = !!userEntries?.length, inProject = !!projectEntries?.length;
const scopeArg = inUser && inProject ? (scope ?? "project") : scope; // disambiguate up front
await manager.upgradePlugin(pluginId, scopeArg);

Type guard

null

Try / catch

try {
  await manager.upgradePlugin(pluginId, scope);
} catch (err) {
  if (err instanceof Error && err.message.includes("installed in both user and project scope")) {
    await manager.upgradePlugin(pluginId, "project"); // pick a deterministic default
  } else throw err;
}

Prevention

When it happens

Trigger: Calling upgradePlugin(id) with no second argument while an entry exists in both userEntries and projectEntries (both arrays non-empty). This is exactly the ambiguity case: inUser && inProject && !scope.

Common situations: A plugin was installed globally for personal use and later also added to a project's plugin registry; both copies linger after a scope migration; scripts call the API without the scope parameter that a human would pass as --scope on the CLI.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/660c8ee06d4f70ba. Report an issue: GitHub.