Files
openclaw/src/plugins/install-source-info.ts
2026-05-02 07:27:20 -07:00

165 lines
5.2 KiB
TypeScript

import { parseClawHubPluginSpec } from "../infra/clawhub-spec.js";
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-clawhub-spec"
| "invalid-npm-spec"
| "invalid-default-choice"
| "default-choice-missing-source"
| "clawhub-spec-floating"
| "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 PluginInstallClawHubSourceInfo = {
spec: string;
packageName: string;
version?: string;
exactVersion: boolean;
};
export type PluginInstallSourceInfo = {
defaultChoice?: PluginPackageInstall["defaultChoice"];
clawhub?: PluginInstallClawHubSourceInfo;
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 === "clawhub" || 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 clawhubSpec = normalizeOptionalString(install.clawhubSpec);
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 clawhub: PluginInstallClawHubSourceInfo | undefined;
let npm: PluginInstallNpmSourceInfo | undefined;
if (install.defaultChoice !== undefined && !defaultChoice) {
warnings.push("invalid-default-choice");
}
if (clawhubSpec) {
const parsed = parseClawHubPluginSpec(clawhubSpec);
if (parsed) {
if (!parsed.version) {
warnings.push("clawhub-spec-floating");
}
clawhub = {
spec: clawhubSpec,
packageName: parsed.name,
...(parsed.version ? { version: parsed.version } : {}),
exactVersion: Boolean(parsed.version),
};
} else {
warnings.push("invalid-clawhub-spec");
}
}
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 === "clawhub" && !clawhub) {
warnings.push("default-choice-missing-source");
}
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 } : {}),
...(clawhub ? { clawhub } : {}),
...(npm ? { npm } : {}),
...(localPath ? { local: { path: localPath } } : {}),
warnings,
};
}