Skip to content

Commit bea1084

Browse files
ooiuuiivelvet-shark
authored andcommitted
fix(cli): sync official plugins during update all
1 parent 19707cc commit bea1084

6 files changed

Lines changed: 98 additions & 10 deletions

File tree

docs/cli/plugins.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,13 +399,17 @@ Updates apply to tracked plugin installs in the managed plugin index and tracked
399399
<Accordion title="Resolving plugin id vs npm spec">
400400
When you pass a plugin id, OpenClaw reuses the recorded install spec for that plugin. That means previously stored dist-tags such as `@beta` and exact pinned versions continue to be used on later `update <id>` runs.
401401

402+
That targeted-update rule is different from the bulk `openclaw plugins update --all` maintenance path. Bulk updates still respect ordinary tracked install specs, but trusted official OpenClaw plugin records can sync to the current official catalog target instead of staying on a stale exact official package. Use targeted `update <id>` when you intentionally want to keep an exact or tagged official spec untouched.
403+
402404
For npm installs, you can also pass an explicit npm package spec with a dist-tag or exact version. OpenClaw resolves that package name back to the tracked plugin record, updates that installed plugin, and records the new npm spec for future id-based updates.
403405

404406
Passing the npm package name without a version or tag also resolves back to the tracked plugin record. Use this when a plugin was pinned to an exact version and you want to move it back to the registry's default release line.
405407

406408
</Accordion>
407409
<Accordion title="Beta channel updates">
408-
`openclaw plugins update` reuses the tracked plugin spec unless you pass a new spec. `openclaw update` additionally knows the active OpenClaw update channel: on the beta channel, default-line npm and ClawHub plugin records try `@beta` first. They fall back to the recorded default/latest spec if no plugin beta release exists; npm plugins also fall back when the beta package exists but fails install validation. That fallback is reported as a warning and does not fail the core update. Exact versions and explicit tags stay pinned to that selector.
410+
Targeted `openclaw plugins update <id-or-npm-spec>` reuses the tracked plugin spec unless you pass a new spec. Bulk `openclaw plugins update --all` uses the configured `update.channel` when it syncs trusted official plugin records to the official catalog target, so beta-channel installs can stay on the beta release line instead of being silently normalized to stable/latest.
411+
412+
`openclaw update` also knows the active OpenClaw update channel: on the beta channel, default-line npm and ClawHub plugin records try `@beta` first. They fall back to the recorded default/latest spec if no plugin beta release exists; npm plugins also fall back when the beta package exists but fails install validation. That fallback is reported as a warning and does not fail the core update. Exact versions and explicit tags stay pinned to that selector for targeted updates.
409413

410414
</Accordion>
411415
<Accordion title="Version checks and integrity drift">

docs/plugins/manage-plugins.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ When you pass a plugin id, OpenClaw reuses the tracked install spec. Stored
110110
dist-tags such as `@beta` and exact pinned versions continue to be used on
111111
later `update <plugin-id>` runs.
112112

113+
`openclaw plugins update --all` is the bulk maintenance path. It still respects
114+
ordinary tracked install specs, but trusted official OpenClaw plugin records can
115+
sync to the current official catalog target instead of staying on a stale exact
116+
official package. If `update.channel` is set to `beta`, that bulk official sync
117+
uses the beta-channel context. Use a targeted `update <plugin-id>` when you
118+
intentionally want to keep an exact or tagged official spec untouched.
119+
113120
For npm installs, you can pass an explicit package spec to switch the tracked
114121
record:
115122

