Skip to content

Commit da1e143

Browse files
committed
fix(doctor): prune stale plugin lock entries
1 parent 43bdb88 commit da1e143

3 files changed

Lines changed: 127 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ Docs: https://docs.openclaw.ai
6161

6262
### Fixes
6363

64+
- Doctor/plugins: remove stale managed npm plugin shadow entries from the managed package lock as well as `package.json` and `node_modules`, so future npm operations do not keep referencing repaired bundled-plugin shadows. Thanks @vincentkoc.
6465
- Plugins/runtime state: keep the key being registered when namespace eviction runs in the same millisecond as existing entries, so `register` and `registerIfAbsent` do not report success while evicting their own fresh value. Thanks @vincentkoc.
6566
- Control UI/Talk: make failed Talk startup errors dismissable and clear the stale Talk error state when dismissed, so missing realtime voice provider configuration does not leave a permanent chat banner. Fixes #77071. Thanks @ijoshdavis.
6667
- Control UI/Talk: stop and clear failed realtime Talk sessions when dismissing runtime error banners, so the next Talk click starts a fresh session instead of only stopping the stale one. Thanks @vincentkoc.

src/commands/doctor-plugin-registry.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ function createManagedNpmPlugin(params: {
104104
id: string;
105105
packageName: string;
106106
version: string;
107+
packageLock?: boolean;
107108
}) {
108109
const npmRoot = path.join(params.stateDir, "npm");
109110
const packageDir = path.join(npmRoot, "node_modules", params.packageName);
@@ -117,6 +118,37 @@ function createManagedNpmPlugin(params: {
117118
}),
118119
"utf8",
119120
);
121+
if (params.packageLock) {
122+
fs.writeFileSync(
123+
path.join(npmRoot, "package-lock.json"),
124+
JSON.stringify({
125+
lockfileVersion: 3,
126+
packages: {
127+
"": {
128+
dependencies: {
129+
[params.packageName]: params.version,
130+
"other-plugin": "1.0.0",
131+
},
132+
},
133+
[`node_modules/${params.packageName}`]: {
134+
version: params.version,
135+
},
136+
"node_modules/other-plugin": {
137+
version: "1.0.0",
138+
},
139+
},
140+
dependencies: {
141+
[params.packageName]: {
142+
version: params.version,
143+
},
144+
"other-plugin": {
145+
version: "1.0.0",
146+
},
147+
},
148+
}),
149+
"utf8",
150+
);
151+
}
120152
fs.writeFileSync(
121153
path.join(packageDir, "package.json"),
122154
JSON.stringify({
@@ -301,4 +333,51 @@ describe("maybeRepairPluginRegistryState", () => {
301333
"Removed stale managed npm plugin package",
302334
);
303335
});
336+
337+
it("removes stale managed npm packages from the package lock during repair", async () => {
338+
const stateDir = makeTempDir();
339+
const bundledDir = path.join(stateDir, "bundled", "google-meet");
340+
fs.mkdirSync(bundledDir, { recursive: true });
341+
createManagedNpmPlugin({
342+
stateDir,
343+
id: "google-meet",
344+
packageName: "@openclaw/google-meet",
345+
version: "2026.5.2",
346+
packageLock: true,
347+
});
348+
await writePersistedInstalledPluginIndex(createCurrentIndex(), { stateDir });
349+
350+
await maybeRepairPluginRegistryState({
351+
stateDir,
352+
candidates: [
353+
createBundledCandidate({
354+
rootDir: bundledDir,
355+
id: "google-meet",
356+
packageName: "@openclaw/google-meet",
357+
version: "2026.5.3",
358+
}),
359+
],
360+
env: hermeticEnv(),
361+
config: {
362+
plugins: {
363+
allow: ["google-meet"],
364+
entries: {
365+
"google-meet": {
366+
enabled: true,
367+
config: {},
368+
},
369+
},
370+
},
371+
},
372+
prompter: { shouldRepair: true },
373+
});
374+
375+
const packageLock = JSON.parse(
376+
fs.readFileSync(path.join(stateDir, "npm", "package-lock.json"), "utf8"),
377+
);
378+
expect(packageLock.packages[""].dependencies).toEqual({ "other-plugin": "1.0.0" });
379+
expect(packageLock.packages).not.toHaveProperty("node_modules/@openclaw/google-meet");
380+
expect(packageLock.dependencies).not.toHaveProperty("@openclaw/google-meet");
381+
expect(packageLock.dependencies).toHaveProperty("other-plugin");
382+
});
304383
});

src/commands/doctor-plugin-registry.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ function readStringMap(value: unknown): Record<string, string> {
5858
return result;
5959
}
6060

61+
function deleteObjectKey(record: Record<string, unknown>, key: string): boolean {
62+
if (!Object.prototype.hasOwnProperty.call(record, key)) {
63+
return false;
64+
}
65+
delete record[key];
66+
return true;
67+
}
68+
6169
function readPackageVersion(packageDir: string): string | undefined {
6270
const packageJson = readJsonObject(path.join(packageDir, "package.json"));
6371
const version = packageJson?.version;
@@ -137,6 +145,7 @@ function removeManagedNpmDependency(params: {
137145
dependencies,
138146
};
139147
saveJsonFile(npmPackageJsonPath, nextPackageJson);
148+
removeManagedNpmPackageLockDependency(params);
140149
fs.rmSync(params.packageDir, { recursive: true, force: true });
141150
const scopeDir = path.dirname(params.packageDir);
142151
if (path.basename(path.dirname(scopeDir)) === "node_modules") {
@@ -148,6 +157,44 @@ function removeManagedNpmDependency(params: {
148157
}
149158
}
150159

160+
function removeManagedNpmPackageLockDependency(params: {
161+
npmRoot: string;
162+
packageName: string;
163+
}): void {
164+
const packageLockPath = path.join(params.npmRoot, "package-lock.json");
165+
const packageLock = readJsonObject(packageLockPath);
166+
if (!packageLock) {
167+
return;
168+
}
169+
170+
let changed = false;
171+
const packages = packageLock.packages;
172+
if (isRecord(packages)) {
173+
const rootPackage = packages[""];
174+
if (isRecord(rootPackage)) {
175+
const rootDependencies = readStringMap(rootPackage.dependencies);
176+
if (deleteObjectKey(rootDependencies, params.packageName)) {
177+
changed = true;
178+
if (Object.keys(rootDependencies).length === 0) {
179+
delete rootPackage.dependencies;
180+
} else {
181+
rootPackage.dependencies = rootDependencies;
182+
}
183+
}
184+
}
185+
changed = deleteObjectKey(packages, `node_modules/${params.packageName}`) || changed;
186+
}
187+
188+
const dependencies = packageLock.dependencies;
189+
if (isRecord(dependencies)) {
190+
changed = deleteObjectKey(dependencies, params.packageName) || changed;
191+
}
192+
193+
if (changed) {
194+
saveJsonFile(packageLockPath, packageLock);
195+
}
196+
}
197+
151198
function maybeRepairStaleManagedNpmBundledPlugins(
152199
params: PluginRegistryDoctorRepairParams,
153200
): boolean {

0 commit comments

Comments
 (0)