mirror of https://github.com/openclaw/openclaw.git
feat!: prefer clawhub plugin installs before npm
This commit is contained in:
parent
13c239039a
commit
8d9686bd0f
|
|
@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
|
|||
|
||||
### Changes
|
||||
|
||||
- Breaking/Plugins: bare `openclaw plugins install <package>` now prefers ClawHub before npm for npm-safe names, and only falls back to npm when ClawHub does not have that package or version.
|
||||
- ClawHub/install: add native `openclaw skills search|install|update` flows plus `openclaw plugins install clawhub:<package>` with tracked update metadata, gateway skill-install/update support for ClawHub-backed requests, and regression coverage/docs for the new source path.
|
||||
- Models/Anthropic Vertex: add core `anthropic-vertex` provider support for Claude via Google Vertex AI, including GCP auth/discovery and main run-path routing. (#43356) Thanks @sallyom and @yossiovadia.
|
||||
- Commands/btw: add `/btw` side questions for quick tool-less answers about the current session without changing future session context, with dismissible in-session TUI answers and explicit BTW replies on external channels. (#45444) Thanks @ngutman.
|
||||
|
|
|
|||
|
|
@ -79,6 +79,13 @@ openclaw plugins install clawhub:openclaw-codex-app-server
|
|||
openclaw plugins install clawhub:openclaw-codex-app-server@1.2.3
|
||||
```
|
||||
|
||||
OpenClaw now also prefers ClawHub for bare npm-safe plugin specs. It only falls
|
||||
back to npm if ClawHub does not have that package or version:
|
||||
|
||||
```bash
|
||||
openclaw plugins install openclaw-codex-app-server
|
||||
```
|
||||
|
||||
OpenClaw downloads the package archive from ClawHub, checks the advertised
|
||||
plugin API / minimum gateway compatibility, then installs it through the normal
|
||||
archive path. Recorded installs keep their ClawHub source metadata for later
|
||||
|
|
|
|||
|
|
@ -35,6 +35,12 @@ openclaw plugins install clawhub:<package>
|
|||
openclaw plugins update --all
|
||||
```
|
||||
|
||||
Bare npm-safe plugin specs are also tried against ClawHub before npm:
|
||||
|
||||
```bash
|
||||
openclaw plugins install openclaw-codex-app-server
|
||||
```
|
||||
|
||||
Native `openclaw` commands install into your active workspace and persist source
|
||||
metadata so later `update` calls can stay on ClawHub.
|
||||
|
||||
|
|
|
|||
|
|
@ -340,6 +340,137 @@ describe("plugins cli", () => {
|
|||
expect(installPluginFromNpmSpec).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("prefers ClawHub before npm for bare plugin specs", async () => {
|
||||
const cfg = {
|
||||
plugins: {
|
||||
entries: {},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const enabledCfg = {
|
||||
plugins: {
|
||||
entries: {
|
||||
demo: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const installedCfg = {
|
||||
...enabledCfg,
|
||||
plugins: {
|
||||
...enabledCfg.plugins,
|
||||
installs: {
|
||||
demo: {
|
||||
source: "clawhub",
|
||||
spec: "clawhub:demo@1.2.3",
|
||||
installPath: "/tmp/openclaw-state/extensions/demo",
|
||||
clawhubPackage: "demo",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
loadConfig.mockReturnValue(cfg);
|
||||
installPluginFromClawHub.mockResolvedValue({
|
||||
ok: true,
|
||||
pluginId: "demo",
|
||||
targetDir: "/tmp/openclaw-state/extensions/demo",
|
||||
version: "1.2.3",
|
||||
packageName: "demo",
|
||||
clawhub: {
|
||||
source: "clawhub",
|
||||
clawhubUrl: "https://clawhub.ai",
|
||||
clawhubPackage: "demo",
|
||||
clawhubFamily: "code-plugin",
|
||||
clawhubChannel: "community",
|
||||
version: "1.2.3",
|
||||
integrity: "sha256-abc",
|
||||
resolvedAt: "2026-03-22T00:00:00.000Z",
|
||||
},
|
||||
});
|
||||
enablePluginInConfig.mockReturnValue({ config: enabledCfg });
|
||||
recordPluginInstall.mockReturnValue(installedCfg);
|
||||
applyExclusiveSlotSelection.mockReturnValue({
|
||||
config: installedCfg,
|
||||
warnings: [],
|
||||
});
|
||||
|
||||
await runCommand(["plugins", "install", "demo"]);
|
||||
|
||||
expect(installPluginFromClawHub).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "clawhub:demo",
|
||||
}),
|
||||
);
|
||||
expect(installPluginFromNpmSpec).not.toHaveBeenCalled();
|
||||
expect(writeConfigFile).toHaveBeenCalledWith(installedCfg);
|
||||
});
|
||||
|
||||
it("falls back to npm when ClawHub does not have the package", async () => {
|
||||
const cfg = {
|
||||
plugins: {
|
||||
entries: {},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const enabledCfg = {
|
||||
plugins: {
|
||||
entries: {
|
||||
demo: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
loadConfig.mockReturnValue(cfg);
|
||||
installPluginFromClawHub.mockResolvedValue({
|
||||
ok: false,
|
||||
error: "ClawHub /api/v1/packages/demo failed (404): Package not found",
|
||||
});
|
||||
installPluginFromNpmSpec.mockResolvedValue({
|
||||
ok: true,
|
||||
pluginId: "demo",
|
||||
targetDir: "/tmp/openclaw-state/extensions/demo",
|
||||
version: "1.2.3",
|
||||
npmResolution: {
|
||||
packageName: "demo",
|
||||
resolvedVersion: "1.2.3",
|
||||
tarballUrl: "https://registry.npmjs.org/demo/-/demo-1.2.3.tgz",
|
||||
},
|
||||
});
|
||||
enablePluginInConfig.mockReturnValue({ config: enabledCfg });
|
||||
recordPluginInstall.mockReturnValue(enabledCfg);
|
||||
applyExclusiveSlotSelection.mockReturnValue({
|
||||
config: enabledCfg,
|
||||
warnings: [],
|
||||
});
|
||||
|
||||
await runCommand(["plugins", "install", "demo"]);
|
||||
|
||||
expect(installPluginFromClawHub).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "clawhub:demo",
|
||||
}),
|
||||
);
|
||||
expect(installPluginFromNpmSpec).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "demo",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not fall back to npm when ClawHub rejects a real package", async () => {
|
||||
installPluginFromClawHub.mockResolvedValue({
|
||||
ok: false,
|
||||
error: 'Use "openclaw skills install demo" instead.',
|
||||
});
|
||||
|
||||
await expect(runCommand(["plugins", "install", "demo"])).rejects.toThrow("__exit__:1");
|
||||
|
||||
expect(installPluginFromNpmSpec).not.toHaveBeenCalled();
|
||||
expect(runtimeErrors.at(-1)).toContain('Use "openclaw skills install demo" instead.');
|
||||
});
|
||||
|
||||
it("shows uninstall dry-run preview without mutating config", async () => {
|
||||
loadConfig.mockReturnValue({
|
||||
plugins: {
|
||||
|
|
|
|||
|
|
@ -289,6 +289,26 @@ function logSlotWarnings(warnings: string[]) {
|
|||
}
|
||||
}
|
||||
|
||||
function buildPreferredClawHubSpec(raw: string): string | null {
|
||||
const parsed = parseRegistryNpmSpec(raw);
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
return formatClawHubSpecifier({
|
||||
name: parsed.name,
|
||||
version: parsed.selector,
|
||||
});
|
||||
}
|
||||
|
||||
function shouldFallbackFromClawHubToNpm(error: string): boolean {
|
||||
const normalized = error.trim();
|
||||
return (
|
||||
/Package not found on ClawHub/i.test(normalized) ||
|
||||
/ClawHub .* failed \(404\)/i.test(normalized) ||
|
||||
/Version not found/i.test(normalized)
|
||||
);
|
||||
}
|
||||
|
||||
async function installBundledPluginSource(params: {
|
||||
config: OpenClawConfig;
|
||||
rawSpec: string;
|
||||
|
|
@ -545,6 +565,46 @@ async function runPluginInstallCommand(params: {
|
|||
return;
|
||||
}
|
||||
|
||||
const preferredClawHubSpec = buildPreferredClawHubSpec(raw);
|
||||
if (preferredClawHubSpec) {
|
||||
const clawhubResult = await installPluginFromClawHub({
|
||||
spec: preferredClawHubSpec,
|
||||
logger: createPluginInstallLogger(),
|
||||
});
|
||||
if (clawhubResult.ok) {
|
||||
clearPluginManifestRegistryCache();
|
||||
|
||||
let next = enablePluginInConfig(cfg, clawhubResult.pluginId).config;
|
||||
next = recordPluginInstall(next, {
|
||||
pluginId: clawhubResult.pluginId,
|
||||
source: "clawhub",
|
||||
spec: formatClawHubSpecifier({
|
||||
name: clawhubResult.clawhub.clawhubPackage,
|
||||
version: clawhubResult.clawhub.version,
|
||||
}),
|
||||
installPath: clawhubResult.targetDir,
|
||||
version: clawhubResult.version,
|
||||
integrity: clawhubResult.clawhub.integrity,
|
||||
resolvedAt: clawhubResult.clawhub.resolvedAt,
|
||||
clawhubUrl: clawhubResult.clawhub.clawhubUrl,
|
||||
clawhubPackage: clawhubResult.clawhub.clawhubPackage,
|
||||
clawhubFamily: clawhubResult.clawhub.clawhubFamily,
|
||||
clawhubChannel: clawhubResult.clawhub.clawhubChannel,
|
||||
});
|
||||
const slotResult = applySlotSelectionForPlugin(next, clawhubResult.pluginId);
|
||||
next = slotResult.config;
|
||||
await writeConfigFile(next);
|
||||
logSlotWarnings(slotResult.warnings);
|
||||
defaultRuntime.log(`Installed plugin: ${clawhubResult.pluginId}`);
|
||||
defaultRuntime.log(`Restart the gateway to load plugins.`);
|
||||
return;
|
||||
}
|
||||
if (!shouldFallbackFromClawHubToNpm(clawhubResult.error)) {
|
||||
defaultRuntime.error(clawhubResult.error);
|
||||
return defaultRuntime.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await installPluginFromNpmSpec({
|
||||
spec: raw,
|
||||
logger: createPluginInstallLogger(),
|
||||
|
|
|
|||
Loading…
Reference in New Issue