From df7348e58656ba55cdc32e71588e3603d4e688e6 Mon Sep 17 00:00:00 2001 From: Shakker Date: Sat, 25 Apr 2026 23:28:57 +0100 Subject: [PATCH] fix: guide config users to plugin commands --- src/cli/config-cli.test.ts | 27 ++++++--------------------- src/cli/config-cli.ts | 23 +++++++++++++++++++++++ 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/cli/config-cli.test.ts b/src/cli/config-cli.test.ts index 137121ab89f..7467df0e798 100644 --- a/src/cli/config-cli.test.ts +++ b/src/cli/config-cli.test.ts @@ -274,26 +274,6 @@ describe("config cli", () => { }); it("rejects plugin install record config updates", async () => { - const resolved = { - plugins: { - installs: { - "openclaw-web-search": { - source: "npm", - spec: "@ollama/openclaw-web-search", - installPath: "/tmp/openclaw-web-search", - version: "0.2.2", - resolvedName: "@ollama/openclaw-web-search", - resolvedVersion: "0.2.2", - resolvedSpec: "@ollama/openclaw-web-search@0.2.2", - integrity: "sha512-test", - resolvedAt: "2026-04-22T10:33:58.083Z", - installedAt: "2026-04-22T10:33:58.240Z", - }, - }, - }, - } as unknown as OpenClawConfig; - setSnapshot(resolved, resolved); - await expect( runConfigCommand([ "config", @@ -306,7 +286,12 @@ describe("config cli", () => { ).rejects.toThrow("__exit__:1"); expect(mockWriteConfigFile).not.toHaveBeenCalled(); - expect(mockError).toHaveBeenCalledWith(expect.stringContaining("Unrecognized key")); + expect(mockError).toHaveBeenCalledWith( + expect.stringContaining("openclaw plugins install "), + ); + expect(mockError).toHaveBeenCalledWith( + expect.stringContaining("openclaw plugins update "), + ); }); it("rejects protected model map replacement unless explicitly requested", async () => { diff --git a/src/cli/config-cli.ts b/src/cli/config-cli.ts index 62d68a28d54..16e786ca0d2 100644 --- a/src/cli/config-cli.ts +++ b/src/cli/config-cli.ts @@ -75,6 +75,7 @@ type ConfigSetOperation = { const GATEWAY_AUTH_MODE_PATH: PathSegment[] = ["gateway", "auth", "mode"]; const SECRET_PROVIDER_PATH_PREFIX: PathSegment[] = ["secrets", "providers"]; +const PLUGIN_INSTALL_RECORD_PATH_PREFIX: PathSegment[] = ["plugins", "installs"]; const CONFIG_SET_EXAMPLE_VALUE = formatCliCommand( "openclaw config set gateway.port 19001 --strict-json", ); @@ -1088,6 +1089,21 @@ function selectDryRunRefsForResolution(params: { refs: SecretRef[]; allowExecInD return { refsToResolve, skippedExecRefs }; } +function pathStartsWith(path: readonly PathSegment[], prefix: readonly PathSegment[]): boolean { + return prefix.every((segment, index) => path[index] === segment); +} + +function formatPluginInstallConfigSetError(): string { + return [ + "plugins.installs is managed by the plugin index and cannot be edited with config set.", + "", + "Use plugin commands instead:", + ` ${formatCliCommand("openclaw plugins install ")}`, + ` ${formatCliCommand("openclaw plugins update ")}`, + ` ${formatCliCommand("openclaw plugins uninstall ")}`, + ].join("\n"); +} + function collectDryRunSchemaErrors(params: { config: OpenClawConfig; operations: ReadonlyArray; @@ -1192,6 +1208,13 @@ export async function runConfigSet(opts: { value: opts.value, opts: opts.cliOptions, }); + if ( + operations.some((operation) => + pathStartsWith(operation.requestedPath, PLUGIN_INSTALL_RECORD_PATH_PREFIX), + ) + ) { + throw new Error(formatPluginInstallConfigSetError()); + } const snapshot = await loadValidConfig(runtime); // Use snapshot.resolved (config after $include and ${ENV} resolution, but BEFORE runtime defaults) // instead of snapshot.config (runtime-merged with defaults).