src/cli/plugins-cli.update.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -978,6 +978,32 @@ describe("plugins cli update", () => {
978978
const updateParams = expectSingleCallParams(updateNpmInstalledPlugins);
979979
expect(updateParams.pluginIds).toEqual(["codex"]);
980980
expect(updateParams.syncOfficialPluginInstalls).toBeUndefined();
981+
expect(updateParams.updateChannel).toBeUndefined();
982+
expect(updateParams.officialPluginUpdateChannel).toBeUndefined();
983+
});
984+
985+
it("syncs official catalog specs with beta channel context for update --all", async () => {
986+
const config = createTrackedPluginConfig({
987+
pluginId: "codex",
988+
spec: "@openclaw/[email protected]",
989+
resolvedName: "@openclaw/codex",
990+
});
991+
config.update = { channel: "beta" };
992+
loadConfig.mockReturnValue(config);
993+
setInstalledPluginIndexInstallRecords(config.plugins?.installs ?? {});
994+
updateNpmInstalledPlugins.mockResolvedValue({
995+
config,
996+
changed: false,
997+
outcomes: [],
998+
});
999+
1000+
await runPluginsCommand(["plugins", "update", "--all"]);
1001+
1002+
const updateParams = expectSingleCallParams(updateNpmInstalledPlugins);
1003+
expect(updateParams.pluginIds).toEqual(["codex"]);
1004+
expect(updateParams.syncOfficialPluginInstalls).toBe(true);
1005+
expect(updateParams.officialPluginUpdateChannel).toBe("beta");
1006+
expect(updateParams.updateChannel).toBeUndefined();
9811007
});
9821008

