mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
fix(plugins): keep bare installs on npm for launch
This commit is contained in:
12
CHANGELOG.md
12
CHANGELOG.md
@@ -17,7 +17,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Highlights
|
||||
|
||||
- Alpha prerelease support adds the `vYYYY.M.D-alpha.N` tag shape, npm `alpha` dist-tag, release workflow inputs, package acceptance, Telegram package checks, and upgrade-survivor validation paths.
|
||||
- ClawHub-first plugin installation now covers diagnostics, onboarding, doctor repair, channel setup, install/update records, and artifact metadata while keeping npm fallback for cutovers. Thanks @vincentkoc.
|
||||
- External plugin installation now covers diagnostics, onboarding, doctor repair, channel setup, install/update records, and artifact metadata while keeping bare package installs on npm for the first cutover. Thanks @vincentkoc.
|
||||
- Gateway startup, session listing, task maintenance, prompt prep, plugin loading, and filesystem hot paths get targeted cache and fanout reductions for large or plugin-heavy installs.
|
||||
- Control UI and WebChat reliability improves across Sessions, Cron, long-running Gateway WebSockets, grouped-message width, slash-command feedback, iOS PWA bounds, selection contrast, and Talk diagnostics.
|
||||
- Channel and provider fixes cover Telegram topic commands and networking, Discord delivery and startup edge cases, OpenAI-compatible TTS/Realtime, OpenRouter/DeepSeek replay, Anthropic-compatible streaming, Brave/SearXNG/Firecrawl web search, and voice-call routing.
|
||||
@@ -26,7 +26,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Release: add first-class alpha prerelease support across version parsing, release workflows, package specs, published-package validation, plugin publish planning, and release docs.
|
||||
- Gateway/startup: skip plugin-backed auth-profile overlays during startup secrets preflight, reducing gateway readiness latency while keeping reload and OAuth recovery paths overlay-capable. (#68327) Thanks @JIRBOY.
|
||||
- Plugins/ClawHub: make diagnostics, onboarding, doctor repair, and channel setup prefer ClawHub installs while carrying ClawPack metadata through install records and npm fallback paths. Thanks @vincentkoc.
|
||||
- Plugins/ClawHub: make diagnostics, onboarding, doctor repair, and channel setup carry ClawPack metadata through install records while keeping explicit `clawhub:` installs on ClawHub and bare package installs on npm for the launch cutover. Thanks @vincentkoc.
|
||||
- Plugins/runtime: scope broad runtime preloads to the effective plugin ids derived from config, startup planning, configured channels, slots, and auto-enable rules instead of importing every discoverable plugin.
|
||||
- Agents/runtime: reuse the startup-loaded plugin registry for request-time providers, tools, channel actions, web/capability/memory/migration helpers, and memoized provider extra-params so stable embedded-run inputs no longer repeat plugin registry resolution while model-specific transport hook patches stay isolated. Thanks @DmitryPogodaev.
|
||||
- Agents/runtime: memoize transcript replay-policy resolution for stable config and process-env runs while preserving custom-env provider hook behavior. Thanks @DmitryPogodaev.
|
||||
@@ -46,8 +46,8 @@ Docs: https://docs.openclaw.ai
|
||||
- Google Meet: add `googlemeet test-listen` and the matching `google_meet` `test_listen` action so transcribe-mode joins wait for real caption or transcript movement before reporting listen-first health. Refs #72478. Thanks @DougButdorf.
|
||||
- Plugins/ClawHub: prefer versioned ClawPack artifacts when ClawHub publishes digest metadata, verifying the ClawPack response header and downloaded bytes before installing. Thanks @vincentkoc.
|
||||
- Plugins/ClawHub: persist ClawPack digest metadata on ClawHub plugin install and update records so registry refreshes and download verification can reuse stored artifact facts. Thanks @vincentkoc.
|
||||
- Plugins/ClawHub: allow official bundled-plugin cutovers to prefer ClawHub installs with npm fallback only when the ClawHub package or version is absent. Thanks @vincentkoc.
|
||||
- Plugins/onboarding: allow install-on-demand provider setup entries to prefer ClawHub packages before npm/local fallback and persist ClawHub artifact metadata after install. Thanks @vincentkoc.
|
||||
- Plugins/ClawHub: allow official bundled-plugin cutovers to record ClawHub artifact metadata while preserving npm as the launch default for bare package specs. Thanks @vincentkoc.
|
||||
- Plugins/onboarding: allow install-on-demand provider setup entries to persist ClawHub artifact metadata after explicit ClawHub installs while retaining npm/local fallback paths. Thanks @vincentkoc.
|
||||
- Plugins/Crestodian: add ClawHub plugin search plus Crestodian plugin list/search/install/uninstall operations, with approval and audit coverage for install and uninstall.
|
||||
- Channels/thread bindings: replace split subagent/ACP thread-spawn toggles with `threadBindings.spawnSessions`, default thread-bound spawns on, and let `openclaw doctor --fix` migrate the legacy keys. (#75943)
|
||||
- Providers/OpenAI: add `extraBody`/`extra_body` passthrough for OpenAI-compatible TTS endpoints, so custom speech servers can receive fields such as `lang` in `/audio/speech` requests. Fixes #39900. Thanks @R3NK0R.
|
||||
@@ -60,7 +60,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Codex/app-server: resolve managed binaries from bundled `dist` chunks and from the `@openai/codex` package bin when installs do not provide a nearby `.bin/codex` shim, avoiding false missing-binary startup failures.
|
||||
- Plugins/ClawHub: use the ClawHub artifact resolver response as the install decision before downloading, keeping legacy ZIP fallback and future ClawPack npm-pack installs on the same explicit resolver path. Thanks @vincentkoc.
|
||||
- Plugins/ClawHub: gate bare plugin specs on ClawHub readiness before preferring ClawHub, so packages without deployed ClawPack readiness keep the npm fallback path instead of failing through a half-ready registry route. Thanks @vincentkoc.
|
||||
- Plugins/ClawHub: keep bare plugin package specs on npm for the launch cutover and reserve ClawHub resolution for explicit `clawhub:` specs until ClawHub pack readiness is deployed. Thanks @vincentkoc.
|
||||
- Plugins/source checkout: discover source-only plugins such as Codex from the `extensions/*` workspace while using npm package excludes as the packaged-core boundary, removing the stale core-bundle metadata path.
|
||||
- Plugins/ClawHub: install ClawPack artifacts from the explicit npm-pack `.tgz` resolver path and persist artifact kind, npm integrity, shasum, and tarball metadata for update and diagnostics flows. Thanks @vincentkoc.
|
||||
- Control UI: allow deployments to configure grouped chat message max-width with a validated `gateway.controlUi.chatMessageMaxWidth` setting instead of patching bundled CSS after upgrades. Fixes #67935. Thanks @xiew4589-lang.
|
||||
@@ -73,7 +73,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/failover: exempt run-level timeouts that fire during tool execution from model fallback, timeout-triggered compaction, and generic timeout payload synthesis, avoiding misleading "LLM request timed out" errors after the primary model has already responded. Fixes #52147. (#75873) Thanks @simonusa.
|
||||
- Docker: copy Bun 1.3.13 from a digest-pinned image and keep CI on the same version. Fixes #74356. Thanks @fede-kamel and @sallyom.
|
||||
- Agents/compaction: keep prior context on consecutive turns against z.ai-style providers (z.ai direct, openrouter z-ai/\*, in-house GLM gateways), avoiding accidental Pi state reset after successful turns. (#76056) Thanks @openperf.
|
||||
- Doctor/plugins: run a one-time 2026.5.2 configured-plugin install repair based on `meta.lastTouchedVersion`, installing actively used downloadable OpenClaw plugins from ClawHub with npm fallback before marking the config touched for the release.
|
||||
- Doctor/plugins: run a one-time 2026.5.2 configured-plugin install repair based on `meta.lastTouchedVersion`, installing actively used downloadable OpenClaw plugins through the configured external source before marking the config touched for the release.
|
||||
- Sessions/transcripts: use one `session.writeLock.acquireTimeoutMs` policy for session transcript lock acquisitions and raise the default wait to 60 seconds, avoiding user-visible lock timeouts during legitimate slow prep, cleanup, compaction, and mirror work. Fixes #75894. Thanks @shandutta.
|
||||
- Control UI: contain the standalone iOS PWA viewport with safe-area-aware document locking, so Add-to-Home-Screen launches cannot scroll past the device bounds. Refs #76072. Thanks @kvncrw.
|
||||
- Agents/restart recovery: match cleaned transcript locks by exact transcript lock paths plus the canonical session fallback, so interrupted main sessions using topic-suffixed transcripts resume after gateway restart. Refs #76052. Thanks @anyech.
|
||||
|
||||
@@ -197,7 +197,7 @@ openclaw hooks disable command-logger
|
||||
## Install hook packs
|
||||
|
||||
```bash
|
||||
openclaw plugins install <package> # ClawHub first, then npm
|
||||
openclaw plugins install <package> # npm by default
|
||||
openclaw plugins install npm:<package> # npm only
|
||||
openclaw plugins install <package> --pin # pin version
|
||||
openclaw plugins install <path> # local path
|
||||
|
||||
@@ -68,7 +68,7 @@ Native OpenClaw plugins must ship `openclaw.plugin.json` with an inline JSON Sch
|
||||
|
||||
```bash
|
||||
openclaw plugins search "calendar" # search ClawHub plugins
|
||||
openclaw plugins install <package> # ClawHub first, then npm
|
||||
openclaw plugins install <package> # npm by default
|
||||
openclaw plugins install clawhub:<package> # ClawHub only
|
||||
openclaw plugins install npm:<package> # npm only
|
||||
openclaw plugins install git:github.com/<owner>/<repo> # git repo
|
||||
@@ -83,7 +83,7 @@ openclaw plugins install <plugin> --marketplace https://github.com/<owner>/<repo
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Bare package names are checked against ClawHub first, then npm. Treat plugin installs like running code. Prefer pinned versions.
|
||||
Bare package names install from npm by default during the launch cutover. Use `clawhub:<package>` for ClawHub. Treat plugin installs like running code. Prefer pinned versions.
|
||||
</Warning>
|
||||
|
||||
`plugins search` queries ClawHub for installable plugin packages and prints
|
||||
@@ -129,7 +129,7 @@ current OpenClaw or a local checkout until a newer npm package is published.
|
||||
|
||||
Npm specs are **registry-only** (package name + optional **exact version** or **dist-tag**). Git/URL/file specs and semver ranges are rejected. Dependency installs run project-local with `--ignore-scripts` for safety, even when your shell has global npm install settings.
|
||||
|
||||
Use `npm:<package>` when you want to skip ClawHub lookup and install directly from npm. Bare package specs still prefer ClawHub and only fall back to npm when ClawHub does not have that package or version.
|
||||
Use `npm:<package>` when you want to make npm resolution explicit. Bare package specs also install directly from npm during the launch cutover.
|
||||
|
||||
Bare specs and `@latest` stay on the stable track. If npm resolves either of those to a prerelease, OpenClaw stops and asks you to opt in explicitly with a prerelease tag such as `@beta`/`@rc` or an exact prerelease version such as `@1.2.3-beta.4`.
|
||||
|
||||
@@ -159,13 +159,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:
|
||||
Bare npm-safe plugin specs install from npm by default during the launch cutover:
|
||||
|
||||
```bash
|
||||
openclaw plugins install openclaw-codex-app-server
|
||||
```
|
||||
|
||||
Use `npm:` to force npm-only resolution, for example when ClawHub is unreachable or you know the package exists only on npm:
|
||||
Use `npm:` to make npm-only resolution explicit:
|
||||
|
||||
```bash
|
||||
openclaw plugins install npm:openclaw-codex-app-server
|
||||
|
||||
@@ -15,8 +15,8 @@ combination.
|
||||
|
||||
You do not need to add your plugin to the OpenClaw repository. Publish to
|
||||
[ClawHub](/tools/clawhub) and users install with
|
||||
`openclaw plugins install <package-name>`. OpenClaw tries ClawHub first and
|
||||
falls back to npm automatically for packages that still use npm distribution.
|
||||
`openclaw plugins install clawhub:<package-name>`. Bare package specs still
|
||||
install from npm during the launch cutover.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -141,9 +141,8 @@ and provider plugins have dedicated guides linked above.
|
||||
openclaw plugins install clawhub:@myorg/openclaw-my-plugin
|
||||
```
|
||||
|
||||
OpenClaw also checks ClawHub before npm for bare package specs like
|
||||
`@myorg/openclaw-my-plugin`; npm remains a fallback for packages that have
|
||||
not migrated to ClawHub yet.
|
||||
Bare package specs like `@myorg/openclaw-my-plugin` install from npm during
|
||||
the launch cutover. Use `clawhub:` when you want ClawHub resolution.
|
||||
|
||||
**In-repo plugins:** place under the bundled plugin workspace tree — automatically discovered.
|
||||
|
||||
|
||||
@@ -9,18 +9,18 @@ title: "Community plugins"
|
||||
Community plugins are third-party packages that extend OpenClaw with new
|
||||
channels, tools, providers, or other capabilities. They are built and maintained
|
||||
by the community, usually published on [ClawHub](/tools/clawhub), and installable
|
||||
with a single command. Npm remains a supported fallback for packages that have
|
||||
not moved to ClawHub yet.
|
||||
with a single command. Npm remains the launch default for bare package specs
|
||||
while ClawHub pack installs roll out.
|
||||
|
||||
ClawHub is the canonical discovery surface for community plugins. Do not open
|
||||
docs-only PRs just to add your plugin here for discoverability; publish it on
|
||||
ClawHub instead.
|
||||
|
||||
```bash
|
||||
openclaw plugins install <package-name>
|
||||
openclaw plugins install clawhub:<package-name>
|
||||
```
|
||||
|
||||
OpenClaw checks ClawHub first and falls back to npm automatically.
|
||||
Use `openclaw plugins install <package-name>` for npm-hosted packages.
|
||||
|
||||
## Listed plugins
|
||||
|
||||
|
||||
@@ -495,12 +495,12 @@ The `ChannelSetupWizard` type supports `credentials`, `textInputs`, `dmPolicy`,
|
||||
**External plugins:** publish to [ClawHub](/tools/clawhub), then install:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Auto (ClawHub then npm)">
|
||||
<Tab title="npm">
|
||||
```bash
|
||||
openclaw plugins install @myorg/openclaw-my-plugin
|
||||
```
|
||||
|
||||
OpenClaw tries ClawHub first and falls back to npm automatically.
|
||||
Bare package specs install from npm during the launch cutover.
|
||||
|
||||
</Tab>
|
||||
<Tab title="ClawHub only">
|
||||
|
||||
@@ -66,16 +66,15 @@ Site: [clawhub.ai](https://clawhub.ai)
|
||||
```
|
||||
|
||||
`plugins search` queries the ClawHub plugin catalog and prints install-ready
|
||||
package names. Bare npm-safe plugin specs use ClawHub only after package
|
||||
readiness says the package is install-ready for OpenClaw; otherwise OpenClaw
|
||||
preserves npm fallback:
|
||||
package names. Use `clawhub:<package>` when you want ClawHub resolution.
|
||||
Bare npm-safe plugin specs install from npm during the launch cutover:
|
||||
|
||||
```bash
|
||||
openclaw plugins install openclaw-codex-app-server
|
||||
```
|
||||
|
||||
Use `npm:<package>` when you want npm-only resolution without a
|
||||
ClawHub lookup:
|
||||
`npm:<package>` is also npm-only and is useful when a spec could otherwise
|
||||
be ambiguous:
|
||||
|
||||
```bash
|
||||
openclaw plugins install npm:openclaw-codex-app-server
|
||||
|
||||
@@ -90,7 +90,7 @@ If you prefer chat-native control, enable `commands.plugins: true` and use:
|
||||
|
||||
The install path uses the same resolver as the CLI: local path/archive, explicit
|
||||
`clawhub:<pkg>`, explicit `npm:<pkg>`, explicit `git:<repo>`, or bare package
|
||||
spec (ClawHub first, then npm fallback).
|
||||
spec through npm.
|
||||
|
||||
If config is invalid, install normally fails closed and points you at
|
||||
`openclaw doctor --fix`. The only recovery exception is a narrow bundled-plugin
|
||||
@@ -467,7 +467,7 @@ openclaw plugins registry # inspect persisted registry state
|
||||
openclaw plugins registry --refresh # rebuild persisted registry
|
||||
openclaw doctor --fix # repair plugin registry state
|
||||
|
||||
openclaw plugins install <package> # install (readiness-gated ClawHub, then npm)
|
||||
openclaw plugins install <package> # install from npm by default
|
||||
openclaw plugins install clawhub:<pkg> # install from ClawHub only
|
||||
openclaw plugins install npm:<pkg> # install from npm only
|
||||
openclaw plugins install git:<repo> # install from git
|
||||
|
||||
@@ -19,9 +19,7 @@ vi.mock("../../cli/npm-resolution.js", () => ({
|
||||
|
||||
vi.mock("../../cli/plugins-command-helpers.js", () => ({
|
||||
createPluginInstallLogger: vi.fn(() => ({})),
|
||||
decidePreferredClawHubFallback: vi.fn(() => "fallback_to_npm"),
|
||||
resolveFileNpmSpecToLocalPath: vi.fn(() => null),
|
||||
resolvePreferredClawHubSpec: vi.fn(() => null),
|
||||
}));
|
||||
|
||||
vi.mock("../../cli/plugins-install-persist.js", () => ({
|
||||
|
||||
@@ -2,9 +2,7 @@ import fs from "node:fs";
|
||||
import { buildNpmInstallRecordFields } from "../../cli/npm-resolution.js";
|
||||
import {
|
||||
createPluginInstallLogger,
|
||||
decidePreferredClawHubFallback,
|
||||
resolveFileNpmSpecToLocalPath,
|
||||
resolvePreferredClawHubSpec,
|
||||
} from "../../cli/plugins-command-helpers.js";
|
||||
import { persistPluginInstall } from "../../cli/plugins-install-persist.js";
|
||||
import type { ConfigSnapshotForInstallPersist } from "../../cli/plugins-install-persist.js";
|
||||
@@ -256,36 +254,6 @@ async function installPluginFromPluginsCommand(params: {
|
||||
return { ok: true, pluginId: result.pluginId };
|
||||
}
|
||||
|
||||
const preferredClawHubSpec = await resolvePreferredClawHubSpec(params.raw);
|
||||
if (preferredClawHubSpec) {
|
||||
const clawhubResult = await installPluginFromClawHub({
|
||||
spec: preferredClawHubSpec,
|
||||
logger: createPluginInstallLogger(),
|
||||
});
|
||||
if (clawhubResult.ok) {
|
||||
await persistPluginInstall({
|
||||
snapshot: params.snapshot,
|
||||
pluginId: clawhubResult.pluginId,
|
||||
install: {
|
||||
source: "clawhub",
|
||||
spec: preferredClawHubSpec,
|
||||
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,
|
||||
},
|
||||
});
|
||||
return { ok: true, pluginId: clawhubResult.pluginId };
|
||||
}
|
||||
if (decidePreferredClawHubFallback(clawhubResult) !== "fallback_to_npm") {
|
||||
return { ok: false, error: clawhubResult.error };
|
||||
}
|
||||
}
|
||||
|
||||
const result = await installPluginFromNpmSpec({
|
||||
spec: params.raw,
|
||||
logger: createPluginInstallLogger(),
|
||||
|
||||
@@ -11,8 +11,6 @@ type UnknownMock = Mock<(...args: unknown[]) => unknown>;
|
||||
type AsyncUnknownMock = Mock<(...args: unknown[]) => Promise<unknown>>;
|
||||
type LoadConfigFn = (typeof import("../config/config.js"))["loadConfig"];
|
||||
type ParseClawHubPluginSpecFn = (typeof import("../infra/clawhub.js"))["parseClawHubPluginSpec"];
|
||||
type FetchClawHubPackageReadinessFn =
|
||||
(typeof import("../infra/clawhub.js"))["fetchClawHubPackageReadiness"];
|
||||
type InstallPluginFromMarketplaceFn =
|
||||
(typeof import("../plugins/marketplace.js"))["installPluginFromMarketplace"];
|
||||
type InstallPluginFromGitSpecFn =
|
||||
@@ -80,7 +78,6 @@ export const installPluginFromNpmSpec: AsyncUnknownMock = vi.fn();
|
||||
export const installPluginFromPath: AsyncUnknownMock = vi.fn();
|
||||
export const installPluginFromClawHub: AsyncUnknownMock = vi.fn();
|
||||
export const parseClawHubPluginSpec: Mock<ParseClawHubPluginSpecFn> = vi.fn();
|
||||
export const fetchClawHubPackageReadiness: Mock<FetchClawHubPackageReadinessFn> = vi.fn();
|
||||
export const installHooksFromNpmSpec: AsyncUnknownMock = vi.fn();
|
||||
export const installHooksFromPath: AsyncUnknownMock = vi.fn();
|
||||
export const recordHookInstall: UnknownMock = vi.fn();
|
||||
@@ -563,16 +560,6 @@ vi.mock("../plugins/clawhub.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../infra/clawhub.js", () => ({
|
||||
fetchClawHubPackageReadiness: ((
|
||||
...args: Parameters<(typeof import("../infra/clawhub.js"))["fetchClawHubPackageReadiness"]>
|
||||
) =>
|
||||
invokeMock<
|
||||
Parameters<(typeof import("../infra/clawhub.js"))["fetchClawHubPackageReadiness"]>,
|
||||
ReturnType<(typeof import("../infra/clawhub.js"))["fetchClawHubPackageReadiness"]>
|
||||
>(
|
||||
fetchClawHubPackageReadiness,
|
||||
...args,
|
||||
)) as (typeof import("../infra/clawhub.js"))["fetchClawHubPackageReadiness"],
|
||||
parseClawHubPluginSpec: ((
|
||||
...args: Parameters<(typeof import("../infra/clawhub.js"))["parseClawHubPluginSpec"]>
|
||||
) =>
|
||||
@@ -634,7 +621,6 @@ export function resetPluginsCliTestState() {
|
||||
installPluginFromPath.mockReset();
|
||||
installPluginFromClawHub.mockReset();
|
||||
parseClawHubPluginSpec.mockReset();
|
||||
fetchClawHubPackageReadiness.mockReset();
|
||||
installHooksFromNpmSpec.mockReset();
|
||||
installHooksFromPath.mockReset();
|
||||
recordHookInstall.mockReset();
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
applyExclusiveSlotSelection,
|
||||
buildPluginSnapshotReport,
|
||||
enablePluginInConfig,
|
||||
fetchClawHubPackageReadiness,
|
||||
installHooksFromNpmSpec,
|
||||
installHooksFromPath,
|
||||
installPluginFromClawHub,
|
||||
@@ -653,112 +652,10 @@ describe("plugins cli install", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers ClawHub before npm for bare plugin specs", async () => {
|
||||
const cfg = {
|
||||
plugins: {
|
||||
entries: {},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const enabledCfg = createEnabledPluginConfig("demo");
|
||||
loadConfig.mockReturnValue(cfg);
|
||||
fetchClawHubPackageReadiness.mockResolvedValue({ readyForOpenClaw: true });
|
||||
installPluginFromClawHub.mockResolvedValue(
|
||||
createClawHubInstallResult({
|
||||
pluginId: "demo",
|
||||
packageName: "demo",
|
||||
version: "1.2.3",
|
||||
channel: "community",
|
||||
}),
|
||||
);
|
||||
enablePluginInConfig.mockReturnValue({ config: enabledCfg });
|
||||
applyExclusiveSlotSelection.mockReturnValue({
|
||||
config: enabledCfg,
|
||||
warnings: [],
|
||||
});
|
||||
|
||||
await runPluginsCommand(["plugins", "install", "demo"]);
|
||||
|
||||
expect(installPluginFromClawHub).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "clawhub:demo",
|
||||
}),
|
||||
);
|
||||
expect(installPluginFromNpmSpec).not.toHaveBeenCalled();
|
||||
expect(writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith({
|
||||
demo: expect.objectContaining({
|
||||
source: "clawhub",
|
||||
spec: "clawhub:demo",
|
||||
installPath: cliInstallPath("demo"),
|
||||
version: "1.2.3",
|
||||
clawhubPackage: "demo",
|
||||
}),
|
||||
});
|
||||
expect(writeConfigFile).toHaveBeenCalledWith(enabledCfg);
|
||||
});
|
||||
|
||||
it("keeps explicit bare ClawHub selectors in install records", async () => {
|
||||
const cfg = {
|
||||
plugins: {
|
||||
entries: {},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const enabledCfg = createEnabledPluginConfig("demo");
|
||||
loadConfig.mockReturnValue(cfg);
|
||||
fetchClawHubPackageReadiness.mockResolvedValue({ phase: "legacy-zip-only" });
|
||||
installPluginFromClawHub.mockResolvedValue(
|
||||
createClawHubInstallResult({
|
||||
pluginId: "demo",
|
||||
packageName: "demo",
|
||||
version: "1.2.3-beta.1",
|
||||
channel: "community",
|
||||
}),
|
||||
);
|
||||
enablePluginInConfig.mockReturnValue({ config: enabledCfg });
|
||||
applyExclusiveSlotSelection.mockReturnValue({
|
||||
config: enabledCfg,
|
||||
warnings: [],
|
||||
});
|
||||
|
||||
await runPluginsCommand(["plugins", "install", "demo@beta"]);
|
||||
|
||||
expect(installPluginFromClawHub).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "clawhub:demo@beta",
|
||||
}),
|
||||
);
|
||||
expect(writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith({
|
||||
demo: expect.objectContaining({
|
||||
source: "clawhub",
|
||||
spec: "clawhub:demo@beta",
|
||||
version: "1.2.3-beta.1",
|
||||
clawhubPackage: "demo",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to npm when ClawHub does not have the package", async () => {
|
||||
primeNpmPluginFallback();
|
||||
fetchClawHubPackageReadiness.mockResolvedValue({ phase: "ready-for-openclaw" });
|
||||
|
||||
await runPluginsCommand(["plugins", "install", "demo"]);
|
||||
|
||||
expect(installPluginFromClawHub).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "clawhub:demo",
|
||||
}),
|
||||
);
|
||||
expect(installPluginFromNpmSpec).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "demo",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves npm install behavior for bare specs until ClawHub readiness is available", async () => {
|
||||
it("installs bare plugin specs through npm without ClawHub lookup", async () => {
|
||||
const cfg = createEmptyPluginConfig();
|
||||
const enabledCfg = createEnabledPluginConfig("demo");
|
||||
loadConfig.mockReturnValue(cfg);
|
||||
fetchClawHubPackageReadiness.mockRejectedValue(new Error("not deployed"));
|
||||
installPluginFromNpmSpec.mockResolvedValue(createNpmPluginInstallResult("demo"));
|
||||
enablePluginInConfig.mockReturnValue({ config: enabledCfg });
|
||||
applyExclusiveSlotSelection.mockReturnValue({
|
||||
@@ -768,13 +665,42 @@ describe("plugins cli install", () => {
|
||||
|
||||
await runPluginsCommand(["plugins", "install", "demo"]);
|
||||
|
||||
expect(fetchClawHubPackageReadiness).toHaveBeenCalledWith({ name: "demo" });
|
||||
expect(installPluginFromClawHub).not.toHaveBeenCalled();
|
||||
expect(installPluginFromNpmSpec).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "demo",
|
||||
}),
|
||||
);
|
||||
expect(writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith({
|
||||
demo: expect.objectContaining({
|
||||
source: "npm",
|
||||
spec: "demo",
|
||||
installPath: cliInstallPath("demo"),
|
||||
version: "1.2.3",
|
||||
}),
|
||||
});
|
||||
expect(writeConfigFile).toHaveBeenCalledWith(enabledCfg);
|
||||
});
|
||||
|
||||
it("passes bare npm selectors through npm without ClawHub lookup", async () => {
|
||||
const cfg = createEmptyPluginConfig();
|
||||
const enabledCfg = createEnabledPluginConfig("demo");
|
||||
loadConfig.mockReturnValue(cfg);
|
||||
installPluginFromNpmSpec.mockResolvedValue(createNpmPluginInstallResult("demo"));
|
||||
enablePluginInConfig.mockReturnValue({ config: enabledCfg });
|
||||
applyExclusiveSlotSelection.mockReturnValue({
|
||||
config: enabledCfg,
|
||||
warnings: [],
|
||||
});
|
||||
|
||||
await runPluginsCommand(["plugins", "install", "demo@beta"]);
|
||||
|
||||
expect(installPluginFromClawHub).not.toHaveBeenCalled();
|
||||
expect(installPluginFromNpmSpec).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "demo@beta",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("installs directly from npm when npm: prefix is used", async () => {
|
||||
@@ -1537,15 +1463,17 @@ describe("plugins cli install", () => {
|
||||
expect(runtimeLogs.some((line) => line.includes("Installed hook pack: demo-hooks"))).toBe(true);
|
||||
});
|
||||
|
||||
it("does not fall back to npm when ClawHub rejects a real package", async () => {
|
||||
fetchClawHubPackageReadiness.mockResolvedValue({ phase: "ready-for-openclaw" });
|
||||
it("does not fall back to npm when explicit ClawHub rejects a real package", async () => {
|
||||
parseClawHubPluginSpec.mockReturnValue({ name: "demo" });
|
||||
installPluginFromClawHub.mockResolvedValue({
|
||||
ok: false,
|
||||
error: 'Use "openclaw skills install demo" instead.',
|
||||
code: "skill_package",
|
||||
});
|
||||
|
||||
await expect(runPluginsCommand(["plugins", "install", "demo"])).rejects.toThrow("__exit__:1");
|
||||
await expect(runPluginsCommand(["plugins", "install", "clawhub:demo"])).rejects.toThrow(
|
||||
"__exit__:1",
|
||||
);
|
||||
|
||||
expect(installPluginFromNpmSpec).not.toHaveBeenCalled();
|
||||
expect(runtimeErrors.at(-1)).toContain('Use "openclaw skills install demo" instead.');
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { fetchClawHubPackageReadiness, type ClawHubPackageReadiness } from "../infra/clawhub.js";
|
||||
import { parseRegistryNpmSpec } from "../infra/npm-registry-spec.js";
|
||||
import { CLAWHUB_INSTALL_ERROR_CODE } from "../plugins/clawhub.js";
|
||||
import type { PluginKind } from "../plugins/plugin-kind.types.js";
|
||||
import { loadPluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js";
|
||||
import { applyExclusiveSlotSelection } from "../plugins/slots.js";
|
||||
@@ -204,53 +201,6 @@ export function logSlotWarnings(warnings: string[], runtime: RuntimeEnv = defaul
|
||||
}
|
||||
}
|
||||
|
||||
export function buildPreferredClawHubSpec(raw: string): string | null {
|
||||
const parsed = parseRegistryNpmSpec(raw);
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
return `clawhub:${parsed.name}${parsed.selector ? `@${parsed.selector}` : ""}`;
|
||||
}
|
||||
|
||||
function normalizeReadinessPhase(readiness: ClawHubPackageReadiness): string {
|
||||
return normalizeLowercaseStringOrEmpty(readiness.phase ?? readiness.status ?? "");
|
||||
}
|
||||
|
||||
export function isClawHubReadinessInstallReady(
|
||||
readiness: ClawHubPackageReadiness | null | undefined,
|
||||
): boolean {
|
||||
if (!readiness) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
readiness.ready === true ||
|
||||
readiness.readyForOpenClaw === true ||
|
||||
readiness.installReady === true
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const phase = normalizeReadinessPhase(readiness);
|
||||
return (
|
||||
phase === "ready-for-openclaw" || phase === "clawpack-ready" || phase === "legacy-zip-only"
|
||||
);
|
||||
}
|
||||
|
||||
export async function resolvePreferredClawHubSpec(raw: string): Promise<string | null> {
|
||||
const parsed = parseRegistryNpmSpec(raw);
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const readiness = await fetchClawHubPackageReadiness({ name: parsed.name });
|
||||
if (!isClawHubReadinessInstallReady(readiness)) {
|
||||
return null;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return `clawhub:${parsed.name}${parsed.selector ? `@${parsed.selector}` : ""}`;
|
||||
}
|
||||
|
||||
export function parseNpmPrefixSpec(raw: string): string | null {
|
||||
const trimmed = raw.trim();
|
||||
if (!normalizeLowercaseStringOrEmpty(trimmed).startsWith("npm:")) {
|
||||
@@ -258,23 +208,3 @@ export function parseNpmPrefixSpec(raw: string): string | null {
|
||||
}
|
||||
return trimmed.slice("npm:".length).trim();
|
||||
}
|
||||
|
||||
const PREFERRED_CLAWHUB_FALLBACK_DECISION = {
|
||||
FALLBACK_TO_NPM: "fallback_to_npm",
|
||||
STOP: "stop",
|
||||
} as const;
|
||||
|
||||
export type PreferredClawHubFallbackDecision =
|
||||
(typeof PREFERRED_CLAWHUB_FALLBACK_DECISION)[keyof typeof PREFERRED_CLAWHUB_FALLBACK_DECISION];
|
||||
|
||||
export function decidePreferredClawHubFallback(params: {
|
||||
code?: string;
|
||||
}): PreferredClawHubFallbackDecision {
|
||||
if (
|
||||
params.code === CLAWHUB_INSTALL_ERROR_CODE.PACKAGE_NOT_FOUND ||
|
||||
params.code === CLAWHUB_INSTALL_ERROR_CODE.VERSION_NOT_FOUND
|
||||
) {
|
||||
return PREFERRED_CLAWHUB_FALLBACK_DECISION.FALLBACK_TO_NPM;
|
||||
}
|
||||
return PREFERRED_CLAWHUB_FALLBACK_DECISION.STOP;
|
||||
}
|
||||
|
||||
@@ -40,10 +40,8 @@ import {
|
||||
import {
|
||||
createHookPackInstallLogger,
|
||||
createPluginInstallLogger,
|
||||
decidePreferredClawHubFallback,
|
||||
formatPluginInstallWithHookFallbackError,
|
||||
parseNpmPrefixSpec,
|
||||
resolvePreferredClawHubSpec,
|
||||
} from "./plugins-command-helpers.js";
|
||||
import { persistHookPackInstall, persistPluginInstall } from "./plugins-install-persist.js";
|
||||
import type { ConfigSnapshotForInstallPersist } from "./plugins-install-persist.js";
|
||||
@@ -776,34 +774,6 @@ export async function runPluginInstallCommand(params: {
|
||||
return;
|
||||
}
|
||||
|
||||
const preferredClawHubSpec = await resolvePreferredClawHubSpec(raw);
|
||||
if (preferredClawHubSpec) {
|
||||
const clawhubResult = await installPluginFromClawHub({
|
||||
...safetyOverrides,
|
||||
mode: installMode,
|
||||
spec: preferredClawHubSpec,
|
||||
extensionsDir,
|
||||
logger: createPluginInstallLogger(runtime),
|
||||
});
|
||||
if (clawhubResult.ok) {
|
||||
await persistPluginInstall({
|
||||
snapshot,
|
||||
pluginId: clawhubResult.pluginId,
|
||||
install: {
|
||||
...buildClawHubPluginInstallRecordFields(clawhubResult.clawhub),
|
||||
spec: preferredClawHubSpec,
|
||||
installPath: clawhubResult.targetDir,
|
||||
},
|
||||
runtime,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (decidePreferredClawHubFallback(clawhubResult) !== "fallback_to_npm") {
|
||||
runtime.error(clawhubResult.error);
|
||||
return runtime.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const npmResult = await tryInstallPluginOrHookPackFromNpmSpec({
|
||||
snapshot,
|
||||
installMode,
|
||||
|
||||
Reference in New Issue
Block a user