mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 17:30:42 +00:00
Warn when provider or channel catalog package identity drifts from openclaw.install.npmSpec while keeping compatible catalogs non-fatal.
131 lines
4.2 KiB
TypeScript
131 lines
4.2 KiB
TypeScript
import { parseRegistryNpmSpec, type ParsedRegistryNpmSpec } from "../infra/npm-registry-spec.js";
|
|
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
|
import type { PluginPackageInstall } from "./manifest.js";
|
|
|
|
export type PluginInstallSourceWarning =
|
|
| "invalid-npm-spec"
|
|
| "invalid-default-choice"
|
|
| "default-choice-missing-source"
|
|
| "npm-integrity-without-source"
|
|
| "npm-spec-floating"
|
|
| "npm-spec-missing-integrity"
|
|
| "npm-spec-package-name-mismatch";
|
|
|
|
export type PluginInstallNpmPinState =
|
|
| "exact-with-integrity"
|
|
| "exact-without-integrity"
|
|
| "floating-with-integrity"
|
|
| "floating-without-integrity";
|
|
|
|
export type PluginInstallNpmSourceInfo = {
|
|
spec: string;
|
|
packageName: string;
|
|
expectedPackageName?: string;
|
|
selector?: string;
|
|
selectorKind: ParsedRegistryNpmSpec["selectorKind"];
|
|
exactVersion: boolean;
|
|
expectedIntegrity?: string;
|
|
pinState: PluginInstallNpmPinState;
|
|
};
|
|
|
|
export type PluginInstallLocalSourceInfo = {
|
|
path: string;
|
|
};
|
|
|
|
export type PluginInstallSourceInfo = {
|
|
defaultChoice?: PluginPackageInstall["defaultChoice"];
|
|
npm?: PluginInstallNpmSourceInfo;
|
|
local?: PluginInstallLocalSourceInfo;
|
|
warnings: readonly PluginInstallSourceWarning[];
|
|
};
|
|
|
|
export type DescribePluginInstallSourceOptions = {
|
|
expectedPackageName?: string | null;
|
|
};
|
|
|
|
function resolveNpmPinState(params: {
|
|
exactVersion: boolean;
|
|
hasIntegrity: boolean;
|
|
}): PluginInstallNpmPinState {
|
|
if (params.exactVersion) {
|
|
return params.hasIntegrity ? "exact-with-integrity" : "exact-without-integrity";
|
|
}
|
|
return params.hasIntegrity ? "floating-with-integrity" : "floating-without-integrity";
|
|
}
|
|
|
|
function resolveDefaultChoice(value: unknown): PluginPackageInstall["defaultChoice"] | undefined {
|
|
return value === "npm" || value === "local" ? value : undefined;
|
|
}
|
|
|
|
function normalizeExpectedPackageName(value: string | null | undefined): string | undefined {
|
|
const expected = normalizeOptionalString(value);
|
|
if (!expected) {
|
|
return undefined;
|
|
}
|
|
return parseRegistryNpmSpec(expected)?.name ?? expected;
|
|
}
|
|
|
|
export function describePluginInstallSource(
|
|
install: PluginPackageInstall,
|
|
options?: DescribePluginInstallSourceOptions,
|
|
): PluginInstallSourceInfo {
|
|
const npmSpec = normalizeOptionalString(install.npmSpec);
|
|
const localPath = normalizeOptionalString(install.localPath);
|
|
const defaultChoice = resolveDefaultChoice(install.defaultChoice);
|
|
const expectedIntegrity = normalizeOptionalString(install.expectedIntegrity);
|
|
const expectedPackageName = normalizeExpectedPackageName(options?.expectedPackageName);
|
|
const warnings: PluginInstallSourceWarning[] = [];
|
|
let npm: PluginInstallNpmSourceInfo | undefined;
|
|
|
|
if (install.defaultChoice !== undefined && !defaultChoice) {
|
|
warnings.push("invalid-default-choice");
|
|
}
|
|
|
|
if (npmSpec) {
|
|
const parsed = parseRegistryNpmSpec(npmSpec);
|
|
if (parsed) {
|
|
const exactVersion = parsed.selectorKind === "exact-version";
|
|
const hasIntegrity = Boolean(expectedIntegrity);
|
|
if (!exactVersion) {
|
|
warnings.push("npm-spec-floating");
|
|
}
|
|
if (!hasIntegrity) {
|
|
warnings.push("npm-spec-missing-integrity");
|
|
}
|
|
if (expectedPackageName && parsed.name !== expectedPackageName) {
|
|
warnings.push("npm-spec-package-name-mismatch");
|
|
}
|
|
npm = {
|
|
spec: parsed.raw,
|
|
packageName: parsed.name,
|
|
...(expectedPackageName && parsed.name !== expectedPackageName
|
|
? { expectedPackageName }
|
|
: {}),
|
|
selectorKind: parsed.selectorKind,
|
|
exactVersion,
|
|
pinState: resolveNpmPinState({ exactVersion, hasIntegrity }),
|
|
...(parsed.selector ? { selector: parsed.selector } : {}),
|
|
...(expectedIntegrity ? { expectedIntegrity } : {}),
|
|
};
|
|
} else {
|
|
warnings.push("invalid-npm-spec");
|
|
}
|
|
}
|
|
if (defaultChoice === "npm" && !npm) {
|
|
warnings.push("default-choice-missing-source");
|
|
}
|
|
if (defaultChoice === "local" && !localPath) {
|
|
warnings.push("default-choice-missing-source");
|
|
}
|
|
if (expectedIntegrity && !npm) {
|
|
warnings.push("npm-integrity-without-source");
|
|
}
|
|
|
|
return {
|
|
...(defaultChoice ? { defaultChoice } : {}),
|
|
...(npm ? { npm } : {}),
|
|
...(localPath ? { local: { path: localPath } } : {}),
|
|
warnings,
|
|
};
|
|
}
|