mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:20:43 +00:00
feat(plugins): bridge externalized bundled updates
This commit is contained in:
66
src/plugins/externalized-bundled-plugins.ts
Normal file
66
src/plugins/externalized-bundled-plugins.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
export type ExternalizedBundledPluginBridge = {
|
||||
/** Plugin id used while the plugin was bundled in core. */
|
||||
bundledPluginId: string;
|
||||
/** Plugin id declared by the external package. Defaults to bundledPluginId. */
|
||||
pluginId?: string;
|
||||
/** npm spec OpenClaw should install when migrating the bundled plugin out. */
|
||||
npmSpec: string;
|
||||
/** Bundled directory name, when it differs from bundledPluginId. */
|
||||
bundledDirName?: string;
|
||||
/** Legacy ids that should be treated as this plugin during enablement checks. */
|
||||
legacyPluginIds?: readonly string[];
|
||||
/** Channel ids that imply this plugin is enabled when configured. */
|
||||
channelIds?: readonly string[];
|
||||
/** Plugin ids this external package supersedes for channel selection. */
|
||||
preferOver?: readonly string[];
|
||||
};
|
||||
|
||||
const EXTERNALIZED_BUNDLED_PLUGIN_BRIDGES: readonly ExternalizedBundledPluginBridge[] = [
|
||||
{
|
||||
bundledPluginId: "tlon",
|
||||
npmSpec: "@openclaw/tlon",
|
||||
channelIds: ["tlon"],
|
||||
},
|
||||
{
|
||||
bundledPluginId: "twitch",
|
||||
npmSpec: "@openclaw/twitch",
|
||||
channelIds: ["twitch", "twitch-chat"],
|
||||
legacyPluginIds: ["twitch-chat"],
|
||||
},
|
||||
{
|
||||
bundledPluginId: "synology-chat",
|
||||
npmSpec: "@openclaw/synology-chat",
|
||||
channelIds: ["synology-chat"],
|
||||
},
|
||||
];
|
||||
|
||||
function normalizePluginId(value: string | undefined): string {
|
||||
return value?.trim() ?? "";
|
||||
}
|
||||
|
||||
export function getExternalizedBundledPluginTargetId(
|
||||
bridge: ExternalizedBundledPluginBridge,
|
||||
): string {
|
||||
return normalizePluginId(bridge.pluginId) || normalizePluginId(bridge.bundledPluginId);
|
||||
}
|
||||
|
||||
export function getExternalizedBundledPluginLookupIds(
|
||||
bridge: ExternalizedBundledPluginBridge,
|
||||
): readonly string[] {
|
||||
return Array.from(
|
||||
new Set(
|
||||
[
|
||||
bridge.bundledPluginId,
|
||||
bridge.pluginId,
|
||||
...(bridge.legacyPluginIds ?? []),
|
||||
...(bridge.channelIds ?? []),
|
||||
]
|
||||
.map(normalizePluginId)
|
||||
.filter(Boolean),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function listExternalizedBundledPluginBridges(): readonly ExternalizedBundledPluginBridge[] {
|
||||
return EXTERNALIZED_BUNDLED_PLUGIN_BRIDGES;
|
||||
}
|
||||
@@ -997,4 +997,272 @@ describe("syncPluginsForUpdateChannel", () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("installs an externalized bundled plugin and rewrites its old bundled path ledger", async () => {
|
||||
resolveBundledPluginSourcesMock.mockReturnValue(new Map());
|
||||
installPluginFromNpmSpecMock.mockResolvedValue(
|
||||
createSuccessfulNpmUpdateResult({
|
||||
pluginId: "legacy-chat",
|
||||
targetDir: "/tmp/openclaw-plugins/legacy-chat",
|
||||
version: "2.0.0",
|
||||
npmResolution: {
|
||||
name: "@openclaw/legacy-chat",
|
||||
version: "2.0.0",
|
||||
resolvedSpec: "@openclaw/legacy-chat@2.0.0",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await syncPluginsForUpdateChannel({
|
||||
channel: "stable",
|
||||
externalizedBundledPluginBridges: [
|
||||
{
|
||||
bundledPluginId: "legacy-chat",
|
||||
npmSpec: "@openclaw/legacy-chat",
|
||||
channelIds: ["legacy-chat"],
|
||||
},
|
||||
],
|
||||
config: {
|
||||
channels: {
|
||||
"legacy-chat": {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
load: { paths: [appBundledPluginRoot("legacy-chat")] },
|
||||
installs: {
|
||||
"legacy-chat": {
|
||||
source: "path",
|
||||
sourcePath: appBundledPluginRoot("legacy-chat"),
|
||||
installPath: appBundledPluginRoot("legacy-chat"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(installPluginFromNpmSpecMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "@openclaw/legacy-chat",
|
||||
mode: "update",
|
||||
expectedPluginId: "legacy-chat",
|
||||
}),
|
||||
);
|
||||
expect(result.changed).toBe(true);
|
||||
expect(result.summary.switchedToNpm).toEqual(["legacy-chat"]);
|
||||
expect(result.summary.errors).toEqual([]);
|
||||
expect(result.config.plugins?.load?.paths).toEqual([]);
|
||||
expect(result.config.plugins?.installs?.["legacy-chat"]).toMatchObject({
|
||||
source: "npm",
|
||||
spec: "@openclaw/legacy-chat",
|
||||
installPath: "/tmp/openclaw-plugins/legacy-chat",
|
||||
version: "2.0.0",
|
||||
resolvedName: "@openclaw/legacy-chat",
|
||||
resolvedVersion: "2.0.0",
|
||||
resolvedSpec: "@openclaw/legacy-chat@2.0.0",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not externalize disabled bundled plugins", async () => {
|
||||
resolveBundledPluginSourcesMock.mockReturnValue(new Map());
|
||||
|
||||
const result = await syncPluginsForUpdateChannel({
|
||||
channel: "stable",
|
||||
externalizedBundledPluginBridges: [
|
||||
{
|
||||
bundledPluginId: "legacy-chat",
|
||||
npmSpec: "@openclaw/legacy-chat",
|
||||
channelIds: ["legacy-chat"],
|
||||
},
|
||||
],
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
"legacy-chat": {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
load: { paths: [appBundledPluginRoot("legacy-chat")] },
|
||||
installs: {
|
||||
"legacy-chat": {
|
||||
source: "path",
|
||||
sourcePath: appBundledPluginRoot("legacy-chat"),
|
||||
installPath: appBundledPluginRoot("legacy-chat"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(installPluginFromNpmSpecMock).not.toHaveBeenCalled();
|
||||
expect(result.changed).toBe(false);
|
||||
expect(result.config.plugins?.installs?.["legacy-chat"]).toMatchObject({
|
||||
source: "path",
|
||||
});
|
||||
});
|
||||
|
||||
it("leaves config unchanged when externalized plugin installation fails", async () => {
|
||||
resolveBundledPluginSourcesMock.mockReturnValue(new Map());
|
||||
installPluginFromNpmSpecMock.mockResolvedValue({
|
||||
ok: false,
|
||||
error: "package unavailable",
|
||||
});
|
||||
const config: OpenClawConfig = {
|
||||
channels: {
|
||||
"legacy-chat": {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
load: { paths: [appBundledPluginRoot("legacy-chat")] },
|
||||
installs: {
|
||||
"legacy-chat": {
|
||||
source: "path",
|
||||
sourcePath: appBundledPluginRoot("legacy-chat"),
|
||||
installPath: appBundledPluginRoot("legacy-chat"),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await syncPluginsForUpdateChannel({
|
||||
channel: "stable",
|
||||
externalizedBundledPluginBridges: [
|
||||
{
|
||||
bundledPluginId: "legacy-chat",
|
||||
npmSpec: "@openclaw/legacy-chat",
|
||||
channelIds: ["legacy-chat"],
|
||||
},
|
||||
],
|
||||
config,
|
||||
});
|
||||
|
||||
expect(result.changed).toBe(false);
|
||||
expect(result.config).toBe(config);
|
||||
expect(result.summary.errors).toEqual(["Failed to update legacy-chat: package unavailable"]);
|
||||
});
|
||||
|
||||
it("does not externalize custom local path installs that only share the old plugin id", async () => {
|
||||
resolveBundledPluginSourcesMock.mockReturnValue(new Map());
|
||||
|
||||
const result = await syncPluginsForUpdateChannel({
|
||||
channel: "stable",
|
||||
externalizedBundledPluginBridges: [
|
||||
{
|
||||
bundledPluginId: "legacy-chat",
|
||||
npmSpec: "@openclaw/legacy-chat",
|
||||
channelIds: ["legacy-chat"],
|
||||
},
|
||||
],
|
||||
config: {
|
||||
channels: {
|
||||
"legacy-chat": {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
load: { paths: ["/workspace/plugins/legacy-chat"] },
|
||||
installs: {
|
||||
"legacy-chat": {
|
||||
source: "path",
|
||||
sourcePath: "/workspace/plugins/legacy-chat",
|
||||
installPath: "/workspace/plugins/legacy-chat",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(installPluginFromNpmSpecMock).not.toHaveBeenCalled();
|
||||
expect(result.changed).toBe(false);
|
||||
expect(result.config.plugins?.installs?.["legacy-chat"]).toMatchObject({
|
||||
source: "path",
|
||||
sourcePath: "/workspace/plugins/legacy-chat",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not externalize while the bundled source is still present in the current build", async () => {
|
||||
mockBundledSources(
|
||||
createBundledSource({
|
||||
pluginId: "legacy-chat",
|
||||
localPath: appBundledPluginRoot("legacy-chat"),
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await syncPluginsForUpdateChannel({
|
||||
channel: "stable",
|
||||
externalizedBundledPluginBridges: [
|
||||
{
|
||||
bundledPluginId: "legacy-chat",
|
||||
npmSpec: "@openclaw/legacy-chat",
|
||||
channelIds: ["legacy-chat"],
|
||||
},
|
||||
],
|
||||
config: {
|
||||
channels: {
|
||||
"legacy-chat": {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
load: { paths: [appBundledPluginRoot("legacy-chat")] },
|
||||
installs: {
|
||||
"legacy-chat": {
|
||||
source: "path",
|
||||
sourcePath: appBundledPluginRoot("legacy-chat"),
|
||||
installPath: appBundledPluginRoot("legacy-chat"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(installPluginFromNpmSpecMock).not.toHaveBeenCalled();
|
||||
expect(result.changed).toBe(false);
|
||||
expect(result.config.plugins?.installs?.["legacy-chat"]).toMatchObject({
|
||||
source: "path",
|
||||
});
|
||||
});
|
||||
|
||||
it("removes stale bundled load paths for already-externalized npm installs", async () => {
|
||||
resolveBundledPluginSourcesMock.mockReturnValue(new Map());
|
||||
|
||||
const result = await syncPluginsForUpdateChannel({
|
||||
channel: "stable",
|
||||
externalizedBundledPluginBridges: [
|
||||
{
|
||||
bundledPluginId: "legacy-chat",
|
||||
npmSpec: "@openclaw/legacy-chat",
|
||||
channelIds: ["legacy-chat"],
|
||||
},
|
||||
],
|
||||
config: {
|
||||
channels: {
|
||||
"legacy-chat": {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
load: {
|
||||
paths: [appBundledPluginRoot("legacy-chat"), "/workspace/plugins/other"],
|
||||
},
|
||||
installs: {
|
||||
"legacy-chat": {
|
||||
source: "npm",
|
||||
spec: "@openclaw/legacy-chat",
|
||||
installPath: "/tmp/openclaw-plugins/legacy-chat",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(installPluginFromNpmSpecMock).not.toHaveBeenCalled();
|
||||
expect(result.changed).toBe(true);
|
||||
expect(result.config.plugins?.load?.paths).toEqual(["/workspace/plugins/other"]);
|
||||
expect(result.config.plugins?.installs?.["legacy-chat"]).toMatchObject({
|
||||
source: "npm",
|
||||
spec: "@openclaw/legacy-chat",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
||||
import type { NpmSpecResolution } from "../infra/install-source-utils.js";
|
||||
import { resolveNpmSpecMetadata } from "../infra/install-source-utils.js";
|
||||
import {
|
||||
@@ -9,6 +10,13 @@ import type { UpdateChannel } from "../infra/update-channels.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { resolveBundledPluginSources } from "./bundled-sources.js";
|
||||
import { installPluginFromClawHub } from "./clawhub.js";
|
||||
import { normalizePluginsConfig, resolveEffectiveEnableState } from "./config-state.js";
|
||||
import {
|
||||
getExternalizedBundledPluginLookupIds,
|
||||
getExternalizedBundledPluginTargetId,
|
||||
listExternalizedBundledPluginBridges,
|
||||
type ExternalizedBundledPluginBridge,
|
||||
} from "./externalized-bundled-plugins.js";
|
||||
import {
|
||||
installPluginFromNpmSpec,
|
||||
PLUGIN_INSTALL_ERROR_CODE,
|
||||
@@ -173,9 +181,20 @@ function buildLoadPathHelpers(existing: string[], env: NodeJS.ProcessEnv = proce
|
||||
changed = true;
|
||||
};
|
||||
|
||||
const removeMatching = (predicate: (value: string) => boolean) => {
|
||||
const next = paths.filter((entry) => !predicate(entry));
|
||||
if (next.length === paths.length) {
|
||||
return;
|
||||
}
|
||||
paths = next;
|
||||
resolved = resolveSet();
|
||||
changed = true;
|
||||
};
|
||||
|
||||
return {
|
||||
addPath,
|
||||
removePath,
|
||||
removeMatching,
|
||||
get changed() {
|
||||
return changed;
|
||||
},
|
||||
@@ -185,6 +204,139 @@ function buildLoadPathHelpers(existing: string[], env: NodeJS.ProcessEnv = proce
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePathSegment(value: string | undefined): string {
|
||||
return (
|
||||
value
|
||||
?.trim()
|
||||
.replaceAll("\\", "/")
|
||||
.replace(/^\/+|\/+$/g, "") ?? ""
|
||||
);
|
||||
}
|
||||
|
||||
function pathEndsWithSegment(params: {
|
||||
value: string | undefined;
|
||||
segment: string | undefined;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): boolean {
|
||||
const value = normalizePathSegment(params.value ? resolveUserPath(params.value, params.env) : "");
|
||||
const segment = normalizePathSegment(params.segment);
|
||||
return Boolean(value && segment && (value === segment || value.endsWith(`/${segment}`)));
|
||||
}
|
||||
|
||||
function isBridgeBundledPathRecord(params: {
|
||||
bridge: ExternalizedBundledPluginBridge;
|
||||
bundledLocalPath?: string;
|
||||
record: PluginInstallRecord;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): boolean {
|
||||
if (params.record.source !== "path") {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
params.bundledLocalPath &&
|
||||
(pathsEqual(params.record.sourcePath, params.bundledLocalPath, params.env) ||
|
||||
pathsEqual(params.record.installPath, params.bundledLocalPath, params.env))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const bundledDirName = params.bridge.bundledDirName ?? params.bridge.bundledPluginId;
|
||||
return (
|
||||
pathEndsWithSegment({
|
||||
value: params.record.sourcePath,
|
||||
segment: `extensions/${bundledDirName}`,
|
||||
env: params.env,
|
||||
}) ||
|
||||
pathEndsWithSegment({
|
||||
value: params.record.installPath,
|
||||
segment: `extensions/${bundledDirName}`,
|
||||
env: params.env,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function removeBridgeBundledLoadPaths(params: {
|
||||
bridge: ExternalizedBundledPluginBridge;
|
||||
loadPaths: ReturnType<typeof buildLoadPathHelpers>;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}) {
|
||||
const bundledDirName = params.bridge.bundledDirName ?? params.bridge.bundledPluginId;
|
||||
params.loadPaths.removeMatching((entry) =>
|
||||
pathEndsWithSegment({
|
||||
value: entry,
|
||||
segment: `extensions/${bundledDirName}`,
|
||||
env: params.env,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveBridgeInstallRecord(params: {
|
||||
installs: Record<string, PluginInstallRecord>;
|
||||
bridge: ExternalizedBundledPluginBridge;
|
||||
}): { pluginId: string; record: PluginInstallRecord } | undefined {
|
||||
for (const pluginId of getExternalizedBundledPluginLookupIds(params.bridge)) {
|
||||
const record = params.installs[pluginId];
|
||||
if (record) {
|
||||
return { pluginId, record };
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isBridgeChannelEnabledByConfig(params: {
|
||||
config: OpenClawConfig;
|
||||
bridge: ExternalizedBundledPluginBridge;
|
||||
}): boolean {
|
||||
const channels = params.config.channels;
|
||||
if (!channels || typeof channels !== "object" || Array.isArray(channels)) {
|
||||
return false;
|
||||
}
|
||||
for (const channelId of params.bridge.channelIds ?? []) {
|
||||
const entry = (channels as Record<string, unknown>)[channelId];
|
||||
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
||||
continue;
|
||||
}
|
||||
if ((entry as Record<string, unknown>).enabled === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isExternalizedBundledPluginEnabled(params: {
|
||||
config: OpenClawConfig;
|
||||
bridge: ExternalizedBundledPluginBridge;
|
||||
}): boolean {
|
||||
const normalized = normalizePluginsConfig(params.config.plugins);
|
||||
if (!normalized.enabled) {
|
||||
return false;
|
||||
}
|
||||
const pluginIds = getExternalizedBundledPluginLookupIds(params.bridge);
|
||||
if (
|
||||
pluginIds.some(
|
||||
(pluginId) =>
|
||||
normalized.deny.includes(pluginId) || normalized.entries[pluginId]?.enabled === false,
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
for (const pluginId of pluginIds) {
|
||||
if (
|
||||
resolveEffectiveEnableState({
|
||||
id: pluginId,
|
||||
origin: "bundled",
|
||||
config: normalized,
|
||||
rootConfig: params.config,
|
||||
}).enabled
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (isBridgeChannelEnabledByConfig(params)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function replacePluginIdInList(
|
||||
entries: string[] | undefined,
|
||||
fromId: string,
|
||||
@@ -664,8 +816,10 @@ export async function syncPluginsForUpdateChannel(params: {
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
logger?: PluginUpdateLogger;
|
||||
externalizedBundledPluginBridges?: readonly ExternalizedBundledPluginBridge[];
|
||||
}): Promise<PluginChannelSyncResult> {
|
||||
const env = params.env ?? process.env;
|
||||
const logger = params.logger ?? {};
|
||||
const summary: PluginChannelSyncSummary = {
|
||||
switchedToBundled: [],
|
||||
switchedToNpm: [],
|
||||
@@ -676,13 +830,10 @@ export async function syncPluginsForUpdateChannel(params: {
|
||||
workspaceDir: params.workspaceDir,
|
||||
env,
|
||||
});
|
||||
if (bundled.size === 0) {
|
||||
return { config: params.config, changed: false, summary };
|
||||
}
|
||||
|
||||
let next = params.config;
|
||||
const loadHelpers = buildLoadPathHelpers(next.plugins?.load?.paths ?? [], env);
|
||||
const installs = next.plugins?.installs ?? {};
|
||||
let installs = next.plugins?.installs ?? {};
|
||||
let changed = false;
|
||||
|
||||
if (params.channel === "dev") {
|
||||
@@ -712,6 +863,105 @@ export async function syncPluginsForUpdateChannel(params: {
|
||||
changed = true;
|
||||
}
|
||||
} else {
|
||||
const bridges =
|
||||
params.externalizedBundledPluginBridges ?? listExternalizedBundledPluginBridges();
|
||||
for (const bridge of bridges) {
|
||||
const targetPluginId = getExternalizedBundledPluginTargetId(bridge);
|
||||
const bundledInfo = bundled.get(bridge.bundledPluginId);
|
||||
if (bundledInfo) {
|
||||
continue;
|
||||
}
|
||||
const existing = resolveBridgeInstallRecord({ installs, bridge });
|
||||
if (
|
||||
!existing &&
|
||||
!isExternalizedBundledPluginEnabled({
|
||||
config: next,
|
||||
bridge,
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
existing &&
|
||||
!isExternalizedBundledPluginEnabled({
|
||||
config: next,
|
||||
bridge,
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (existing?.record.source === "npm" && existing.record.spec === bridge.npmSpec) {
|
||||
if (existing.pluginId !== targetPluginId) {
|
||||
next = migratePluginConfigId(next, existing.pluginId, targetPluginId);
|
||||
installs = next.plugins?.installs ?? {};
|
||||
changed = true;
|
||||
}
|
||||
if (bundledInfo?.localPath) {
|
||||
loadHelpers.removePath(bundledInfo.localPath);
|
||||
}
|
||||
removeBridgeBundledLoadPaths({ bridge, loadPaths: loadHelpers, env });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
existing &&
|
||||
!isBridgeBundledPathRecord({
|
||||
bridge,
|
||||
bundledLocalPath: bundledInfo?.localPath,
|
||||
record: existing.record,
|
||||
env,
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = await installPluginFromNpmSpec({
|
||||
spec: bridge.npmSpec,
|
||||
mode: "update",
|
||||
expectedPluginId: targetPluginId,
|
||||
logger,
|
||||
});
|
||||
if (!result.ok) {
|
||||
const message = formatNpmInstallFailure({
|
||||
pluginId: targetPluginId,
|
||||
spec: bridge.npmSpec,
|
||||
phase: "update",
|
||||
result,
|
||||
});
|
||||
summary.errors.push(message);
|
||||
logger.error?.(message);
|
||||
continue;
|
||||
}
|
||||
|
||||
const resolvedPluginId = result.pluginId;
|
||||
if (existing && existing.pluginId !== resolvedPluginId) {
|
||||
next = migratePluginConfigId(next, existing.pluginId, resolvedPluginId);
|
||||
}
|
||||
const nextVersion = result.version ?? (await readInstalledPackageVersion(result.targetDir));
|
||||
next = recordPluginInstall(next, {
|
||||
pluginId: resolvedPluginId,
|
||||
source: "npm",
|
||||
spec: bridge.npmSpec,
|
||||
installPath: result.targetDir,
|
||||
version: nextVersion,
|
||||
...buildNpmResolutionInstallFields(result.npmResolution),
|
||||
});
|
||||
installs = next.plugins?.installs ?? {};
|
||||
if (bundledInfo?.localPath) {
|
||||
loadHelpers.removePath(bundledInfo.localPath);
|
||||
}
|
||||
if (existing?.record.sourcePath) {
|
||||
loadHelpers.removePath(existing.record.sourcePath);
|
||||
}
|
||||
if (existing?.record.installPath) {
|
||||
loadHelpers.removePath(existing.record.installPath);
|
||||
}
|
||||
removeBridgeBundledLoadPaths({ bridge, loadPaths: loadHelpers, env });
|
||||
summary.switchedToNpm.push(resolvedPluginId);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
for (const [pluginId, record] of Object.entries(installs)) {
|
||||
const bundledInfo = bundled.get(pluginId);
|
||||
if (!bundledInfo) {
|
||||
|
||||
Reference in New Issue
Block a user