9831009
it("writes updated config when updater reports changes", async () => {

src/cli/plugins-update-command.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { extractShippedPluginInstallConfigRecords } from "../config/plugin-insta
1212
import type { OpenClawConfig } from "../config/types.openclaw.js";
1313
import type { PluginInstallRecord } from "../config/types.plugins.js";
1414
import { updateNpmInstalledHookPacks } from "../hooks/update.js";
15+
import { normalizeUpdateChannel } from "../infra/update-channels.js";
1516
import {
1617
loadInstalledPluginIndexInstallRecords,
1718
withoutPluginInstallRecords,
@@ -260,6 +261,10 @@ export async function runPluginUpdateCommand(params: {
260261
pluginIds: pluginSelection.pluginIds,
261262
specOverrides: pluginSelection.specOverrides,
262263
dryRun: params.opts.dryRun,
264+
officialPluginUpdateChannel: params.opts.all
265+
? (normalizeUpdateChannel(cfg.update?.channel) ?? undefined)
266+
: undefined,
267+
syncOfficialPluginInstalls: params.opts.all ? true : undefined,
263268
dangerouslyForceUnsafeInstall: params.opts.dangerouslyForceUnsafeInstall,
264269
logger,
265270
onIntegrityDrift: async (drift) => {

src/plugins/update.test.ts

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@ const tempDirs: string[] = [];
4242

4343
vi.mock("./install.js", () => ({
4444
installPluginFromNpmSpec: (...args: unknown[]) => installPluginFromNpmSpecMock(...args),
45-
resolvePluginInstallDir: (pluginId: string, extensionsDir = "/tmp") =>
46-
`${extensionsDir}/${pluginId}`,
45+
resolvePluginInstallDir: (pluginId: string, extensionsDir = "/tmp") => {
46+
const separator = process.platform === "win32" ? "\\" : "/";
47+
return `${extensionsDir.replace(/[\\/]+$/, "")}${separator}${pluginId}`;
48+
},
4749
PLUGIN_INSTALL_ERROR_CODE: {
4850
NPM_PACKAGE_NOT_FOUND: "npm_package_not_found",
4951
},
@@ -1079,6 +1081,45 @@ describe("updateNpmInstalledPlugins", () => {
10791081
);
10801082
});
10811083

1084+
it("does not apply official beta-channel sync to third-party npm specs", async () => {
1085+
const installPath = createInstalledPackageDir({
1086+
name: "@martian-engineering/lossless-claw",
1087+
version: "0.9.0",
1088+
});
1089+
mockNpmViewMetadata({
1090+
name: "@martian-engineering/lossless-claw",
1091+
version: "0.9.1",
1092+
});
1093+
installPluginFromNpmSpecMock.mockResolvedValue(
1094+
createSuccessfulNpmUpdateResult({
1095+
pluginId: "lossless-claw",
1096+
targetDir: installPath,
1097+
version: "0.9.1",
1098+
npmResolution: {
1099+
name: "@martian-engineering/lossless-claw",
1100+
version: "0.9.1",
1101+
resolvedSpec: "@martian-engineering/[email protected]",
1102+
},
1103+
}),
1104+
);
1105+
1106+
await updateNpmInstalledPlugins({
1107+
config: createNpmInstallConfig({
1108+
pluginId: "lossless-claw",
1109+
spec: "@martian-engineering/lossless-claw",
1110+
installPath,
1111+
resolvedName: "@martian-engineering/lossless-claw",
1112+
resolvedSpec: "@martian-engineering/[email protected]",
1113+
resolvedVersion: "0.9.0",
1114+
}),
1115+
pluginIds: ["lossless-claw"],
1116+
syncOfficialPluginInstalls: true,
1117+
officialPluginUpdateChannel: "beta",
1118+
});
1119+
1120+
expect(npmInstallCall()?.spec).toBe("@martian-engineering/lossless-claw");
1121+
});
1122+
10821123
it("does not skip trusted official default updates when latest resolves to the installed prerelease", async () => {
10831124
const installPath = createInstalledPackageDir({
10841125
name: "@openclaw/acpx",
@@ -3906,10 +3947,11 @@ describe("updateNpmInstalledPlugins", () => {
39063947
pluginIds: ["demo"],
39073948
});
39083949

3909-
expect(npmInstallCall()?.extensionsDir).toBe(extensionsDir);
3910-
expect(clawHubInstallCall()?.extensionsDir).toBe(extensionsDir);
3911-
expect(marketplaceInstallCall()?.extensionsDir).toBe(extensionsDir);
3912-
expect(gitInstallCall()?.extensionsDir).toBe(extensionsDir);
3950+
const expectedExtensionsDir = path.resolve(extensionsDir);
3951+
expect(npmInstallCall()?.extensionsDir).toBe(expectedExtensionsDir);
3952+
expect(clawHubInstallCall()?.extensionsDir).toBe(expectedExtensionsDir);
3953+
expect(marketplaceInstallCall()?.extensionsDir).toBe(expectedExtensionsDir);
3954+
expect(gitInstallCall()?.extensionsDir).toBe(expectedExtensionsDir);
39133955
});
39143956
});
39153957

src/plugins/update.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1238,6 +1238,7 @@ export async function updateNpmInstalledPlugins(params: {
12381238
timeoutMs?: number;
12391239
dryRun?: boolean;
12401240
updateChannel?: UpdateChannel;
1241+
officialPluginUpdateChannel?: UpdateChannel;
12411242
dangerouslyForceUnsafeInstall?: boolean;
12421243
specOverrides?: Record<string, string>;
12431244
onIntegrityDrift?: (params: PluginUpdateIntegrityDriftParams) => boolean | Promise<boolean>;
@@ -1314,6 +1315,7 @@ export async function updateNpmInstalledPlugins(params: {
13141315
const officialClawHubSpec = params.syncOfficialPluginInstalls
13151316
? resolveTrustedSourceLinkedOfficialClawHubSpec({ pluginId, record })
13161317
: undefined;
1318+
const officialSyncUpdateChannel = params.officialPluginUpdateChannel ?? params.updateChannel;
13171319

13181320
if (normalizedPluginConfig) {
13191321
const enableState = resolveEffectiveEnableState({
@@ -1347,15 +1349,15 @@ export async function updateNpmInstalledPlugins(params: {
13471349
record,
13481350
specOverride: params.specOverrides?.[pluginId],
13491351
officialSpecOverride: officialNpmSpec,
1350-
updateChannel: params.updateChannel,
1352+
updateChannel: officialNpmSpec ? officialSyncUpdateChannel : params.updateChannel,
13511353
})
13521354
: undefined;
13531355
const clawhubSpecs =
13541356
record.source === "clawhub"
13551357
? resolveClawHubUpdateSpecs({
13561358
record,
13571359
officialSpecOverride: officialClawHubSpec,
1358-
updateChannel: params.updateChannel,
1360+
updateChannel: officialClawHubSpec ? officialSyncUpdateChannel : params.updateChannel,
13591361
})
13601362
: undefined;
13611363
const effectiveSpec =
@@ -1377,7 +1379,9 @@ export async function updateNpmInstalledPlugins(params: {
13771379
record,
13781380
effectiveClawHubSpec: effectiveSpec,
13791381
recordClawHubSpec: recordSpec,
1380-
updateChannel: params.updateChannel,
1382+
updateChannel: params.syncOfficialPluginInstalls
1383+
? officialSyncUpdateChannel
1384+
: params.updateChannel,
13811385
})
13821386
: null;
13831387
let officialNpmFallbackInstallSpec = officialNpmFallbackSpecs?.installSpec;

0 commit comments

Comments
 (0)