From 47fdadef6a2119ad20ac7183423fd3d5f51ec49b Mon Sep 17 00:00:00 2001 From: long Date: Tue, 27 Jan 2026 10:35:29 +0700 Subject: [PATCH] fix(plugin-install): handle existing plugins and filter workspace deps Skip installation if plugin already exists in registry, unless it's a bundled plugin with local workspace. Filter out workspace:* dependencies before npm install to prevent installation errors in standalone packages. --- .../onboarding/plugin-install.test.ts | 4 +++ src/commands/onboarding/plugin-install.ts | 36 +++++++++++++++++++ src/plugins/install.ts | 17 ++++++++- 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/commands/onboarding/plugin-install.test.ts b/src/commands/onboarding/plugin-install.test.ts index 87f06c2c6..40d13502a 100644 --- a/src/commands/onboarding/plugin-install.test.ts +++ b/src/commands/onboarding/plugin-install.test.ts @@ -16,6 +16,10 @@ vi.mock("../../plugins/loader.js", () => ({ loadClawdbotPlugins: vi.fn(), })); +vi.mock("../../plugins/manifest-registry.js", () => ({ + loadPluginManifestRegistry: vi.fn(() => ({ plugins: [], diagnostics: [] })), +})); + import fs from "node:fs"; import type { ChannelPluginCatalogEntry } from "../../channels/plugins/catalog.js"; import type { ClawdbotConfig } from "../../config/config.js"; diff --git a/src/commands/onboarding/plugin-install.ts b/src/commands/onboarding/plugin-install.ts index d676c26ef..5e605f6fb 100644 --- a/src/commands/onboarding/plugin-install.ts +++ b/src/commands/onboarding/plugin-install.ts @@ -8,6 +8,7 @@ import { recordPluginInstall } from "../../plugins/installs.js"; import { enablePluginInConfig } from "../../plugins/enable.js"; import { loadClawdbotPlugins } from "../../plugins/loader.js"; import { installPluginFromNpmSpec } from "../../plugins/install.js"; +import { loadPluginManifestRegistry } from "../../plugins/manifest-registry.js"; import type { RuntimeEnv } from "../../runtime.js"; import type { WizardPrompter } from "../../wizard/prompts.js"; @@ -18,6 +19,24 @@ type InstallResult = { installed: boolean; }; +function findExistingPluginOrigin(params: { + pluginId: string; + cfg: ClawdbotConfig; + workspaceDir?: string; +}): "config" | "workspace" | "global" | "bundled" | null { + const workspaceDir = + params.workspaceDir ?? + resolveAgentWorkspaceDir(params.cfg, resolveDefaultAgentId(params.cfg)) ?? + undefined; + const registry = loadPluginManifestRegistry({ + config: params.cfg, + workspaceDir, + cache: false, + }); + const found = registry.plugins.find((plugin) => plugin.id === params.pluginId); + return found?.origin ?? null; +} + function hasGitWorkspace(workspaceDir?: string): boolean { const candidates = new Set(); candidates.add(path.join(process.cwd(), ".git")); @@ -122,7 +141,24 @@ export async function ensureOnboardingPluginInstalled(params: { }): Promise { const { entry, prompter, runtime, workspaceDir } = params; let next = params.cfg; + const allowLocal = hasGitWorkspace(workspaceDir); + const existingOrigin = findExistingPluginOrigin({ + pluginId: entry.id, + cfg: next, + workspaceDir, + }); + if (existingOrigin && (existingOrigin !== "bundled" || !allowLocal)) { + const enabled = enablePluginInConfig(next, entry.id); + next = enabled.config; + if (enabled.enabled) return { cfg: next, installed: true }; + await prompter.note( + `Cannot enable ${entry.id}: ${enabled.reason ?? "plugin disabled"}.`, + "Plugin install", + ); + return { cfg: next, installed: false }; + } + const localPath = resolveLocalPath(entry, workspaceDir, allowLocal); const defaultChoice = resolveInstallDefaultChoice({ cfg: next, diff --git a/src/plugins/install.ts b/src/plugins/install.ts index 581d34e48..2dcbbfc51 100644 --- a/src/plugins/install.ts +++ b/src/plugins/install.ts @@ -162,8 +162,23 @@ async function installPluginFromPackageDir(params: { } const deps = manifest.dependencies ?? {}; - const hasDeps = Object.keys(deps).length > 0; + // Filter out workspace:* dependencies as they're not supported in standalone npm packages + const filteredDeps = Object.fromEntries( + Object.entries(deps).filter(([, version]) => !String(version).startsWith("workspace:")), + ); + const hasDeps = Object.keys(filteredDeps).length > 0; if (hasDeps) { + // Update package.json in targetDir to remove workspace: dependencies before npm install + const targetManifestPath = path.join(targetDir, "package.json"); + const filteredManifest = { + ...manifest, + dependencies: filteredDeps, + }; + await fs.writeFile( + targetManifestPath, + JSON.stringify(filteredManifest, null, 2) + "\n", + "utf-8", + ); logger.info?.("Installing plugin dependencies…"); const npmRes = await runCommandWithTimeout(["npm", "install", "--omit=dev", "--silent"], { timeoutMs: Math.max(timeoutMs, 300_000),