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.
This commit is contained in:
long 2026-01-27 10:35:29 +07:00
parent dce7925e2a
commit 47fdadef6a
3 changed files with 56 additions and 1 deletions

View File

@ -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";

View File

@ -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<string>();
candidates.add(path.join(process.cwd(), ".git"));
@ -122,7 +141,24 @@ export async function ensureOnboardingPluginInstalled(params: {
}): Promise<InstallResult> {
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,

View File

@ -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),