mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(update): surface plugin channel fallbacks (#81422)
* fix: surface plugin update channel fallbacks * fix: clarify dry-run plugin fallback output * fix: preserve failed plugin fallback metadata * chore: mark compatibility aliases deprecated * chore: fix channel runtime lint directive --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -223,7 +223,11 @@ export function estimateRenderedLlmBoundaryTokenPressure(params: {
|
||||
return Math.max(0, Math.ceil((systemTokens + promptTokens) * SAFETY_MARGIN));
|
||||
}
|
||||
|
||||
/** Backward-compatible alias for callers that still name this a pre-prompt estimate. */
|
||||
/**
|
||||
* Backward-compatible alias for callers that still name this a pre-prompt estimate.
|
||||
*
|
||||
* @deprecated Use estimateLlmBoundaryTokenPressure.
|
||||
*/
|
||||
export function estimatePrePromptTokens(params: {
|
||||
messages: AgentMessage[];
|
||||
systemPrompt?: string;
|
||||
|
||||
@@ -4,6 +4,9 @@ import { theme } from "../../packages/terminal-core/src/theme.js";
|
||||
type PluginUpdateCliOutcome = {
|
||||
status: string;
|
||||
message: string;
|
||||
channelFallback?: {
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
|
||||
/** Log update outcomes with severity styling and report whether any errors occurred. */
|
||||
@@ -16,13 +19,22 @@ export function logPluginUpdateOutcomes(params: {
|
||||
if (outcome.status === "error") {
|
||||
hasErrors = true;
|
||||
params.log(theme.error(outcome.message));
|
||||
if (outcome.channelFallback) {
|
||||
params.log(theme.warn(outcome.channelFallback.message));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (outcome.status === "skipped") {
|
||||
params.log(theme.warn(outcome.message));
|
||||
if (outcome.channelFallback) {
|
||||
params.log(theme.warn(outcome.channelFallback.message));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
params.log(outcome.message);
|
||||
if (outcome.channelFallback) {
|
||||
params.log(theme.warn(outcome.channelFallback.message));
|
||||
}
|
||||
}
|
||||
return { hasErrors };
|
||||
}
|
||||
|
||||
@@ -1580,6 +1580,49 @@ describe("update-cli", () => {
|
||||
expect(npmPluginUpdateCall()?.timeoutMs).toBe(1_800_000);
|
||||
});
|
||||
|
||||
it("prints plugin channel fallbacks near the post-core plugin summary", async () => {
|
||||
updateNpmInstalledPlugins.mockResolvedValueOnce({
|
||||
changed: false,
|
||||
config: baseConfig,
|
||||
outcomes: [
|
||||
{
|
||||
pluginId: "lossless-claw",
|
||||
status: "updated",
|
||||
message: "Updated lossless-claw: 1.0.0 -> 1.0.1.",
|
||||
channelFallback: {
|
||||
requestedSpec: "lossless-claw@beta",
|
||||
usedSpec: "lossless-claw",
|
||||
requestedLabel: "@beta",
|
||||
usedLabel: "@latest",
|
||||
reason: "unavailable",
|
||||
message:
|
||||
"plugin channel fallback: lossless-claw used @latest because @beta was unavailable",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await withEnvAsync(
|
||||
{
|
||||
OPENCLAW_UPDATE_POST_CORE: "1",
|
||||
OPENCLAW_UPDATE_POST_CORE_CHANNEL: "beta",
|
||||
},
|
||||
async () => {
|
||||
await updateCommand({ restart: false });
|
||||
},
|
||||
);
|
||||
|
||||
const logs = vi.mocked(runtimeCapture.log).mock.calls.map((call) => String(call[0]));
|
||||
expect(logs.some((line) => line.includes("npm plugins: 1 updated, 0 unchanged."))).toBe(true);
|
||||
expect(
|
||||
logs.some((line) =>
|
||||
line.includes(
|
||||
"plugin channel fallback: lossless-claw used @latest because @beta was unavailable",
|
||||
),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("uses a fail-closed integrity policy for post-core plugin updates", async () => {
|
||||
await withEnvAsync(
|
||||
{
|
||||
|
||||
@@ -560,6 +560,20 @@ function createGuidedPostUpdatePluginOutcome(outcome: PluginUpdateOutcome): {
|
||||
};
|
||||
}
|
||||
|
||||
function collectPluginChannelFallbackMessages(outcomes: readonly PluginUpdateOutcome[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
const messages: string[] = [];
|
||||
for (const outcome of outcomes) {
|
||||
const message = outcome.channelFallback?.message;
|
||||
if (!message || seen.has(message)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(message);
|
||||
messages.push(message);
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
function isDisabledAfterFailureOutcome(outcome: PluginUpdateOutcome): boolean {
|
||||
return outcome.status === "skipped" && outcome.message.includes("after plugin update failure");
|
||||
}
|
||||
@@ -1923,6 +1937,10 @@ export async function updatePluginsAfterCoreUpdate(params: {
|
||||
defaultRuntime.log(theme.muted(`npm plugins: ${parts.join(", ")}.`));
|
||||
}
|
||||
|
||||
for (const message of collectPluginChannelFallbackMessages(pluginUpdateOutcomes)) {
|
||||
defaultRuntime.log(theme.warn(message));
|
||||
}
|
||||
|
||||
for (const outcome of pluginUpdateOutcomes) {
|
||||
if (outcome.status !== "error") {
|
||||
continue;
|
||||
|
||||
@@ -49,8 +49,8 @@ export function registerChannelRuntimeContext(
|
||||
});
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Runtime context values are caller-typed by key.
|
||||
/** Reads a channel-scoped runtime context from the current runtime registry. */
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Runtime context values are caller-typed by key.
|
||||
export function getChannelRuntimeContext<T = unknown>(
|
||||
params: ChannelRuntimeContextKey & {
|
||||
channelRuntime?: ChannelRuntimeSurface;
|
||||
|
||||
@@ -129,7 +129,11 @@ export function resolveHomeRelativePath(
|
||||
return path.resolve(trimmed);
|
||||
}
|
||||
|
||||
/** Backward-compatible alias for resolving user paths against the effective home. */
|
||||
/**
|
||||
* Backward-compatible alias for resolving user paths against the effective home.
|
||||
*
|
||||
* @deprecated Use resolveHomeRelativePath.
|
||||
*/
|
||||
export function resolveUserPath(
|
||||
input: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
|
||||
@@ -86,6 +86,14 @@ export type UpdateRunResult = {
|
||||
message: string;
|
||||
currentVersion?: string;
|
||||
nextVersion?: string;
|
||||
channelFallback?: {
|
||||
requestedSpec: string;
|
||||
usedSpec: string;
|
||||
requestedLabel: string;
|
||||
usedLabel: string;
|
||||
reason: "unavailable" | "failed";
|
||||
message: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
integrityDrifts: Array<{
|
||||
|
||||
@@ -35,7 +35,11 @@ export function isVoiceMessageCompatibleAudio(opts: {
|
||||
return VOICE_MESSAGE_AUDIO_EXTENSIONS.has(ext);
|
||||
}
|
||||
|
||||
/** Backward-compatible alias for voice-message audio compatibility checks. */
|
||||
/**
|
||||
* Backward-compatible alias for voice-message audio compatibility checks.
|
||||
*
|
||||
* @deprecated Use isVoiceMessageCompatibleAudio.
|
||||
*/
|
||||
export function isVoiceCompatibleAudio(opts: {
|
||||
contentType?: string | null;
|
||||
fileName?: string | null;
|
||||
|
||||
@@ -2188,6 +2188,52 @@ describe("updateNpmInstalledPlugins", () => {
|
||||
expect(result.outcomes[0]?.message).toBe(
|
||||
"Updated openclaw-codex-app-server: unknown -> 0.2.6. (warning: beta channel fallback used openclaw-codex-app-server because openclaw-codex-app-server@beta could not be used).",
|
||||
);
|
||||
expect(result.outcomes[0]?.channelFallback).toEqual({
|
||||
requestedSpec: "openclaw-codex-app-server@beta",
|
||||
usedSpec: "openclaw-codex-app-server",
|
||||
requestedLabel: "@beta",
|
||||
usedLabel: "@latest",
|
||||
reason: "unavailable",
|
||||
message:
|
||||
"plugin channel fallback: openclaw-codex-app-server used @latest because @beta was unavailable",
|
||||
});
|
||||
});
|
||||
|
||||
it("reports npm beta fallback as tentative during dry-run checks", async () => {
|
||||
installPluginFromNpmSpecMock
|
||||
.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
error:
|
||||
"npm ERR! code ETARGET\nnpm ERR! No matching version found for openclaw-codex-app-server@beta.",
|
||||
})
|
||||
.mockResolvedValueOnce(
|
||||
createSuccessfulNpmUpdateResult({
|
||||
pluginId: "openclaw-codex-app-server",
|
||||
targetDir: "/tmp/openclaw-codex-app-server",
|
||||
version: "0.2.6",
|
||||
npmResolution: {
|
||||
name: "openclaw-codex-app-server",
|
||||
version: "0.2.6",
|
||||
resolvedSpec: "openclaw-codex-app-server@0.2.6",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await updateNpmInstalledPlugins({
|
||||
config: createCodexAppServerInstallConfig({
|
||||
spec: "openclaw-codex-app-server",
|
||||
}),
|
||||
pluginIds: ["openclaw-codex-app-server"],
|
||||
updateChannel: "beta",
|
||||
dryRun: true,
|
||||
});
|
||||
|
||||
expect(result.outcomes[0]?.message).toBe(
|
||||
"Would update openclaw-codex-app-server: unknown -> 0.2.6. (warning: beta channel fallback would use openclaw-codex-app-server because openclaw-codex-app-server@beta could not be used).",
|
||||
);
|
||||
expect(result.outcomes[0]?.channelFallback?.message).toBe(
|
||||
"plugin channel fallback: openclaw-codex-app-server would use @latest because @beta was unavailable",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to the default npm spec when the beta package exists but is invalid", async () => {
|
||||
@@ -2233,6 +2279,12 @@ describe("updateNpmInstalledPlugins", () => {
|
||||
expect(result.outcomes[0]?.message).toBe(
|
||||
"Updated openclaw-codex-app-server: unknown -> 0.2.6. (warning: beta channel fallback used openclaw-codex-app-server because openclaw-codex-app-server@beta could not be used).",
|
||||
);
|
||||
expect(result.outcomes[0]?.channelFallback).toMatchObject({
|
||||
requestedLabel: "@beta",
|
||||
usedLabel: "@latest",
|
||||
reason: "failed",
|
||||
message: "plugin channel fallback: openclaw-codex-app-server used @latest after @beta failed",
|
||||
});
|
||||
});
|
||||
|
||||
it("reports the fallback npm spec when beta fallback also fails", async () => {
|
||||
@@ -2262,6 +2314,55 @@ describe("updateNpmInstalledPlugins", () => {
|
||||
status: "error",
|
||||
message:
|
||||
"Failed to update openclaw-codex-app-server: npm package not found for openclaw-codex-app-server.",
|
||||
channelFallback: {
|
||||
requestedSpec: "openclaw-codex-app-server@beta",
|
||||
usedSpec: "openclaw-codex-app-server",
|
||||
requestedLabel: "@beta",
|
||||
usedLabel: "@latest",
|
||||
reason: "failed",
|
||||
message:
|
||||
"plugin channel fallback: openclaw-codex-app-server used @latest after @beta failed",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps fallback metadata when a dry-run beta fallback also fails", async () => {
|
||||
installPluginFromNpmSpecMock
|
||||
.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
error: "Installed plugin package uses a TypeScript entry without compiled runtime output.",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
code: "npm_package_not_found",
|
||||
error: "npm package not found",
|
||||
});
|
||||
|
||||
const result = await updateNpmInstalledPlugins({
|
||||
config: createCodexAppServerInstallConfig({
|
||||
spec: "openclaw-codex-app-server",
|
||||
}),
|
||||
pluginIds: ["openclaw-codex-app-server"],
|
||||
updateChannel: "beta",
|
||||
dryRun: true,
|
||||
});
|
||||
|
||||
expect(result.outcomes).toEqual([
|
||||
{
|
||||
pluginId: "openclaw-codex-app-server",
|
||||
status: "error",
|
||||
message:
|
||||
"Failed to check openclaw-codex-app-server: npm package not found for openclaw-codex-app-server.",
|
||||
channelFallback: {
|
||||
requestedSpec: "openclaw-codex-app-server@beta",
|
||||
usedSpec: "openclaw-codex-app-server",
|
||||
requestedLabel: "@beta",
|
||||
usedLabel: "@latest",
|
||||
reason: "failed",
|
||||
message:
|
||||
"plugin channel fallback: openclaw-codex-app-server would use @latest after @beta failed",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -68,12 +68,22 @@ export type PluginUpdateLogger = {
|
||||
|
||||
export type PluginUpdateStatus = "updated" | "unchanged" | "skipped" | "error";
|
||||
|
||||
export type PluginUpdateChannelFallback = {
|
||||
requestedSpec: string;
|
||||
usedSpec: string;
|
||||
requestedLabel: string;
|
||||
usedLabel: string;
|
||||
reason: "unavailable" | "failed";
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type PluginUpdateOutcome = {
|
||||
pluginId: string;
|
||||
status: PluginUpdateStatus;
|
||||
message: string;
|
||||
currentVersion?: string;
|
||||
nextVersion?: string;
|
||||
channelFallback?: PluginUpdateChannelFallback;
|
||||
};
|
||||
|
||||
export type PluginUpdateSummary = {
|
||||
@@ -485,6 +495,13 @@ function shouldFallbackBetaClawHubUpdate(result: { ok: false; code?: string }):
|
||||
return shouldFallbackClawHubToDefault(result);
|
||||
}
|
||||
|
||||
function isUnavailableNpmTarget(result: { ok: false; code?: string; error: string }): boolean {
|
||||
return (
|
||||
result.code === PLUGIN_INSTALL_ERROR_CODE.NPM_PACKAGE_NOT_FOUND ||
|
||||
/\b(ETARGET|notarget)\b|No matching version found|dist-tag|tag .*not found/i.test(result.error)
|
||||
);
|
||||
}
|
||||
|
||||
function describeBetaNpmFallback(params: {
|
||||
pluginId: string;
|
||||
betaSpec: string | undefined;
|
||||
@@ -492,15 +509,47 @@ function describeBetaNpmFallback(params: {
|
||||
result: { ok: false; code?: string; error: string };
|
||||
}): string {
|
||||
const betaSpec = params.betaSpec ?? "the beta npm release";
|
||||
const missingBeta =
|
||||
params.result.code === PLUGIN_INSTALL_ERROR_CODE.NPM_PACKAGE_NOT_FOUND ||
|
||||
/\b(ETARGET|notarget)\b|No matching version found|dist-tag|tag .*not found/i.test(
|
||||
params.result.error,
|
||||
);
|
||||
const missingBeta = isUnavailableNpmTarget(params.result);
|
||||
const reason = missingBeta ? "has no beta npm release" : "failed beta npm update";
|
||||
return `Plugin "${params.pluginId}" ${reason} for ${betaSpec}; using ${params.fallbackSpec} instead. Core update can still complete.`;
|
||||
}
|
||||
|
||||
function formatNpmSpecSelectorLabel(spec: string | undefined): string {
|
||||
const parsed = spec ? parseRegistryNpmSpec(spec) : undefined;
|
||||
if (!parsed) {
|
||||
return spec ?? "unknown";
|
||||
}
|
||||
if (parsed.selectorKind === "none") {
|
||||
return "@latest";
|
||||
}
|
||||
return `@${parsed.selector}`;
|
||||
}
|
||||
|
||||
function describeNpmChannelFallback(params: {
|
||||
pluginId: string;
|
||||
requestedSpec: string | undefined;
|
||||
usedSpec: string;
|
||||
result: { ok: false; code?: string; error: string };
|
||||
verb: "used" | "would use";
|
||||
}): PluginUpdateChannelFallback {
|
||||
const requestedSpec = params.requestedSpec ?? "unknown";
|
||||
const requestedLabel = formatNpmSpecSelectorLabel(params.requestedSpec);
|
||||
const usedLabel = formatNpmSpecSelectorLabel(params.usedSpec);
|
||||
const reason = isUnavailableNpmTarget(params.result) ? "unavailable" : "failed";
|
||||
const message =
|
||||
reason === "unavailable"
|
||||
? `plugin channel fallback: ${params.pluginId} ${params.verb} ${usedLabel} because ${requestedLabel} was unavailable`
|
||||
: `plugin channel fallback: ${params.pluginId} ${params.verb} ${usedLabel} after ${requestedLabel} failed`;
|
||||
return {
|
||||
requestedSpec,
|
||||
usedSpec: params.usedSpec,
|
||||
requestedLabel,
|
||||
usedLabel,
|
||||
reason,
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
function formatBetaChannelFallbackOutcomeSuffix(params: {
|
||||
fallbackLabel: string | undefined;
|
||||
fallbackSpec: string | undefined;
|
||||
@@ -990,7 +1039,11 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
return await installPluginFromNpmSpec(installParams);
|
||||
};
|
||||
|
||||
const recordFailure = (pluginId: string, message: string) => {
|
||||
const recordFailure = (
|
||||
pluginId: string,
|
||||
message: string,
|
||||
channelFallback?: PluginUpdateChannelFallback,
|
||||
) => {
|
||||
if (params.disableOnFailure && !params.dryRun) {
|
||||
const disabledMessage =
|
||||
`Disabled "${pluginId}" after plugin update failure; OpenClaw will continue without it. ` +
|
||||
@@ -1002,6 +1055,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
pluginId,
|
||||
status: "skipped",
|
||||
message: disabledMessage,
|
||||
...(channelFallback ? { channelFallback } : {}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -1009,6 +1063,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
pluginId,
|
||||
status: "error",
|
||||
message,
|
||||
...(channelFallback ? { channelFallback } : {}),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1317,6 +1372,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
let usedNpmFallback = false;
|
||||
let usedOfficialNpmFallback = false;
|
||||
let channelFallbackSuffix = "";
|
||||
let npmChannelFallback: PluginUpdateChannelFallback | undefined;
|
||||
if (!probe.ok && record.source === "npm" && npmSpecs?.fallbackSpec) {
|
||||
logger.warn?.(
|
||||
describeBetaNpmFallback({
|
||||
@@ -1327,6 +1383,13 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
}),
|
||||
);
|
||||
usedNpmFallback = true;
|
||||
npmChannelFallback = describeNpmChannelFallback({
|
||||
pluginId,
|
||||
requestedSpec: npmSpecs.fallbackLabel ?? effectiveSpec,
|
||||
usedSpec: npmSpecs.fallbackSpec,
|
||||
result: probe,
|
||||
verb: "would use",
|
||||
});
|
||||
channelFallbackSuffix = formatBetaChannelFallbackOutcomeSuffix({
|
||||
fallbackLabel: npmSpecs.fallbackLabel ?? effectiveSpec,
|
||||
fallbackSpec: npmSpecs.fallbackSpec,
|
||||
@@ -1448,6 +1511,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
phase: "check",
|
||||
error: probe.error,
|
||||
}),
|
||||
npmChannelFallback,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@@ -1485,6 +1549,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
currentVersion: currentVersion ?? undefined,
|
||||
nextVersion: resolvedProbeVersion,
|
||||
message: `${pluginId} is up to date (${currentLabel}).${channelFallbackSuffix}`,
|
||||
...(npmChannelFallback ? { channelFallback: npmChannelFallback } : {}),
|
||||
});
|
||||
} else {
|
||||
outcomes.push({
|
||||
@@ -1493,6 +1558,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
currentVersion: currentVersion ?? undefined,
|
||||
nextVersion: resolvedProbeVersion,
|
||||
message: `Would update ${pluginId}: ${currentLabel} -> ${nextVersion}.${channelFallbackSuffix}`,
|
||||
...(npmChannelFallback ? { channelFallback: npmChannelFallback } : {}),
|
||||
});
|
||||
}
|
||||
continue;
|
||||
@@ -1567,6 +1633,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
let channelFallbackSuffix = "";
|
||||
let resultSource = record.source;
|
||||
activeClawHubInstallSpec = effectiveSpec;
|
||||
let npmChannelFallback: PluginUpdateChannelFallback | undefined;
|
||||
if (!result.ok && record.source === "npm" && npmSpecs?.fallbackSpec) {
|
||||
logger.warn?.(
|
||||
describeBetaNpmFallback({
|
||||
@@ -1577,6 +1644,13 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
}),
|
||||
);
|
||||
usedNpmFallback = true;
|
||||
npmChannelFallback = describeNpmChannelFallback({
|
||||
pluginId,
|
||||
requestedSpec: npmSpecs.fallbackLabel ?? effectiveSpec,
|
||||
usedSpec: npmSpecs.fallbackSpec,
|
||||
result,
|
||||
verb: "used",
|
||||
});
|
||||
channelFallbackSuffix = formatBetaChannelFallbackOutcomeSuffix({
|
||||
fallbackLabel: npmSpecs.fallbackLabel ?? effectiveSpec,
|
||||
fallbackSpec: npmSpecs.fallbackSpec,
|
||||
@@ -1696,6 +1770,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
phase: "update",
|
||||
error: result.error,
|
||||
}),
|
||||
npmChannelFallback,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@@ -1776,6 +1851,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
currentVersion: currentVersion ?? undefined,
|
||||
nextVersion: nextVersion ?? undefined,
|
||||
message: `${pluginId} already at ${currentLabel}.${channelFallbackSuffix}`,
|
||||
...(npmChannelFallback ? { channelFallback: npmChannelFallback } : {}),
|
||||
});
|
||||
} else {
|
||||
outcomes.push({
|
||||
@@ -1784,6 +1860,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
currentVersion: currentVersion ?? undefined,
|
||||
nextVersion: nextVersion ?? undefined,
|
||||
message: `Updated ${pluginId}: ${currentLabel} -> ${nextLabel}.${channelFallbackSuffix}`,
|
||||
...(npmChannelFallback ? { channelFallback: npmChannelFallback } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user