mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:10:45 +00:00
fix(plugins): keep bare installs on npm for launch
This commit is contained in:
@@ -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