From 0588dfe15d8065026111ab9087dbc9db3459600f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 22 Apr 2026 22:10:25 +0100 Subject: [PATCH] fix(config): parse quoted bracket paths --- docs/cli/update.md | 6 ++++++ src/cli/config-cli.test.ts | 35 +++++++++++++++++++++++++++++++++++ src/cli/config-cli.ts | 21 ++++++++++++++++++++- 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/docs/cli/update.md b/docs/cli/update.md index e0adde19814..9da75e59490 100644 --- a/docs/cli/update.md +++ b/docs/cli/update.md @@ -82,6 +82,12 @@ install method aligned: The Gateway core auto-updater (when enabled via config) reuses this same update path. +For package-manager installs, `openclaw update` resolves the target package +version before invoking the package manager. If the installed version exactly +matches the target and no update-channel change needs to be persisted, the +command exits as skipped before package install, plugin sync, completion refresh, +or gateway restart work. + ## Git checkout flow Channels: diff --git a/src/cli/config-cli.test.ts b/src/cli/config-cli.test.ts index 314be8d15eb..00f8dec72a1 100644 --- a/src/cli/config-cli.test.ts +++ b/src/cli/config-cli.test.ts @@ -273,6 +273,41 @@ describe("config cli", () => { }); }); + it("dry-runs nested plugin install updates without dropping sibling fields", 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 runConfigCommand([ + "config", + "set", + 'plugins.installs["openclaw-web-search"].spec', + '"@ollama/openclaw-web-search@0.2.2"', + "--strict-json", + "--dry-run", + ]); + + expect(mockWriteConfigFile).not.toHaveBeenCalled(); + expect(mockError).not.toHaveBeenCalled(); + expect(mockLog).toHaveBeenCalledWith(expect.stringContaining("Dry run successful")); + }); + it("writes agents.defaults.llm.idleTimeoutSeconds without disturbing sibling defaults", async () => { const resolved: OpenClawConfig = { agents: { diff --git a/src/cli/config-cli.ts b/src/cli/config-cli.ts index b08200ca6f7..60f15756a5a 100644 --- a/src/cli/config-cli.ts +++ b/src/cli/config-cli.ts @@ -108,6 +108,25 @@ function isIndexSegment(raw: string): boolean { return /^[0-9]+$/.test(raw); } +function parseBracketPathSegment(raw: string, fullPath: string): string { + const trimmed = raw.trim(); + if (!trimmed) { + throw new Error(`Invalid path (empty "[]"): ${fullPath}`); + } + if (trimmed.startsWith('"') || trimmed.startsWith("'")) { + try { + const parsed = JSON5.parse(trimmed) as unknown; + if (typeof parsed === "string" && parsed.trim()) { + return parsed; + } + } catch (err) { + throw new Error(`Invalid path bracket string (${trimmed}): ${fullPath}`, { cause: err }); + } + throw new Error(`Invalid path bracket string (${trimmed}): ${fullPath}`); + } + return trimmed; +} + function parsePath(raw: string): PathSegment[] { const trimmed = raw.trim(); if (!trimmed) { @@ -147,7 +166,7 @@ function parsePath(raw: string): PathSegment[] { if (!inside) { throw new Error(`Invalid path (empty "[]"): ${raw}`); } - parts.push(inside); + parts.push(parseBracketPathSegment(inside, raw)); i = close + 1; continue; }