mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:20:42 +00:00
fix: keep plugin uninstall on metadata path
This commit is contained in:
@@ -37,6 +37,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/startup: precompute bundled runtime mirror fingerprints before taking the mirror lock and keep Docker bundled plugin runtime deps/mirrors in a Docker-managed volume instead of the Windows/WSL config bind mount, so cold starts avoid slow host-volume mirror writes. Fixes #73339. Thanks @1yihui.
|
||||
- Channels/LINE: persist inbound image, video, audio, and file downloads in `~/.openclaw/media/inbound/` instead of temporary files so agents can still read LINE media after `/tmp` cleanup. Fixes #73370. Thanks @hijirii and @wenxu007.
|
||||
- CLI/plugins: keep bundled plugin installs out of `plugins.load.paths` while preserving install records, so install/inspect/doctor loops no longer warn about the current bundled plugin directory. Thanks @vincentkoc.
|
||||
- CLI/plugins: remove managed copied-path plugin directories during uninstall and plan uninstall from metadata instead of runtime-loading plugins, so plugin lifecycle commands avoid unnecessary bundled runtime-deps work. Thanks @shakkernerd.
|
||||
- Cron tool: infer the creating session's agentId for `cron.add` jobs when `agentId` is omitted or passed as undefined, keeping scheduled agentTurn jobs routed to the session agent; #40571 identified the guard bug and supplied the focused regression coverage. Thanks @ChanningYul.
|
||||
- Cron/Telegram: add `--thread-id` to `openclaw cron add` and `openclaw cron edit`, preserving Telegram forum topic delivery targets across scheduled announcements. Carries forward #51581, #60373, and #60890. Thanks @ChunHao-dev.
|
||||
- Cron/Telegram: preserve session-derived Telegram topic thread IDs when isolated cron delivery explicitly targets the parent chat, keeping bare chat targets in the active forum topic without leaking stale topics to other chats. Carries forward #64708. Thanks @addelh.
|
||||
|
||||
@@ -570,7 +570,7 @@ export function registerPluginsCli(program: Command) {
|
||||
withoutPluginInstallRecords,
|
||||
withPluginInstallRecords,
|
||||
} = await import("../plugins/installed-plugin-index-records.js");
|
||||
const { buildPluginDiagnosticsReport } = await import("../plugins/status.js");
|
||||
const { buildPluginSnapshotReport } = await import("../plugins/status.js");
|
||||
const {
|
||||
applyPluginUninstallDirectoryRemoval,
|
||||
formatUninstallActionLabels,
|
||||
@@ -589,7 +589,7 @@ export function registerPluginsCli(program: Command) {
|
||||
const sourceConfig = (snapshot.sourceConfig ?? snapshot.config) as OpenClawConfig;
|
||||
const installRecords = await loadInstalledPluginIndexInstallRecords();
|
||||
const cfg = withPluginInstallRecords(sourceConfig, installRecords);
|
||||
const report = buildPluginDiagnosticsReport({ config: cfg });
|
||||
const report = buildPluginSnapshotReport({ config: cfg });
|
||||
const extensionsDir = path.join(resolveStateDir(process.env, os.homedir), "extensions");
|
||||
const keepFiles = Boolean(opts.keepFiles || opts.keepConfig);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
applyPluginUninstallDirectoryRemoval,
|
||||
buildPluginDiagnosticsReport,
|
||||
buildPluginSnapshotReport,
|
||||
loadConfig,
|
||||
planPluginUninstall,
|
||||
promptYesNo,
|
||||
@@ -46,7 +47,7 @@ describe("plugins cli uninstall", () => {
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig);
|
||||
buildPluginDiagnosticsReport.mockReturnValue({
|
||||
buildPluginSnapshotReport.mockReturnValue({
|
||||
plugins: [{ id: "alpha", name: "alpha" }],
|
||||
diagnostics: [],
|
||||
});
|
||||
@@ -68,6 +69,8 @@ describe("plugins cli uninstall", () => {
|
||||
|
||||
await runPluginsCommand(["plugins", "uninstall", "alpha", "--dry-run"]);
|
||||
|
||||
expect(buildPluginSnapshotReport).toHaveBeenCalled();
|
||||
expect(buildPluginDiagnosticsReport).not.toHaveBeenCalled();
|
||||
expect(planPluginUninstall).toHaveBeenCalled();
|
||||
expect(writeConfigFile).not.toHaveBeenCalled();
|
||||
expect(refreshPluginRegistry).not.toHaveBeenCalled();
|
||||
@@ -99,7 +102,7 @@ describe("plugins cli uninstall", () => {
|
||||
|
||||
loadConfig.mockReturnValue(baseConfig);
|
||||
setInstalledPluginIndexInstallRecords(baseConfig.plugins?.installs ?? {});
|
||||
buildPluginDiagnosticsReport.mockReturnValue({
|
||||
buildPluginSnapshotReport.mockReturnValue({
|
||||
plugins: [{ id: "alpha", name: "alpha" }],
|
||||
diagnostics: [],
|
||||
});
|
||||
@@ -170,7 +173,7 @@ describe("plugins cli uninstall", () => {
|
||||
|
||||
loadConfig.mockReturnValue(baseConfig);
|
||||
setInstalledPluginIndexInstallRecords(installRecords);
|
||||
buildPluginDiagnosticsReport.mockReturnValue({
|
||||
buildPluginSnapshotReport.mockReturnValue({
|
||||
plugins: [{ id: "alpha", name: "alpha" }],
|
||||
diagnostics: [],
|
||||
});
|
||||
@@ -229,7 +232,7 @@ describe("plugins cli uninstall", () => {
|
||||
|
||||
loadConfig.mockReturnValue(baseConfig);
|
||||
setInstalledPluginIndexInstallRecords(installRecords);
|
||||
buildPluginDiagnosticsReport.mockReturnValue({
|
||||
buildPluginSnapshotReport.mockReturnValue({
|
||||
plugins: [{ id: "alpha", name: "alpha" }],
|
||||
diagnostics: [],
|
||||
});
|
||||
@@ -275,7 +278,7 @@ describe("plugins cli uninstall", () => {
|
||||
installs: {},
|
||||
},
|
||||
} as OpenClawConfig);
|
||||
buildPluginDiagnosticsReport.mockReturnValue({
|
||||
buildPluginSnapshotReport.mockReturnValue({
|
||||
plugins: [{ id: "alpha", name: "alpha" }],
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
@@ -761,6 +761,32 @@ describe("uninstallPlugin", () => {
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "deletes managed directory for copied path installs",
|
||||
setup: async (baseDir: string) => {
|
||||
const sourceDir = await createPluginDirFixture(path.join(baseDir, "source"));
|
||||
const extensionsDir = path.join(baseDir, "extensions");
|
||||
const installDir = resolvePluginInstallDir("my-plugin", extensionsDir);
|
||||
await fs.mkdir(installDir, { recursive: true });
|
||||
await fs.writeFile(path.join(installDir, "index.js"), "// copied plugin");
|
||||
return {
|
||||
config: createPluginConfig({
|
||||
entries: createSinglePluginEntries(),
|
||||
installs: {
|
||||
"my-plugin": createPathInstallRecord(installDir, sourceDir),
|
||||
},
|
||||
}),
|
||||
deleteFiles: true,
|
||||
extensionsDir,
|
||||
accessPath: installDir,
|
||||
preservedPath: sourceDir,
|
||||
expectedAccess: "missing" as const,
|
||||
expectedActions: {
|
||||
directory: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "does not delete directory when deleteFiles is false",
|
||||
setup: async (baseDir: string) => {
|
||||
@@ -793,12 +819,16 @@ describe("uninstallPlugin", () => {
|
||||
config: params.config,
|
||||
pluginId: "my-plugin",
|
||||
deleteFiles: params.deleteFiles,
|
||||
extensionsDir: "extensionsDir" in params ? params.extensionsDir : undefined,
|
||||
});
|
||||
|
||||
expectSuccessfulUninstallActions(result, params.expectedActions);
|
||||
if ("accessPath" in params && "expectedAccess" in params) {
|
||||
await expectPathAccessState(params.accessPath, params.expectedAccess);
|
||||
}
|
||||
if ("preservedPath" in params) {
|
||||
await expectPathAccessState(params.preservedPath, "exists");
|
||||
}
|
||||
});
|
||||
|
||||
it("returns a warning when directory deletion fails unexpectedly", async () => {
|
||||
@@ -892,6 +922,24 @@ describe("resolveUninstallDirectoryTarget", () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("returns managed install path for copied path installs", () => {
|
||||
const extensionsDir = path.join(os.tmpdir(), "openclaw-uninstall-safe");
|
||||
const installPath = resolvePluginInstallDir("my-plugin", extensionsDir);
|
||||
|
||||
expect(
|
||||
resolveUninstallDirectoryTarget({
|
||||
pluginId: "my-plugin",
|
||||
hasInstall: true,
|
||||
installRecord: {
|
||||
source: "path",
|
||||
sourcePath: "/tmp/source-plugin",
|
||||
installPath,
|
||||
},
|
||||
extensionsDir,
|
||||
}),
|
||||
).toBe(installPath);
|
||||
});
|
||||
|
||||
it("falls back to default path when configured installPath is untrusted", () => {
|
||||
const extensionsDir = path.join(os.tmpdir(), "openclaw-uninstall-safe");
|
||||
const target = resolveUninstallDirectoryTarget({
|
||||
|
||||
@@ -110,7 +110,7 @@ export function resolveUninstallDirectoryTarget(params: {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (params.installRecord?.source === "path") {
|
||||
if (isLinkedPathInstallRecord(params.installRecord)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -167,6 +167,19 @@ function resolveRecordedManagedInstallPath(params: {
|
||||
}
|
||||
}
|
||||
|
||||
function isLinkedPathInstallRecord(installRecord: PluginInstallRecord | undefined): boolean {
|
||||
if (installRecord?.source !== "path") {
|
||||
return false;
|
||||
}
|
||||
if (!installRecord.sourcePath || !installRecord.installPath) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
resolveComparablePath(installRecord.sourcePath) ===
|
||||
resolveComparablePath(installRecord.installPath)
|
||||
);
|
||||
}
|
||||
|
||||
const SHARED_CHANNEL_CONFIG_KEYS = new Set(["defaults", "modelByChannel"]);
|
||||
|
||||
/**
|
||||
@@ -369,7 +382,8 @@ export type UninstallPluginParams = {
|
||||
|
||||
/**
|
||||
* Plan a plugin uninstall by removing it from config and resolving a safe file-removal target.
|
||||
* Linked plugins (source === "path") never have their source directory deleted.
|
||||
* Linked path plugins never have their source directory deleted. Copied path installs still remove
|
||||
* their managed install directory.
|
||||
*/
|
||||
export function planPluginUninstall(params: UninstallPluginParams): PluginUninstallPlanResult {
|
||||
const { config, pluginId, channelIds, deleteFiles = true, extensionsDir } = params;
|
||||
@@ -383,7 +397,7 @@ export function planPluginUninstall(params: UninstallPluginParams): PluginUninst
|
||||
}
|
||||
|
||||
const installRecord = config.plugins?.installs?.[pluginId];
|
||||
const isLinked = installRecord?.source === "path";
|
||||
const isLinked = isLinkedPathInstallRecord(installRecord);
|
||||
|
||||
// Remove from config
|
||||
const { config: newConfig, actions: configActions } = removePluginFromConfig(config, pluginId, {
|
||||
|
||||
Reference in New Issue
Block a user