fix(update): prune stale local bundled plugin shadows

Summary:\n- prune stale local bundled plugin path records during update/doctor repair\n- keep current, same-version, versionless, source-checkout, and arbitrary local path records preserved\n- add changelog and deterministic sort comparator cleanup\n\nVerification:\n- node scripts/run-vitest.mjs src/plugins/contracts/boundary-invariants.test.ts src/plugins/stale-local-bundled-plugin-install-records.test.ts src/cli/update-cli/post-core-plugin-convergence.test.ts src/commands/doctor-plugin-registry.test.ts\n- node scripts/run-oxlint-shards.mjs --threads=8\n- ./node_modules/.bin/oxfmt --check --threads=1 CHANGELOG.md src/plugins/stale-local-bundled-plugin-install-records.ts src/commands/doctor-plugin-registry.ts\n- git diff --check\n- GitHub exact-SHA: Real behavior proof, build-artifacts, checks-fast-contracts-plugins-a, check-prod-types, check-lint, check-test-types green on 8bcbf681ec
This commit is contained in:
Jason (Json)
2026-05-21 01:49:19 -06:00
committed by GitHub
parent 3eb2d64392
commit 4a360ac1cc
8 changed files with 609 additions and 8 deletions

View File

@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
- fix(config): validate browser sandbox bind sources [AI]. (#84799) Thanks @pgondhi987.
- doctor: constrain legacy plugin cleanup paths [AI]. (#84801) Thanks @pgondhi987.
- Update/doctor: prune stale local bundled plugin install records that point at old compiled bundled output so current bundled plugin schemas win after upgrade. (#84863) Thanks @fuller-stack-dev.
- Media/audio: skip empty structured sherpa-onnx transcripts instead of treating the raw JSON payload as spoken text. (#84667) Thanks @TurboTheTurtle.
- Node/Linux: keep `OPENCLAW_GATEWAY_TOKEN` out of generated systemd unit files by writing node service token values to a node-specific env file. (#84408)
- Memory-core/dreaming: reuse stable narrative subagent session keys per workspace and phase while keeping per-run idempotency and bounded cleanup, so stale `dreaming-narrative-*` sessions do not accumulate. Fixes #68252, #69187, and #70402. (#70464) Thanks @chiyouYCH.

View File

@@ -1,4 +1,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
repairMissingConfiguredPluginInstalls: vi.fn(),
@@ -21,6 +24,8 @@ import {
} from "./post-core-plugin-convergence.js";
describe("runPostCorePluginConvergence", () => {
const tempDirs: string[] = [];
beforeEach(() => {
vi.clearAllMocks();
mocks.repairMissingConfiguredPluginInstalls.mockResolvedValue({
@@ -31,6 +36,43 @@ describe("runPostCorePluginConvergence", () => {
mocks.runPluginPayloadSmokeCheck.mockResolvedValue({ checked: [], failures: [] });
});
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
fs.rmSync(dir, { recursive: true, force: true });
}
});
function makeTempDir(): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-post-core-convergence-"));
tempDirs.push(dir);
return dir;
}
function writeBundledPlugin(rootDir: string, pluginId: string): string {
const pluginDir = path.join(rootDir, pluginId);
fs.mkdirSync(pluginDir, { recursive: true });
fs.writeFileSync(path.join(pluginDir, "index.js"), "export default {};\n", "utf8");
fs.writeFileSync(
path.join(pluginDir, "openclaw.plugin.json"),
JSON.stringify({
id: pluginId,
name: pluginId,
version: "2026.5.20-beta.1",
configSchema: { type: "object" },
}),
"utf8",
);
fs.writeFileSync(
path.join(pluginDir, "package.json"),
JSON.stringify({
name: `@openclaw/${pluginId}`,
version: "2026.5.20-beta.1",
}),
"utf8",
);
return pluginDir;
}
it("calls repair with OPENCLAW_UPDATE_POST_CORE_CONVERGENCE=1 set", async () => {
const cfg = { plugins: { entries: {} } } as unknown as OpenClawConfig;
await runPostCorePluginConvergence({
@@ -121,6 +163,55 @@ describe("runPostCorePluginConvergence", () => {
});
});
it("prunes stale local bundled plugin shadows from baseline records before repair", async () => {
const bundledRoot = makeTempDir();
writeBundledPlugin(bundledRoot, "discord");
const baseline = {
discord: {
source: "path" as const,
installPath: path.join(makeTempDir(), "dist", "extensions", "discord"),
version: "2026.5.4-beta.3",
},
brave: { source: "npm" as const, installPath: "/p/brave" },
};
mocks.repairMissingConfiguredPluginInstalls.mockResolvedValue({
changes: [],
warnings: [],
records: { brave: baseline.brave },
});
const cfg = {
plugins: { entries: { discord: { enabled: true }, brave: { enabled: true } } },
} as unknown as OpenClawConfig;
const result = await runPostCorePluginConvergence({
cfg,
env: {
OPENCLAW_BUNDLED_PLUGINS_DIR: bundledRoot,
OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR: "1",
VITEST: "true",
},
baselineInstallRecords: baseline,
});
expect(mocks.repairMissingConfiguredPluginInstalls).toHaveBeenCalledWith({
cfg,
env: {
OPENCLAW_BUNDLED_PLUGINS_DIR: bundledRoot,
OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR: "1",
VITEST: "true",
OPENCLAW_COMPATIBILITY_HOST_VERSION: VERSION,
OPENCLAW_UPDATE_POST_CORE_CONVERGENCE: "1",
},
baselineRecords: {
brave: baseline.brave,
},
});
expect(result.changes).toEqual([
'Removed stale local bundled plugin install record "discord".',
]);
expect(result.installRecords).toEqual({ brave: baseline.brave });
});
it("flags errored=true and surfaces actionable guidance when repair warns", async () => {
mocks.repairMissingConfiguredPluginInstalls.mockResolvedValue({
changes: [],

View File

@@ -3,6 +3,7 @@ import { UPDATE_POST_CORE_CONVERGENCE_ENV } from "../../commands/doctor/shared/u
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { PluginInstallRecord } from "../../config/types.plugins.js";
import { normalizePluginsConfig, resolveEffectiveEnableState } from "../../plugins/config-state.js";
import { pruneStaleLocalBundledPluginInstallRecords } from "../../plugins/stale-local-bundled-plugin-install-records.js";
import {
resolveTrustedSourceLinkedOfficialClawHubSpec,
resolveTrustedSourceLinkedOfficialNpmSpec,
@@ -66,11 +67,17 @@ export async function runPostCorePluginConvergence(params: {
OPENCLAW_COMPATIBILITY_HOST_VERSION: VERSION,
[UPDATE_POST_CORE_CONVERGENCE_ENV]: "1",
};
const prunedBaseline = params.baselineInstallRecords
? pruneStaleLocalBundledPluginInstallRecords({
installRecords: params.baselineInstallRecords,
env,
})
: null;
const repair = await repairMissingConfiguredPluginInstalls({
cfg: params.cfg,
env,
...(params.baselineInstallRecords ? { baselineRecords: params.baselineInstallRecords } : {}),
...(prunedBaseline ? { baselineRecords: prunedBaseline.records } : {}),
});
const warnings: PostCoreConvergenceWarning[] = repair.warnings.map((message) => ({
@@ -99,7 +106,12 @@ export async function runPostCorePluginConvergence(params: {
}
return {
changes: repair.changes,
changes: [
...(prunedBaseline?.stale.map(
(record) => `Removed stale local bundled plugin install record "${record.pluginId}".`,
) ?? []),
...repair.changes,
],
warnings,
errored: warnings.length > 0,
smokeFailures: smoke.failures,

View File

@@ -222,6 +222,23 @@ function createCurrentIndexWithNpmRecord(params: {
};
}
function createCurrentIndexWithPathRecord(params: {
pluginId: string;
installPath: string;
version?: string;
}): InstalledPluginIndex {
return {
...createCurrentIndex(),
installRecords: {
[params.pluginId]: {
source: "path",
installPath: params.installPath,
...(params.version ? { version: params.version } : {}),
},
},
};
}
function expectedPluginIndexRecord(params: {
rootDir: string;
pluginId: string;
@@ -462,6 +479,113 @@ describe("maybeRepairPluginRegistryState", () => {
]);
});
it("warns about stale local bundled plugin install records that shadow bundled plugins", async () => {
const stateDir = makeTempDir();
const bundledDir = path.join(stateDir, "current", "dist", "extensions", "discord");
const staleDir = path.join(stateDir, "old-checkout", "dist", "extensions", "discord");
fs.mkdirSync(bundledDir, { recursive: true });
fs.mkdirSync(staleDir, { recursive: true });
createCandidate(staleDir, "discord");
await writePersistedInstalledPluginIndex(
createCurrentIndexWithPathRecord({
pluginId: "discord",
installPath: staleDir,
version: "2026.5.4-beta.3",
}),
{ stateDir },
);
await maybeRepairPluginRegistryState({
stateDir,
candidates: [
createBundledCandidate({
rootDir: bundledDir,
id: "discord",
packageName: "@openclaw/discord",
version: "2026.5.20-beta.1",
}),
],
env: hermeticEnv(),
config: {
plugins: {
allow: ["discord"],
entries: {
discord: {
enabled: true,
config: {},
},
},
},
},
prompter: { shouldRepair: false },
});
const notes = vi.mocked(note).mock.calls.join("\n");
expect(notes).toContain("Local bundled plugin install records shadow bundled plugins");
expect(notes).toContain("discord");
expect(notes).toContain(staleDir);
const persisted = await readRequiredPersistedInstalledPluginIndex(stateDir);
expect(persisted.installRecords).toHaveProperty("discord");
});
it("removes stale local bundled plugin install records during repair", async () => {
const stateDir = makeTempDir();
const bundledDir = path.join(stateDir, "current", "dist", "extensions", "discord");
const staleDir = path.join(stateDir, "old-checkout", "dist", "extensions", "discord");
fs.mkdirSync(bundledDir, { recursive: true });
fs.mkdirSync(staleDir, { recursive: true });
createCandidate(staleDir, "discord");
await writePersistedInstalledPluginIndex(
createCurrentIndexWithPathRecord({
pluginId: "discord",
installPath: staleDir,
version: "2026.5.4-beta.3",
}),
{ stateDir },
);
await maybeRepairPluginRegistryState({
stateDir,
candidates: [
createBundledCandidate({
rootDir: bundledDir,
id: "discord",
packageName: "@openclaw/discord",
version: "2026.5.20-beta.1",
}),
],
env: hermeticEnv(),
config: {
plugins: {
allow: ["discord"],
entries: {
discord: {
enabled: true,
config: {},
},
},
},
},
prompter: { shouldRepair: true },
});
const persisted = await readRequiredPersistedInstalledPluginIndex(stateDir);
expect(persisted.installRecords).toStrictEqual({});
expect(persisted.refreshReason).toBe("migration");
expect(persisted.plugins).toStrictEqual([
expectedPluginIndexRecord({
pluginId: "discord",
rootDir: bundledDir,
origin: "bundled",
packageName: "@openclaw/discord",
packageVersion: "2026.5.20-beta.1",
}),
]);
expect(vi.mocked(note).mock.calls.join("\n")).toContain(
"Removed stale local bundled plugin install record",
);
});
it("removes stale managed npm packages from the package lock during repair", async () => {
const stateDir = makeTempDir();
const bundledDir = path.join(stateDir, "bundled", "google-meet");

View File

@@ -4,6 +4,7 @@ import { formatCliCommand } from "../cli/command-format.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { saveJsonFile } from "../infra/json-file.js";
import { tryReadJsonSync } from "../infra/json-files.js";
import type { BundledPluginSource } from "../plugins/bundled-sources.js";
import { resolveDefaultPluginNpmDir } from "../plugins/install-paths.js";
import {
loadInstalledPluginIndexInstallRecords,
@@ -15,6 +16,10 @@ import {
relinkOpenClawPeerDependenciesInManagedNpmRoot,
} from "../plugins/plugin-peer-link.js";
import { refreshPluginRegistry } from "../plugins/plugin-registry.js";
import {
listStaleLocalBundledPluginInstallRecords,
type StaleLocalBundledPluginInstallRecord,
} from "../plugins/stale-local-bundled-plugin-install-records.js";
import { note } from "../terminal/note.js";
import { shortenHomePath } from "../utils.js";
import type { DoctorPrompter } from "./doctor-prompter.js";
@@ -107,7 +112,9 @@ function listStaleManagedNpmBundledPlugins(
const dependencies = readStringMap(readJsonObject(npmPackageJsonPath)?.dependencies);
const stale: StaleManagedNpmBundledPlugin[] = [];
for (const packageName of Object.keys(dependencies).toSorted()) {
for (const packageName of Object.keys(dependencies).toSorted((left, right) =>
left.localeCompare(right),
)) {
if (!packageName.startsWith("@openclaw/")) {
continue;
}
@@ -132,6 +139,40 @@ function listStaleManagedNpmBundledPlugins(
return stale;
}
function loadCurrentBundledPluginSources(
params: PluginRegistryDoctorRepairParams,
): Map<string, BundledPluginSource> {
const currentBundled = loadInstalledPluginIndex({
...params,
installRecords: {},
}).plugins.filter((plugin) => plugin.origin === "bundled");
return new Map(
currentBundled.map(
(plugin) =>
[
plugin.pluginId,
{
pluginId: plugin.pluginId,
localPath: plugin.rootDir,
...(plugin.packageName ? { npmSpec: plugin.packageName } : {}),
...(plugin.packageVersion ? { version: plugin.packageVersion } : {}),
},
] as const,
),
);
}
async function listStaleLocalBundledPluginInstallRecordShadows(
params: PluginRegistryDoctorRepairParams,
): Promise<StaleLocalBundledPluginInstallRecord[]> {
return listStaleLocalBundledPluginInstallRecords({
installRecords: await loadInstalledPluginIndexInstallRecords(params),
workspaceDir: params.workspaceDir,
env: params.env,
bundled: loadCurrentBundledPluginSources(params),
});
}
function removeManagedNpmDependency(params: {
npmRoot: string;
packageName: string;
@@ -241,6 +282,36 @@ export function maybeRepairStaleManagedNpmBundledPlugins(
return true;
}
export async function maybeRepairStaleLocalBundledPluginInstallRecords(
params: PluginRegistryDoctorRepairParams,
): Promise<string[]> {
const stale = await listStaleLocalBundledPluginInstallRecordShadows(params);
if (stale.length === 0) {
return [];
}
if (!params.prompter.shouldRepair) {
note(
[
"Local bundled plugin install records shadow bundled plugins:",
...stale.map((record) => `- ${record.pluginId}: ${shortenHomePath(record.stalePath)}`),
`Repair with ${formatCliCommand("openclaw doctor --fix")} to remove stale local install records and rebuild the plugin registry.`,
].join("\n"),
"Plugin registry",
);
return [];
}
note(
[
"Removed stale local bundled plugin install record(s) shadowing bundled plugins:",
...stale.map((record) => `- ${record.pluginId}: ${shortenHomePath(record.stalePath)}`),
].join("\n"),
"Plugin registry",
);
return stale.map((record) => record.pluginId);
}
export async function maybeRepairManagedNpmOpenClawPeerLinks(
params: PluginRegistryDoctorRepairParams,
): Promise<boolean> {
@@ -323,7 +394,15 @@ export async function maybeRepairPluginRegistryState(
(plugin) => plugin.pluginId,
);
const removedStaleManagedNpmBundledPlugins = maybeRepairStaleManagedNpmBundledPlugins(params);
const removedStaleLocalBundledPluginIds =
await maybeRepairStaleLocalBundledPluginInstallRecords(params);
const repairedManagedNpmOpenClawPeerLinks = await maybeRepairManagedNpmOpenClawPeerLinks(params);
const stalePluginIdsToRemove = [
...new Set([
...(removedStaleManagedNpmBundledPlugins ? staleManagedNpmBundledPluginIds : []),
...removedStaleLocalBundledPluginIds,
]),
];
if (!params.prompter.shouldRepair) {
if (preflight.action === "migrate") {
note(
@@ -338,7 +417,17 @@ export async function maybeRepairPluginRegistryState(
}
if (preflight.action === "migrate") {
const result = await migratePluginRegistryForInstall(migrationParams);
const result = await migratePluginRegistryForInstall({
...migrationParams,
...(stalePluginIdsToRemove.length > 0
? {
installRecords: await loadInstallRecordsWithoutPluginIds(
params,
stalePluginIdsToRemove,
),
}
: {}),
});
if (result.migrated) {
const total = result.current.plugins.length;
const enabled = result.current.plugins.filter((plugin) => plugin.enabled).length;
@@ -353,16 +442,17 @@ export async function maybeRepairPluginRegistryState(
if (
preflight.action === "skip-existing" ||
removedStaleManagedNpmBundledPlugins ||
removedStaleLocalBundledPluginIds.length > 0 ||
repairedManagedNpmOpenClawPeerLinks
) {
const index = await refreshPluginRegistry({
...migrationParams,
reason: "migration",
...(removedStaleManagedNpmBundledPlugins
...(stalePluginIdsToRemove.length > 0
? {
installRecords: await loadInstallRecordsWithoutPluginIds(
params,
staleManagedNpmBundledPluginIds,
stalePluginIdsToRemove,
),
}
: {}),

View File

@@ -293,9 +293,11 @@ export async function migratePluginRegistryForInstall(
const rawConfig = await readMigrationConfig(params);
const config = stripShippedPluginInstallConfigRecords(rawConfig) as OpenClawConfig;
const durableInstallRecords =
params.installRecords ?? (await loadInstalledPluginIndexInstallRecords(params));
const installRecords = {
...extractShippedPluginInstallConfigRecords(rawConfig),
...(await loadInstalledPluginIndexInstallRecords(params)),
...durableInstallRecords,
};
const migrationParams = {
...params,

View File

@@ -0,0 +1,158 @@
import path from "node:path";
import { describe, expect, it } from "vitest";
import type { PluginInstallRecord } from "../config/types.plugins.js";
import type { BundledPluginSource } from "./bundled-sources.js";
import {
listStaleLocalBundledPluginInstallRecords,
pruneStaleLocalBundledPluginInstallRecords,
} from "./stale-local-bundled-plugin-install-records.js";
function bundledSource(pluginId: string, localPath: string): Map<string, BundledPluginSource> {
return new Map([
[
pluginId,
{
pluginId,
localPath,
version: "2026.5.20",
},
],
]);
}
describe("listStaleLocalBundledPluginInstallRecords", () => {
it("lists path install records that point at stale compiled bundled output", () => {
const currentPath = path.join("/opt/openclaw", "dist", "extensions", "discord");
const stalePath = path.join("/tmp/old-openclaw", "dist", "extensions", "discord");
const records: Record<string, PluginInstallRecord> = {
discord: {
source: "path",
installPath: stalePath,
version: "2026.5.4-beta.3",
},
brave: {
source: "npm",
installPath: "/tmp/plugins/brave",
},
};
expect(
listStaleLocalBundledPluginInstallRecords({
installRecords: records,
bundled: bundledSource("discord", currentPath),
}),
).toStrictEqual([
{
pluginId: "discord",
record: records.discord,
recordPathField: "installPath",
stalePath,
bundledPath: currentPath,
},
]);
});
it("does not list the current bundled path", () => {
const currentPath = path.join("/opt/openclaw", "dist", "extensions", "discord");
expect(
listStaleLocalBundledPluginInstallRecords({
installRecords: {
discord: {
source: "path",
installPath: currentPath,
version: "2026.5.4-beta.3",
},
},
bundled: bundledSource("discord", currentPath),
}),
).toStrictEqual([]);
});
it("does not list compiled bundled paths without a stale version", () => {
const currentPath = path.join("/opt/openclaw", "dist", "extensions", "discord");
expect(
listStaleLocalBundledPluginInstallRecords({
installRecords: {
discord: {
source: "path",
installPath: path.join("/tmp/local-openclaw", "dist", "extensions", "discord"),
},
acpx: {
source: "path",
installPath: path.join("/tmp/local-openclaw", "dist", "extensions", "acpx"),
version: "2026.5.20",
},
},
bundled: new Map([
...bundledSource("discord", currentPath),
...bundledSource("acpx", path.join("/opt/openclaw", "dist", "extensions", "acpx")),
]),
}),
).toStrictEqual([]);
});
it("does not list source checkout or arbitrary local plugin paths", () => {
const currentPath = path.join("/opt/openclaw", "dist", "extensions", "discord");
expect(
listStaleLocalBundledPluginInstallRecords({
installRecords: {
discord: {
source: "path",
installPath: path.join("/tmp/openclaw", "extensions", "discord"),
version: "2026.5.4-beta.3",
},
acpx: {
source: "path",
installPath: path.join("/tmp/custom-plugins", "acpx"),
version: "2026.5.4-beta.3",
},
},
bundled: new Map([
...bundledSource("discord", currentPath),
...bundledSource("acpx", path.join("/opt/openclaw", "dist", "extensions", "acpx")),
]),
}),
).toStrictEqual([]);
});
});
describe("pruneStaleLocalBundledPluginInstallRecords", () => {
it("removes only stale local bundled plugin install records", () => {
const currentPath = path.join("/opt/openclaw", "dist", "extensions", "discord");
const stalePath = path.join("/tmp/old-openclaw", "dist", "extensions", "discord");
const records: Record<string, PluginInstallRecord> = {
discord: {
source: "path",
installPath: stalePath,
version: "2026.5.4-beta.3",
},
brave: {
source: "npm",
installPath: "/tmp/plugins/brave",
},
};
expect(
pruneStaleLocalBundledPluginInstallRecords({
installRecords: records,
bundled: bundledSource("discord", currentPath),
}),
).toStrictEqual({
records: {
brave: records.brave,
},
stale: [
{
pluginId: "discord",
record: records.discord,
recordPathField: "installPath",
stalePath,
bundledPath: currentPath,
},
],
});
});
});

View File

@@ -0,0 +1,123 @@
import path from "node:path";
import type { PluginInstallRecord } from "../config/types.plugins.js";
import { resolveUserPath } from "../utils.js";
import { normalizeBundledLookupPath } from "./bundled-load-path-aliases.js";
import { resolveBundledPluginSources, type BundledPluginSource } from "./bundled-sources.js";
export type StaleLocalBundledPluginInstallRecord = {
pluginId: string;
record: PluginInstallRecord;
recordPathField: "installPath" | "sourcePath";
stalePath: string;
bundledPath: string;
};
function normalizePathForCompare(rawPath: string, env?: NodeJS.ProcessEnv): string {
return path.resolve(normalizeBundledLookupPath(resolveUserPath(rawPath, env)));
}
function primaryInstallRecordPath(record: PluginInstallRecord): {
field: "installPath" | "sourcePath";
path: string;
} | null {
if (typeof record.installPath === "string" && record.installPath.trim()) {
return { field: "installPath", path: record.installPath };
}
if (typeof record.sourcePath === "string" && record.sourcePath.trim()) {
return { field: "sourcePath", path: record.sourcePath };
}
return null;
}
function looksLikeCompiledBundledPluginPath(targetPath: string, pluginId: string): boolean {
const segments = normalizeBundledLookupPath(targetPath).split(/[\\/]+/u);
return segments.some((segment, index) => {
return (
(segment === "dist" || segment === "dist-runtime") &&
segments[index + 1] === "extensions" &&
segments[index + 2] === pluginId
);
});
}
function hasStaleBundledVersion(
record: PluginInstallRecord,
bundledSource: BundledPluginSource,
): boolean {
const recordVersion = record.version?.trim();
const bundledVersion = bundledSource.version?.trim();
return Boolean(recordVersion && bundledVersion && recordVersion !== bundledVersion);
}
export function listStaleLocalBundledPluginInstallRecords(params: {
installRecords: Record<string, PluginInstallRecord>;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
bundled?: ReadonlyMap<string, BundledPluginSource>;
}): StaleLocalBundledPluginInstallRecord[] {
const bundled =
params.bundled ??
resolveBundledPluginSources({
workspaceDir: params.workspaceDir,
env: params.env,
});
const stale: StaleLocalBundledPluginInstallRecord[] = [];
for (const [pluginId, record] of Object.entries(params.installRecords).toSorted(
([left], [right]) => left.localeCompare(right),
)) {
if (record.source !== "path") {
continue;
}
const bundledSource = bundled.get(pluginId);
if (!bundledSource?.localPath) {
continue;
}
if (!hasStaleBundledVersion(record, bundledSource)) {
continue;
}
const recordPath = primaryInstallRecordPath(record);
if (!recordPath) {
continue;
}
const stalePath = normalizePathForCompare(recordPath.path, params.env);
const bundledPath = normalizePathForCompare(bundledSource.localPath, params.env);
if (stalePath === bundledPath) {
continue;
}
if (!looksLikeCompiledBundledPluginPath(stalePath, pluginId)) {
continue;
}
stale.push({
pluginId,
record,
recordPathField: recordPath.field,
stalePath,
bundledPath,
});
}
return stale;
}
export function pruneStaleLocalBundledPluginInstallRecords(params: {
installRecords: Record<string, PluginInstallRecord>;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
bundled?: ReadonlyMap<string, BundledPluginSource>;
}): {
records: Record<string, PluginInstallRecord>;
stale: StaleLocalBundledPluginInstallRecord[];
} {
const stale = listStaleLocalBundledPluginInstallRecords(params);
if (stale.length === 0) {
return { records: params.installRecords, stale };
}
const staleIds = new Set(stale.map((record) => record.pluginId));
return {
records: Object.fromEntries(
Object.entries(params.installRecords).filter(([pluginId]) => !staleIds.has(pluginId)),
),
stale,
};
}