mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-26 05:31:12 +00:00
* refactor(plugins): split update lifecycle * fix(plugins): break update type cycle * fix(plugins): keep update modules below line budget * fix(plugins): preserve update export surface
744 lines
24 KiB
TypeScript
744 lines
24 KiB
TypeScript
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
|
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
|
import type { ClawHubTrustErrorCode } from "../infra/clawhub-install-trust.js";
|
|
import { parseClawHubPluginSpec } from "../infra/clawhub-spec.js";
|
|
import { satisfiesPluginApiRange } from "../infra/clawhub.js";
|
|
import { unscopedPackageName } from "../infra/install-safe-path.js";
|
|
import type { NpmSpecResolution } from "../infra/install-source-utils.js";
|
|
import { createNpmMetadataEnv, resolveNpmSpecMetadata } from "../infra/install-source-utils.js";
|
|
import {
|
|
compareOpenClawReleaseVersions,
|
|
isExactSemverVersion,
|
|
isPrereleaseResolutionAllowed,
|
|
isPrereleaseSemverVersion,
|
|
parseRegistryNpmSpec,
|
|
} from "../infra/npm-registry-spec.js";
|
|
import { expectedIntegrityForUpdate } from "../infra/package-update-utils.js";
|
|
import { compareValidSemver } from "../infra/semver.js";
|
|
import type { UpdateChannel } from "../infra/update-channels.js";
|
|
import { runCommandWithTimeout } from "../process/exec.js";
|
|
import { resolveCompatibilityHostVersion } from "../version.js";
|
|
import { CLAWHUB_INSTALL_ERROR_CODE } from "./clawhub-error-codes.js";
|
|
import {
|
|
getExternalizedBundledPluginClawHubSpec,
|
|
getExternalizedBundledPluginNpmSpec,
|
|
getExternalizedBundledPluginPreferredSource,
|
|
type ExternalizedBundledPluginBridge,
|
|
} from "./externalized-bundled-plugins.js";
|
|
import {
|
|
resolveClawHubInstallSpecsForUpdateChannel,
|
|
resolveNpmInstallSpecsForUpdateChannel,
|
|
} from "./install-channel-specs.js";
|
|
import { PLUGIN_INSTALL_ERROR_CODE } from "./install.js";
|
|
import { checkMinHostVersion } from "./min-host-version.js";
|
|
import { resolveTrustedSourceLinkedOfficialNpmSpec } from "./official-external-install-records.js";
|
|
import {
|
|
getOfficialExternalPluginCatalogEntry,
|
|
resolveOfficialExternalPluginInstall,
|
|
} from "./official-external-plugin-catalog.js";
|
|
import { resolvePackagePluginApiRange } from "./package-compat.js";
|
|
|
|
/** Logger surface used by plugin update flows. */
|
|
export type PluginUpdateLogger = {
|
|
info?: (message: string) => void;
|
|
warn?: (message: string) => void;
|
|
error?: (message: string) => void;
|
|
terminalLinks?: boolean;
|
|
};
|
|
|
|
type PluginUpdateStatus = "updated" | "unchanged" | "skipped" | "error";
|
|
|
|
export type PluginUpdateChannelFallback = {
|
|
requestedSpec: string;
|
|
usedSpec: string;
|
|
requestedLabel: string;
|
|
usedLabel: string;
|
|
reason: "unavailable" | "failed";
|
|
message: string;
|
|
};
|
|
|
|
type BasePluginUpdateOutcome = {
|
|
pluginId: string;
|
|
message: string;
|
|
currentVersion?: string;
|
|
nextVersion?: string;
|
|
channelFallback?: PluginUpdateChannelFallback;
|
|
warning?: string;
|
|
};
|
|
|
|
export type PluginUpdateOutcome =
|
|
| (BasePluginUpdateOutcome & {
|
|
status: "skipped";
|
|
code?: ClawHubTrustErrorCode;
|
|
})
|
|
| (BasePluginUpdateOutcome & {
|
|
status: Exclude<PluginUpdateStatus, "skipped">;
|
|
code?: string;
|
|
});
|
|
|
|
export type PluginUpdateSummary = {
|
|
config: OpenClawConfig;
|
|
changed: boolean;
|
|
outcomes: PluginUpdateOutcome[];
|
|
};
|
|
|
|
export type PluginUpdateIntegrityDriftParams = {
|
|
pluginId: string;
|
|
spec: string;
|
|
expectedIntegrity: string;
|
|
actualIntegrity: string;
|
|
resolvedSpec?: string;
|
|
resolvedVersion?: string;
|
|
dryRun: boolean;
|
|
};
|
|
|
|
export type UpdatablePluginInstallRecord = PluginInstallRecord & {
|
|
source: "npm" | "marketplace" | "clawhub" | "git";
|
|
};
|
|
|
|
export function isPluginInstallRecordUpdateSource(
|
|
record: PluginInstallRecord | undefined,
|
|
): record is UpdatablePluginInstallRecord {
|
|
return (
|
|
record?.source === "npm" ||
|
|
record?.source === "marketplace" ||
|
|
record?.source === "clawhub" ||
|
|
record?.source === "git"
|
|
);
|
|
}
|
|
|
|
/** Return whether update identity compatibility can migrate an unscoped install key. */
|
|
export function pluginInstallRecordMayMigrateConfigId(params: {
|
|
pluginId: string;
|
|
record: PluginInstallRecord | undefined;
|
|
specOverride?: string;
|
|
}): boolean {
|
|
if (!isPluginInstallRecordUpdateSource(params.record)) {
|
|
return false;
|
|
}
|
|
if (params.record?.source !== "npm") {
|
|
// Generic package/archive installers can resolve an unscoped tracked key
|
|
// to a scoped package id; the exact package identity is unavailable preflight.
|
|
return !params.pluginId.includes("/");
|
|
}
|
|
const packageName =
|
|
resolveNpmSpecPackageName(params.specOverride ?? params.record.spec) ??
|
|
params.record.resolvedName ??
|
|
resolveNpmSpecPackageName(params.record.resolvedSpec);
|
|
return Boolean(
|
|
packageName &&
|
|
packageName !== params.pluginId &&
|
|
unscopedPackageName(packageName) === params.pluginId,
|
|
);
|
|
}
|
|
|
|
export function shouldSkipUnchangedNpmInstall(params: {
|
|
currentVersion?: string;
|
|
record: {
|
|
integrity?: string;
|
|
shasum?: string;
|
|
resolvedName?: string;
|
|
resolvedSpec?: string;
|
|
resolvedVersion?: string;
|
|
};
|
|
metadata: NpmSpecResolution;
|
|
}): boolean {
|
|
if (!params.currentVersion || !params.metadata.version) {
|
|
return false;
|
|
}
|
|
if (params.currentVersion !== params.metadata.version) {
|
|
return false;
|
|
}
|
|
if (
|
|
!params.record.resolvedName ||
|
|
!params.record.resolvedSpec ||
|
|
!params.record.resolvedVersion
|
|
) {
|
|
return false;
|
|
}
|
|
if (!params.metadata.name || !params.metadata.resolvedSpec) {
|
|
return false;
|
|
}
|
|
if (params.metadata.integrity && !params.record.integrity) {
|
|
return false;
|
|
}
|
|
if (params.metadata.shasum && !params.record.shasum) {
|
|
return false;
|
|
}
|
|
return (
|
|
(!params.metadata.integrity || params.record.integrity === params.metadata.integrity) &&
|
|
(!params.metadata.shasum || params.record.shasum === params.metadata.shasum) &&
|
|
params.record.resolvedName === params.metadata.name &&
|
|
params.record.resolvedSpec === params.metadata.resolvedSpec &&
|
|
params.record.resolvedVersion === params.metadata.version
|
|
);
|
|
}
|
|
|
|
export function shouldBypassTrustedOfficialUnchangedNpmCheck(params: {
|
|
metadata: NpmSpecResolution;
|
|
spec: string;
|
|
trustedSourceLinkedOfficialInstall: boolean;
|
|
}): boolean {
|
|
if (!params.trustedSourceLinkedOfficialInstall || !params.metadata.version) {
|
|
return false;
|
|
}
|
|
const parsedSpec = parseRegistryNpmSpec(params.spec);
|
|
return Boolean(
|
|
parsedSpec &&
|
|
!isPrereleaseResolutionAllowed({
|
|
spec: parsedSpec,
|
|
resolvedVersion: params.metadata.version,
|
|
}),
|
|
);
|
|
}
|
|
|
|
export function expectedIntegrityForNpmUpdate(params: {
|
|
effectiveSpec: string | undefined;
|
|
metadata?: NpmSpecResolution;
|
|
record: PluginInstallRecord;
|
|
trustedSourceLinkedOfficialInstall: boolean;
|
|
}): string | undefined {
|
|
if (params.record.source !== "npm") {
|
|
return undefined;
|
|
}
|
|
if (params.effectiveSpec === params.record.spec) {
|
|
return expectedIntegrityForUpdate(params.record.spec, params.record.integrity);
|
|
}
|
|
if (!params.trustedSourceLinkedOfficialInstall || !params.metadata) {
|
|
return undefined;
|
|
}
|
|
const metadataName = params.metadata.name ?? resolveNpmSpecPackageName(params.effectiveSpec);
|
|
const recordName =
|
|
params.record.resolvedName ??
|
|
resolveNpmSpecPackageName(params.record.resolvedSpec) ??
|
|
resolveNpmSpecPackageName(params.record.spec);
|
|
if (!metadataName || metadataName !== recordName) {
|
|
return undefined;
|
|
}
|
|
if (!params.metadata.version || params.metadata.version !== params.record.resolvedVersion) {
|
|
return undefined;
|
|
}
|
|
return expectedIntegrityForUpdate(
|
|
params.record.resolvedSpec ?? params.record.spec,
|
|
params.record.integrity,
|
|
);
|
|
}
|
|
|
|
function compareNpmSemverForUpdate(left: string, right: string): number {
|
|
const releaseCmp = compareOpenClawReleaseVersions(left, right);
|
|
if (releaseCmp !== null) {
|
|
return releaseCmp;
|
|
}
|
|
return compareValidSemver(left, right) ?? 0;
|
|
}
|
|
|
|
export async function resolveNewerExactPinnedNpmDefaultLine(params: {
|
|
currentVersion: string | undefined;
|
|
effectiveSpec: string | undefined;
|
|
probeNpmVersion: string | undefined;
|
|
timeoutMs?: number;
|
|
}): Promise<{ packageName: string; version: string } | undefined> {
|
|
if (!params.currentVersion || !params.probeNpmVersion || !params.effectiveSpec) {
|
|
return undefined;
|
|
}
|
|
const packageName = resolveNpmSpecPackageName(params.effectiveSpec);
|
|
const exactVersion = resolveExactNpmSpecVersion(params.effectiveSpec);
|
|
const probeNpmVersion = normalizeExactNpmVersion(params.probeNpmVersion);
|
|
if (!packageName || !exactVersion || probeNpmVersion !== exactVersion) {
|
|
return undefined;
|
|
}
|
|
|
|
const metadataResult = await resolveNpmSpecMetadata({
|
|
spec: packageName,
|
|
timeoutMs: params.timeoutMs,
|
|
}).catch(() => undefined);
|
|
if (
|
|
!metadataResult?.ok ||
|
|
metadataResult.metadata.name !== packageName ||
|
|
!metadataResult.metadata.version
|
|
) {
|
|
return undefined;
|
|
}
|
|
return compareNpmSemverForUpdate(metadataResult.metadata.version, params.currentVersion) > 0
|
|
? { packageName, version: metadataResult.metadata.version }
|
|
: undefined;
|
|
}
|
|
|
|
async function loadNpmPackageVersionsForUpdate(params: {
|
|
packageName: string;
|
|
timeoutMs?: number;
|
|
}): Promise<string[] | null> {
|
|
const versions = await runCommandWithTimeout(
|
|
["npm", "view", params.packageName, "versions", "--json"],
|
|
{
|
|
timeoutMs: Math.max(params.timeoutMs ?? 0, 60_000),
|
|
env: createNpmMetadataEnv(),
|
|
},
|
|
);
|
|
if (!versions || versions.code !== 0) {
|
|
return null;
|
|
}
|
|
|
|
let parsed: unknown;
|
|
try {
|
|
parsed = JSON.parse(versions.stdout.trim());
|
|
} catch {
|
|
return null;
|
|
}
|
|
return (Array.isArray(parsed) ? parsed : [parsed]).filter(
|
|
(value): value is string => typeof value === "string" && isExactSemverVersion(value),
|
|
);
|
|
}
|
|
|
|
export async function resolveTrustedOfficialPrereleaseFallbackMetadataForUpdate(params: {
|
|
metadata: NpmSpecResolution;
|
|
spec: string;
|
|
timeoutMs?: number;
|
|
}): Promise<
|
|
| {
|
|
kind: "stable" | "prerelease-only";
|
|
metadata: NpmSpecResolution;
|
|
}
|
|
| undefined
|
|
> {
|
|
const parsedSpec = parseRegistryNpmSpec(params.spec);
|
|
if (
|
|
!parsedSpec ||
|
|
!parsedSpec.name.startsWith("@openclaw/") ||
|
|
!params.metadata.version ||
|
|
isPrereleaseResolutionAllowed({
|
|
spec: parsedSpec,
|
|
resolvedVersion: params.metadata.version,
|
|
})
|
|
) {
|
|
return undefined;
|
|
}
|
|
const versions = await loadNpmPackageVersionsForUpdate({
|
|
packageName: parsedSpec.name,
|
|
timeoutMs: params.timeoutMs,
|
|
});
|
|
const stableVersion = versions
|
|
?.filter((value) => !isPrereleaseSemverVersion(value))
|
|
.toSorted(compareNpmSemverForUpdate)
|
|
.at(-1);
|
|
if (stableVersion) {
|
|
const stableMetadata = await resolveNpmSpecMetadata({
|
|
spec: `${parsedSpec.name}@${stableVersion}`,
|
|
timeoutMs: params.timeoutMs,
|
|
});
|
|
return stableMetadata.ok ? { kind: "stable", metadata: stableMetadata.metadata } : undefined;
|
|
}
|
|
|
|
const prereleaseVersion = versions
|
|
?.filter(isPrereleaseSemverVersion)
|
|
.toSorted(compareNpmSemverForUpdate)
|
|
.at(-1);
|
|
if (!prereleaseVersion || !versions?.every(isPrereleaseSemverVersion)) {
|
|
return undefined;
|
|
}
|
|
if (prereleaseVersion === params.metadata.version) {
|
|
return { kind: "prerelease-only", metadata: params.metadata };
|
|
}
|
|
const prereleaseMetadata = await resolveNpmSpecMetadata({
|
|
spec: `${parsedSpec.name}@${prereleaseVersion}`,
|
|
timeoutMs: params.timeoutMs,
|
|
});
|
|
return prereleaseMetadata.ok
|
|
? { kind: "prerelease-only", metadata: prereleaseMetadata.metadata }
|
|
: undefined;
|
|
}
|
|
|
|
export async function expectedIntegrityForNpmFallback(params: {
|
|
fallbackSpec: string | undefined;
|
|
record: PluginInstallRecord;
|
|
timeoutMs?: number;
|
|
trustedSourceLinkedOfficialInstall: boolean;
|
|
}): Promise<string | undefined> {
|
|
if (params.record.source !== "npm" || !params.fallbackSpec) {
|
|
return undefined;
|
|
}
|
|
if (params.fallbackSpec === params.record.spec) {
|
|
return expectedIntegrityForUpdate(params.record.spec, params.record.integrity);
|
|
}
|
|
if (!params.trustedSourceLinkedOfficialInstall) {
|
|
return undefined;
|
|
}
|
|
const fallbackMetadata = await resolveNpmSpecMetadata({
|
|
spec: params.fallbackSpec,
|
|
timeoutMs: params.timeoutMs,
|
|
});
|
|
if (!fallbackMetadata.ok) {
|
|
return undefined;
|
|
}
|
|
const trustedPrereleaseFallback = await resolveTrustedOfficialPrereleaseFallbackMetadataForUpdate(
|
|
{
|
|
metadata: fallbackMetadata.metadata,
|
|
spec: params.fallbackSpec,
|
|
timeoutMs: params.timeoutMs,
|
|
},
|
|
);
|
|
const expectedIntegrityMetadata =
|
|
trustedPrereleaseFallback?.metadata ?? fallbackMetadata.metadata;
|
|
if (!isNpmMetadataCompatibleWithCurrentHost(expectedIntegrityMetadata)) {
|
|
return undefined;
|
|
}
|
|
return expectedIntegrityForNpmUpdate({
|
|
effectiveSpec: params.fallbackSpec,
|
|
metadata: expectedIntegrityMetadata,
|
|
record: params.record,
|
|
trustedSourceLinkedOfficialInstall: true,
|
|
});
|
|
}
|
|
|
|
export function isNpmMetadataCompatibleWithCurrentHost(metadata: NpmSpecResolution): boolean {
|
|
const hostVersion = resolveCompatibilityHostVersion();
|
|
const installMetadata = metadata.packageOpenClaw?.install;
|
|
const minHostVersionCheck = checkMinHostVersion({
|
|
currentVersion: hostVersion,
|
|
minHostVersion: isRecord(installMetadata) ? installMetadata.minHostVersion : undefined,
|
|
});
|
|
if (!minHostVersionCheck.ok) {
|
|
return false;
|
|
}
|
|
const pluginApiRangeCheck = resolvePackagePluginApiRange(metadata.packageOpenClaw);
|
|
if (!pluginApiRangeCheck.ok) {
|
|
return false;
|
|
}
|
|
const pluginApiRange = pluginApiRangeCheck.range;
|
|
if (!pluginApiRange) {
|
|
return true;
|
|
}
|
|
return satisfiesPluginApiRange(hostVersion, pluginApiRange);
|
|
}
|
|
|
|
export function isBundledVersionNewer(bundledVersion: string, installedVersion: string): boolean {
|
|
const releaseCmp = compareOpenClawReleaseVersions(bundledVersion, installedVersion);
|
|
if (releaseCmp !== null) {
|
|
return releaseCmp > 0;
|
|
}
|
|
return (compareValidSemver(bundledVersion, installedVersion) ?? 0) > 0;
|
|
}
|
|
|
|
function shouldFallbackClawHubToDefault(result: { ok: false; code?: string }): boolean {
|
|
return (
|
|
result.code === CLAWHUB_INSTALL_ERROR_CODE.PACKAGE_NOT_FOUND ||
|
|
result.code === CLAWHUB_INSTALL_ERROR_CODE.VERSION_NOT_FOUND
|
|
);
|
|
}
|
|
|
|
export function shouldFallbackBetaClawHubUpdate(result: { ok: false; code?: string }): boolean {
|
|
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)
|
|
);
|
|
}
|
|
|
|
export function describeBetaNpmFallback(params: {
|
|
pluginId: string;
|
|
betaSpec: string | undefined;
|
|
fallbackSpec: string;
|
|
result: { ok: false; code?: string; error: string };
|
|
}): string {
|
|
const betaSpec = params.betaSpec ?? "the beta npm release";
|
|
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}`;
|
|
}
|
|
|
|
export 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,
|
|
};
|
|
}
|
|
|
|
export function formatBetaChannelFallbackOutcomeSuffix(params: {
|
|
fallbackLabel: string | undefined;
|
|
fallbackSpec: string | undefined;
|
|
verb: "used" | "would use";
|
|
}): string {
|
|
if (!params.fallbackSpec) {
|
|
return "";
|
|
}
|
|
const betaTarget = params.fallbackLabel ?? "beta target";
|
|
return ` (warning: beta channel fallback ${params.verb} ${params.fallbackSpec} because ${betaTarget} could not be used).`;
|
|
}
|
|
|
|
export function npmUpdateFailureSpec(params: {
|
|
effectiveSpec: string | undefined;
|
|
fallbackSpec: string | undefined;
|
|
usedFallback: boolean;
|
|
}): string {
|
|
if (params.usedFallback && params.fallbackSpec) {
|
|
return params.fallbackSpec;
|
|
}
|
|
return params.effectiveSpec ?? params.fallbackSpec ?? "unknown";
|
|
}
|
|
|
|
export function resolveNpmSpecPackageName(spec: string | undefined): string | undefined {
|
|
return spec ? parseRegistryNpmSpec(spec)?.name : undefined;
|
|
}
|
|
|
|
export function resolveExactNpmSpecVersion(spec: string | undefined): string | undefined {
|
|
const parsed = spec ? parseRegistryNpmSpec(spec) : null;
|
|
return parsed?.selectorKind === "exact-version"
|
|
? normalizeExactNpmVersion(parsed.selector)
|
|
: undefined;
|
|
}
|
|
|
|
function normalizeExactNpmVersion(value: string | undefined): string | undefined {
|
|
if (!value) {
|
|
return undefined;
|
|
}
|
|
const trimmed = value.trim();
|
|
if (!isExactSemverVersion(trimmed)) {
|
|
return undefined;
|
|
}
|
|
return trimmed.startsWith("v") ? trimmed.slice(1) : trimmed;
|
|
}
|
|
|
|
export function resolveNpmResultVersion(result: {
|
|
npmResolution?: NpmSpecResolution;
|
|
}): string | undefined {
|
|
return result.npmResolution?.version;
|
|
}
|
|
|
|
function resolveClawHubSpecPackageName(spec: string | undefined): string | undefined {
|
|
return spec ? parseClawHubPluginSpec(spec)?.name : undefined;
|
|
}
|
|
|
|
function isOfficialClawHubInstallRecord(record: PluginInstallRecord): boolean {
|
|
if (record.source !== "clawhub" || record.clawhubChannel !== "official") {
|
|
return false;
|
|
}
|
|
return (record.clawhubUrl ?? "").replace(/\/+$/, "") === "https://clawhub.ai";
|
|
}
|
|
|
|
export function resolveTrustedSourceLinkedOfficialNpmFallbackForClawHubUpdate(params: {
|
|
pluginId: string;
|
|
record: PluginInstallRecord;
|
|
effectiveClawHubSpec?: string;
|
|
recordClawHubSpec?: string;
|
|
updateChannel?: UpdateChannel;
|
|
coreVersion?: string;
|
|
}): {
|
|
installSpec: string;
|
|
recordSpec: string;
|
|
fallbackSpec?: string;
|
|
fallbackLabel?: string;
|
|
} | null {
|
|
if (!isOfficialClawHubInstallRecord(params.record)) {
|
|
return null;
|
|
}
|
|
const entry = getOfficialExternalPluginCatalogEntry(params.pluginId);
|
|
if (!entry) {
|
|
return null;
|
|
}
|
|
const officialSpec = resolveOfficialExternalPluginInstall(entry)?.npmSpec;
|
|
const officialPackageName = resolveNpmSpecPackageName(officialSpec);
|
|
if (!officialSpec || !officialPackageName) {
|
|
return null;
|
|
}
|
|
const recordedPackageNames = [
|
|
params.record.clawhubPackage,
|
|
resolveClawHubSpecPackageName(params.record.spec),
|
|
resolveClawHubSpecPackageName(params.effectiveClawHubSpec),
|
|
].filter((value): value is string => Boolean(value));
|
|
if (!recordedPackageNames.includes(officialPackageName)) {
|
|
return null;
|
|
}
|
|
|
|
const effectiveClawHubVersion = params.effectiveClawHubSpec
|
|
? parseClawHubPluginSpec(params.effectiveClawHubSpec)?.version
|
|
: undefined;
|
|
const recordClawHubVersion = params.recordClawHubSpec
|
|
? parseClawHubPluginSpec(params.recordClawHubSpec)?.version
|
|
: undefined;
|
|
if (effectiveClawHubVersion && effectiveClawHubVersion.toLowerCase() !== "latest") {
|
|
return {
|
|
installSpec: `${officialPackageName}@${effectiveClawHubVersion}`,
|
|
recordSpec:
|
|
recordClawHubVersion && recordClawHubVersion.toLowerCase() !== "latest"
|
|
? `${officialPackageName}@${recordClawHubVersion}`
|
|
: officialSpec,
|
|
...(params.updateChannel === "beta" && effectiveClawHubVersion.toLowerCase() === "beta"
|
|
? { fallbackSpec: officialSpec, fallbackLabel: `${officialPackageName}@beta` }
|
|
: {}),
|
|
};
|
|
}
|
|
return resolveNpmInstallSpecsForUpdateChannel({
|
|
spec: officialSpec,
|
|
updateChannel: params.updateChannel,
|
|
officialPackageName,
|
|
coreVersion: params.coreVersion,
|
|
});
|
|
}
|
|
|
|
export function isTrustedSourceLinkedOfficialNpmUpdate(params: {
|
|
pluginId: string;
|
|
spec: string | undefined;
|
|
record: PluginInstallRecord;
|
|
}): boolean {
|
|
const officialSpec = resolveTrustedSourceLinkedOfficialNpmSpec(params);
|
|
const officialPackageName = resolveNpmSpecPackageName(officialSpec);
|
|
const requestedPackageName = resolveNpmSpecPackageName(params.spec);
|
|
return Boolean(officialPackageName && requestedPackageName === officialPackageName);
|
|
}
|
|
|
|
export function isTrustedSourceLinkedOfficialBridgeNpmInstall(params: {
|
|
targetPluginId: string;
|
|
npmSpec: string | undefined;
|
|
}): boolean {
|
|
const entry = getOfficialExternalPluginCatalogEntry(params.targetPluginId);
|
|
if (!entry) {
|
|
return false;
|
|
}
|
|
const officialPackageName = resolveNpmSpecPackageName(
|
|
resolveOfficialExternalPluginInstall(entry)?.npmSpec,
|
|
);
|
|
const requestedPackageName = resolveNpmSpecPackageName(params.npmSpec);
|
|
return Boolean(officialPackageName && requestedPackageName === officialPackageName);
|
|
}
|
|
|
|
function isBridgeNpmInstall(params: {
|
|
bridge: ExternalizedBundledPluginBridge;
|
|
record: PluginInstallRecord;
|
|
}): boolean {
|
|
const npmSpec = getExternalizedBundledPluginNpmSpec(params.bridge);
|
|
if (!npmSpec || params.record.source !== "npm") {
|
|
return false;
|
|
}
|
|
const bridgePackageName = resolveNpmSpecPackageName(npmSpec);
|
|
const recordPackageName =
|
|
params.record.resolvedName ??
|
|
resolveNpmSpecPackageName(params.record.spec) ??
|
|
resolveNpmSpecPackageName(params.record.resolvedSpec);
|
|
return Boolean(bridgePackageName && recordPackageName === bridgePackageName);
|
|
}
|
|
|
|
function isBridgeClawHubInstall(params: {
|
|
bridge: ExternalizedBundledPluginBridge;
|
|
record: PluginInstallRecord;
|
|
}): boolean {
|
|
if (params.record.source !== "clawhub") {
|
|
return false;
|
|
}
|
|
const clawhubSpec = getExternalizedBundledPluginClawHubSpec(params.bridge);
|
|
const bridgeClawHubPackage = clawhubSpec ? parseClawHubPluginSpec(clawhubSpec)?.name : undefined;
|
|
const recordClawHubPackage =
|
|
params.record.clawhubPackage ?? parseClawHubPluginSpec(params.record.spec ?? "")?.name;
|
|
return Boolean(bridgeClawHubPackage && recordClawHubPackage === bridgeClawHubPackage);
|
|
}
|
|
|
|
export function resolveNpmUpdateSpecs(params: {
|
|
record: PluginInstallRecord;
|
|
specOverride?: string;
|
|
officialSpecOverride?: string;
|
|
updateChannel?: UpdateChannel;
|
|
officialPackageName?: string;
|
|
coreVersion?: string;
|
|
}): {
|
|
installSpec?: string;
|
|
recordSpec?: string;
|
|
fallbackSpec?: string;
|
|
fallbackLabel?: string;
|
|
} {
|
|
const recordSpec =
|
|
params.specOverride ??
|
|
(params.updateChannel === "extended-stable" && params.record.spec
|
|
? params.record.spec
|
|
: (params.officialSpecOverride ?? params.record.spec));
|
|
if (!recordSpec) {
|
|
return {};
|
|
}
|
|
if (params.specOverride) {
|
|
return resolveNpmInstallSpecsForUpdateChannel({
|
|
spec: recordSpec,
|
|
updateChannel: params.updateChannel,
|
|
officialPackageName: params.officialPackageName,
|
|
coreVersion: params.coreVersion,
|
|
});
|
|
}
|
|
return resolveNpmInstallSpecsForUpdateChannel({
|
|
spec: recordSpec,
|
|
updateChannel: params.updateChannel,
|
|
officialPackageName: params.officialPackageName,
|
|
coreVersion: params.coreVersion,
|
|
});
|
|
}
|
|
|
|
export function resolveClawHubUpdateSpecs(params: {
|
|
record: PluginInstallRecord;
|
|
officialSpecOverride?: string;
|
|
updateChannel?: UpdateChannel;
|
|
}): {
|
|
installSpec?: string;
|
|
recordSpec?: string;
|
|
fallbackSpec?: string;
|
|
fallbackLabel?: string;
|
|
} {
|
|
if (!params.officialSpecOverride && !params.record.clawhubPackage) {
|
|
return {};
|
|
}
|
|
const recordSpec =
|
|
params.officialSpecOverride ?? params.record.spec ?? `clawhub:${params.record.clawhubPackage}`;
|
|
return resolveClawHubInstallSpecsForUpdateChannel({
|
|
spec: recordSpec,
|
|
updateChannel: params.updateChannel,
|
|
});
|
|
}
|
|
|
|
export function isBridgeAlreadyInstalledFromPreferredSource(params: {
|
|
bridge: ExternalizedBundledPluginBridge;
|
|
record: PluginInstallRecord;
|
|
}): boolean {
|
|
const preferredSource = getExternalizedBundledPluginPreferredSource(params.bridge);
|
|
return preferredSource === "clawhub"
|
|
? isBridgeClawHubInstall(params)
|
|
: isBridgeNpmInstall(params);
|
|
}
|
|
|
|
export function isBridgeInstalledFromFallbackSource(params: {
|
|
bridge: ExternalizedBundledPluginBridge;
|
|
record: PluginInstallRecord;
|
|
}): boolean {
|
|
const preferredSource = getExternalizedBundledPluginPreferredSource(params.bridge);
|
|
return preferredSource === "clawhub"
|
|
? isBridgeNpmInstall(params)
|
|
: isBridgeClawHubInstall(params);
|
|
}
|