openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
664 lines (663 loc) • 27.8 kB
JavaScript
import { o as resolveUserPath } from "./home-dir-Dc6nN6HB.mjs";
import { c as isRecord } from "./record-coerce-TIsquoVR.mjs";
import { y as uniqueStrings } from "./string-normalization-D6DKCpwE.mjs";
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.mjs";
import { A as withTimeout } from "./fs-safe-BBzelG8F.mjs";
import { i as stripAnsi } from "./ansi-DyIsQZAA.mjs";
import { n as sanitizeTerminalText } from "./safe-text-Eo8-QSFS.mjs";
import "./utils-CitNzLyF.mjs";
import { r as isPathInside } from "./path-guards-CH7noY0Z.mjs";
import { t as VERSION } from "./version-D0DaWqMn.mjs";
import { o as parseRegistryNpmSpec } from "./npm-registry-spec-B_IER0ZB.mjs";
import { c as normalizeUpdateChannel, d as resolveRegistryUpdateChannel } from "./update-channels-BLoHtGj7.mjs";
import { n as isUnavailableClawHubTarget } from "./clawhub-error-codes-DV-j2dZ7.mjs";
import { n as isUnavailableNpmTarget } from "./install-types-auyGAGw8.mjs";
import { t as clearPluginMetadataLifecycleCaches } from "./plugin-metadata-lifecycle-COmFOHdR.mjs";
import { a as resolveDefaultPluginExtensionsDir } from "./install-paths-Ik5BUD8W.mjs";
import { A as resolvePluginInstallSources, C as NpmChannelResolutionError, D as resolveClawHubInstallSpecsForUpdateChannel, E as isUnavailablePluginSource, T as installWithSourceFallback, k as resolveNpmInstallSpecsForUpdateChannel, w as installWithChannelFallback } from "./official-external-plugin-catalog-BKfROutK.mjs";
import { t as parseClawHubPluginSpec } from "./clawhub-spec-C_XvO17V.mjs";
import { o as clearLoadInstalledPluginIndexInstallRecordsCache } from "./installed-plugin-index-record-reader-D5Zh_tO0.mjs";
import { t as ManagedPluginLifecycleError } from "./management-lifecycle-error-ySeX1uI2.mjs";
import { t as enableExplicitlySelectedPluginInConfig } from "./enable-DpFYy764.mjs";
import { r as assertConfigWriteAllowedInCurrentMode } from "./config-write-guard-C8fkwYP0.mjs";
import { a as withPluginLifecycleLease } from "./plugin-lifecycle-lease-CJFG12c4.mjs";
import { t as buildNpmResolutionFields } from "./install-source-utils-BYMfWr8X.mjs";
import { c as recordPluginInstall } from "./installed-plugin-index-records-3TTKz0gf.mjs";
import "./with-timeout-BBLF7VHp.mjs";
import { a as installPluginFromNpmPackArchive, r as installPluginFromNpmSpec } from "./install-DH1DMp0M.mjs";
import { a as resolveBundledPluginSources, n as findBundledPluginSourceInMap } from "./bundled-sources-D86vbSpz.mjs";
import { r as prepareManagedPluginArtifactConsentHandler, t as capturePluginCapabilityConsentHandlerErrors } from "./capability-consent-CtWStuOs.mjs";
import { t as buildClawHubPluginInstallRecordFields } from "./clawhub-install-records-Dy2deHKG.mjs";
import { t as invalidatePluginRuntimeDiscoveryAfterConfigMutation } from "./registry-refresh-RTh3c5qs.mjs";
import { r as t } from "./i18n-Du5n3tJe.mjs";
import { n as WizardCancelledError, r as WizardNavigationError } from "./prompts-DLsO8MlU.mjs";
import { t as createPluginCapabilityConsentPrompter } from "./plugin-capability-consent-DOqMNDMz.mjs";
import { n as resolveBundledInstallPlanForCatalogEntry } from "./install-source-plan-DwFNMr7g.mjs";
import fs from "node:fs";
import path from "node:path";
//#region src/plugins/install-overrides.ts
/** Env var containing JSON plugin install override specs. */
const PLUGIN_INSTALL_OVERRIDES_ENV = "OPENCLAW_PLUGIN_INSTALL_OVERRIDES";
/** Env var gate that must be enabled before install overrides are honored. */
const ALLOW_PLUGIN_INSTALL_OVERRIDES_ENV = "OPENCLAW_ALLOW_PLUGIN_INSTALL_OVERRIDES";
function overrideAllowed(env) {
return env[ALLOW_PLUGIN_INSTALL_OVERRIDES_ENV]?.trim() === "1";
}
function parseOverrideSpec(raw) {
const trimmed = raw.trim();
if (!trimmed) return null;
if (trimmed.startsWith("npm:")) {
const spec = trimmed.slice(4).trim();
return spec && parseRegistryNpmSpec(spec) ? {
kind: "npm",
spec
} : null;
}
if (trimmed.startsWith("npm-pack:")) {
const rawPath = trimmed.slice(9).trim();
if (!rawPath) return null;
return {
kind: "npm-pack",
archivePath: resolveUserPath(rawPath)
};
}
return null;
}
/** Resolves a gated plugin install override from environment configuration. */
function resolvePluginInstallOverride(params) {
const env = params.env ?? process.env;
if (!overrideAllowed(env)) return null;
const raw = env[PLUGIN_INSTALL_OVERRIDES_ENV]?.trim();
if (!raw) return null;
let parsed;
try {
parsed = JSON.parse(raw);
} catch {
return null;
}
if (!isRecord(parsed)) return null;
const value = parsed[params.pluginId];
return typeof value === "string" ? parseOverrideSpec(value) : null;
}
//#endregion
//#region src/commands/onboarding-plugin-install.ts
const ONBOARDING_PLUGIN_INSTALL_TIMEOUT_MS = 3e5;
const ONBOARDING_PLUGIN_INSTALL_WATCHDOG_TIMEOUT_MS = 305e3;
function incompletePluginInstall(cfg, pluginId, status, error) {
return {
cfg,
installed: false,
pluginId,
status,
...error === void 0 ? {} : { error }
};
}
function resolveRealDirectory(dir) {
try {
const resolved = fs.realpathSync(dir);
return fs.statSync(resolved).isDirectory() ? resolved : null;
} catch {
return null;
}
}
function resolveGitDirectoryMarker(dir) {
const marker = path.join(dir, ".git");
try {
const stat = fs.statSync(marker);
if (stat.isDirectory()) return resolveRealDirectory(marker);
if (!stat.isFile()) return null;
const content = fs.readFileSync(marker, "utf8").trim();
const gitDir = /^gitdir:\s*(.+)$/i.exec(content)?.[1]?.trim();
if (!gitDir) return null;
return resolveRealDirectory(path.isAbsolute(gitDir) ? gitDir : path.resolve(dir, gitDir));
} catch {
return null;
}
}
function hasTrustedGitWorkspace(root) {
const realRoot = resolveRealDirectory(root);
if (!realRoot) return false;
for (let dir = realRoot;; dir = path.dirname(dir)) {
if (resolveGitDirectoryMarker(dir)) return true;
if (path.dirname(dir) === dir) return false;
}
}
function resolveLocalPluginRoots(workspaceDir) {
const roots = [process.cwd()];
if (workspaceDir && workspaceDir !== process.cwd()) roots.push(workspaceDir);
return roots;
}
function addPluginLoadPath(cfg, pluginPath) {
const existing = cfg.plugins?.load?.paths ?? [];
const merged = uniqueStrings([...existing, pluginPath]);
return {
...cfg,
plugins: {
...cfg.plugins,
load: {
...cfg.plugins?.load,
paths: merged
}
}
};
}
function pathsReferToSameDirectory(left, right) {
if (!left || !right) return false;
const realLeft = resolveRealDirectory(left);
const realRight = resolveRealDirectory(right);
return Boolean(realLeft && realRight && realLeft === realRight);
}
function formatPortableLocalPath(localPath, workspaceDir) {
const bases = [workspaceDir, process.cwd()].filter((entry) => Boolean(entry));
for (const base of bases) {
const realBase = resolveRealDirectory(base);
if (!realBase) continue;
if (isPathInside(realBase, localPath)) {
const portable = path.relative(realBase, localPath).split(path.sep).join("/");
return portable ? `./${portable}` : ".";
}
}
}
function resolveLocalPath(params) {
if (!params.allowLocal) return null;
const raw = params.entry.install.localPath?.trim();
if (!raw) return null;
const candidates = /* @__PURE__ */ new Set();
const bases = resolveLocalPluginRoots(params.workspaceDir);
for (const base of bases) {
const realBase = resolveRealDirectory(base);
if (!realBase) continue;
candidates.add(path.resolve(realBase, raw));
}
for (const candidate of candidates) try {
const resolved = fs.realpathSync(candidate);
if (!bases.some((base) => {
const realBase = resolveRealDirectory(base);
return realBase ? isPathInside(realBase, resolved) : false;
})) continue;
if (fs.statSync(resolved).isDirectory()) return resolved;
} catch {
continue;
}
return null;
}
function resolveBundledLocalPath(params) {
const bundledSources = resolveBundledPluginSources({ workspaceDir: params.workspaceDir });
const npmSpec = params.entry.install.npmSpec?.trim();
if (npmSpec) return resolveBundledInstallPlanForCatalogEntry({
pluginId: params.entry.pluginId,
npmSpec,
findBundledSource: (lookup) => findBundledPluginSourceInMap({
bundled: bundledSources,
lookup
})
})?.bundledSource.localPath ?? null;
return findBundledPluginSourceInMap({
bundled: bundledSources,
lookup: {
kind: "pluginId",
value: params.entry.pluginId
}
})?.localPath ?? null;
}
function resolveInstallDefaultChoice(params) {
const { cfg, entry, localPath, bundledLocalPath, hasClawHubSpec, hasNpmSpec } = params;
const hasRemoteSpec = hasClawHubSpec || hasNpmSpec;
const remoteDefault = () => resolvePluginInstallSources(entry.install)[0]?.source ?? "skip";
if (!hasRemoteSpec) return localPath ? "local" : "skip";
if (!localPath) return remoteDefault();
if (bundledLocalPath) return "local";
switch (cfg.update?.channel) {
case "dev": return "local";
case "stable":
case "extended-stable":
case "beta": return remoteDefault();
default: return entry.install.defaultChoice === "local" ? "local" : remoteDefault();
}
}
async function promptInstallChoice(params) {
const { npmSpec, clawhubSpec } = params;
const safeLabel = sanitizeTerminalText(params.label);
const safeClawHubSpec = clawhubSpec ? sanitizeTerminalText(clawhubSpec) : null;
const safeNpmSpec = npmSpec ? sanitizeTerminalText(npmSpec) : null;
const safeLocalPath = params.localPath ? sanitizeTerminalText(params.localPath) : null;
const options = [];
if (safeNpmSpec) options.push({
value: "npm",
label: t("wizard.plugins.downloadFromNpm", { spec: safeNpmSpec })
});
if (safeClawHubSpec) options.push({
value: "clawhub",
label: t("wizard.plugins.downloadFromClawHub", { spec: safeClawHubSpec })
});
if (params.localPath) options.push({
value: "local",
label: t("wizard.plugins.useLocalPluginPath"),
...safeLocalPath ? { hint: safeLocalPath } : {}
});
if (params.autoConfirmSingleSource && options.length === 1) return options[0].value;
options.push({
value: "skip",
label: t("common.skipForNow")
});
const initialValue = [
params.defaultChoice,
"clawhub",
"npm",
"local",
"skip"
].find((choice) => options.some((option) => option.value === choice));
return await params.prompter.select({
message: t("wizard.plugins.installPluginPrompt", { plugin: safeLabel }),
options,
initialValue
});
}
async function reportPluginInstallTimeout(params, spec) {
const safeSpec = sanitizeTerminalText(spec);
await params.prompter.note([t("wizard.plugins.installTimedOut", {
spec: safeSpec,
duration: t("common.minutes", { count: ONBOARDING_PLUGIN_INSTALL_TIMEOUT_MS / 6e4 })
}), t("wizard.plugins.returningToSelection")].join("\n"), t("wizard.plugins.installTitle"));
params.runtime.error?.(`Plugin install timed out after ${ONBOARDING_PLUGIN_INSTALL_TIMEOUT_MS}ms: ${safeSpec}`);
return incompletePluginInstall(params.cfg, params.entry.pluginId, "timed_out");
}
function summarizeInstallError(message) {
const cleaned = sanitizeTerminalText(message).replace(/^Install failed(?:\s*\([^)]*\))?\s*:?\s*/i, "").trim();
if (!cleaned) return "Unknown install failure";
return cleaned.length > 180 ? `${truncateUtf16Safe(cleaned, 179)}…` : cleaned;
}
const ONBOARDING_PLUGIN_INSTALL_ERROR_MAX_CHARS = 12e3;
function formatInstallErrorDetail(message) {
const cleaned = message.replace(/\r\n?/g, "\n").split("\n").map((line) => sanitizeTerminalText(line)).join("\n").trim();
if (cleaned.length <= ONBOARDING_PLUGIN_INSTALL_ERROR_MAX_CHARS) return cleaned;
return `${truncateUtf16Safe(cleaned, 11969).trimEnd()}
… (installer output truncated)`;
}
async function notePluginInstallFailure(prompter, spec, error) {
await prompter.note([t("wizard.plugins.installFailed", {
spec: sanitizeTerminalText(spec),
error: summarizeInstallError(error)
}), t("wizard.plugins.returningToSelection")].join("\n"), t("wizard.plugins.installTitle"));
}
function isTimeoutError(error) {
return error instanceof Error && error.message === "timeout";
}
async function finishOnboardingPluginInstall(params, options) {
const enableResult = enableExplicitlySelectedPluginInConfig(params.cfg, options.pluginId);
if (!enableResult.enabled) {
const safeLabel = sanitizeTerminalText(params.entry.label);
const reason = enableResult.reason ?? "plugin disabled";
await params.prompter.note(t("wizard.plugins.enableFailed", {
plugin: safeLabel,
reason
}), t("wizard.plugins.installTitle"));
params.runtime.error?.(`Plugin install failed: ${sanitizeTerminalText(options.pluginId)} is disabled (${reason}).`);
return incompletePluginInstall(enableResult.config, options.pluginId, "failed");
}
const cfg = options.install ? recordPluginInstall(enableResult.config, options.install) : await options.prepareConfig?.(enableResult.config) ?? enableResult.config;
clearLoadInstalledPluginIndexInstallRecordsCache();
clearPluginMetadataLifecycleCaches();
await invalidatePluginRuntimeDiscoveryAfterConfigMutation({ logger: { warn: (message) => params.runtime.log(message) } });
return {
cfg,
installed: true,
pluginId: options.pluginId,
status: "installed"
};
}
async function installLocalOnboardingPlugin(params) {
const consent = capturePluginCapabilityConsentHandlerErrors(params.onCapabilityConsent);
try {
return await finishOnboardingPluginInstall(params, {
pluginId: params.entry.pluginId,
prepareConfig: async (cfg) => {
if (pathsReferToSameDirectory(params.localPath, params.bundledLocalPath)) return cfg;
const capabilityConsent = await prepareManagedPluginArtifactConsentHandler({
config: params.cfg,
source: "path",
spec: params.npmSpec ?? params.localPath,
onCapabilityConsent: consent.onCapabilityConsent,
beforePersistentEffect: params.beforePersistentEffect
});
await capabilityConsent.onBeforePluginArtifactCommit({
pluginId: params.entry.pluginId,
stagedArtifactDir: params.localPath,
mode: "install"
});
const sourcePath = formatPortableLocalPath(params.localPath, params.workspaceDir);
return recordPluginInstall(addPluginLoadPath(cfg, params.localPath), capabilityConsent.applyAcceptedSurface(params.entry.pluginId, {
pluginId: params.entry.pluginId,
source: "path",
installPath: params.localPath,
...sourcePath ? { sourcePath } : {},
...params.npmSpec ? { spec: params.npmSpec } : {}
}));
}
});
} catch (error) {
consent.rethrowCallbackError();
const detail = error instanceof Error ? error.message : String(error);
await notePluginInstallFailure(params.prompter, params.localPath, detail);
return incompletePluginInstall(params.cfg, params.entry.pluginId, "failed", formatInstallErrorDetail(detail));
}
}
function logInstallWarning(runtime, message, preserveLineBreaks = false) {
const sanitized = (preserveLineBreaks ? message.split("\n") : [message]).map((line) => sanitizeTerminalText(line)).join("\n").trim();
if (!sanitized) return;
runtime.log?.(`${sanitized}\n`);
}
function startPluginInstallProgress(prompter, safeLabel) {
const progress = prompter.progress(t("wizard.plugins.installingPlugin", { plugin: safeLabel }));
progress.update(t("wizard.plugins.preparingInstall"));
return {
progress,
updateProgress: (message) => {
const sanitized = sanitizeTerminalText(message).trim();
if (sanitized) progress.update(sanitized);
}
};
}
async function runInstallWatchdog(install) {
const controller = new AbortController();
const ownedInstallPromise = install(controller.signal);
try {
return await withTimeout(ownedInstallPromise, ONBOARDING_PLUGIN_INSTALL_WATCHDOG_TIMEOUT_MS);
} catch (error) {
if (isTimeoutError(error)) {
controller.abort();
await ownedInstallPromise.catch(() => void 0);
}
throw error;
}
}
async function installPluginWithProgress(params) {
const { source } = params;
const consent = capturePluginCapabilityConsentHandlerErrors(params.onCapabilityConsent);
const capabilityConsent = await prepareManagedPluginArtifactConsentHandler({
config: params.cfg,
source: source.kind === "clawhub" ? "clawhub" : "npm",
spec: source.kind === "npm-pack" ? `npm-pack:${source.archivePath}` : source.spec,
expectedIntegrity: params.entry.install.expectedIntegrity,
onCapabilityConsent: consent.onCapabilityConsent,
beforePersistentEffect: params.beforePersistentEffect
});
const safeLabel = sanitizeTerminalText(params.entry.label);
const { progress, updateProgress } = startPluginInstallProgress(params.prompter, safeLabel);
let renderedTrustWarning = false;
const renderTrustWarning = (message) => {
logInstallWarning(params.runtime, message, true);
renderedTrustWarning = true;
};
const installOptions = () => ({
config: params.cfg,
timeoutMs: ONBOARDING_PLUGIN_INSTALL_TIMEOUT_MS,
expectedPluginId: params.entry.pluginId,
expectedIntegrity: params.entry.install.expectedIntegrity,
extensionsDir: resolveDefaultPluginExtensionsDir(),
onBeforePluginArtifactCommit: capabilityConsent.onBeforePluginArtifactCommit,
logger: {
info: updateProgress,
warn: (message) => {
updateProgress(message);
if (source.kind === "clawhub") {
const plain = stripAnsi(message);
if (plain.startsWith("Warning\n")) return;
if (plain.startsWith("Blocked\n") || plain.startsWith("Review\n")) {
renderTrustWarning(message);
return;
}
}
logInstallWarning(params.runtime, message);
}
}
});
try {
let result;
if (source.kind === "clawhub") {
const { installPluginFromClawHub } = await import("./clawhub-63-8LPEM.mjs");
result = await installPluginFromClawHub({
...installOptions(),
spec: source.spec,
mode: "install"
});
const failureWarning = !result.ok && "warning" in result ? result.warning : void 0;
if (failureWarning && !renderedTrustWarning) {
progress.stop("Review ClawHub warning");
renderTrustWarning(failureWarning);
}
} else result = await runInstallWatchdog((signal) => {
const options = {
...installOptions(),
signal
};
return source.kind === "npm-pack" ? installPluginFromNpmPackArchive({
...options,
archivePath: source.archivePath
}) : installPluginFromNpmSpec({
...options,
spec: source.spec,
mode: "update",
...params.trustedSourceLinkedOfficialInstall ?? params.entry.trustedSourceLinkedOfficialInstall ? { trustedSourceLinkedOfficialInstall: true } : {}
});
});
progress.stop(t(result.ok ? "wizard.plugins.installedPlugin" : "wizard.plugins.installFailedShort", { plugin: safeLabel }));
consent.rethrowCallbackError();
return {
status: "completed",
result,
capabilityConsent
};
} catch (error) {
const timedOut = source.kind !== "clawhub" && isTimeoutError(error);
progress.stop(t(timedOut ? "wizard.plugins.installTimedOutShort" : "wizard.plugins.installFailedShort", { plugin: safeLabel }));
consent.rethrowCallbackError();
if (timedOut) return { status: "timed_out" };
if (source.kind === "npm-pack" && !(error instanceof ManagedPluginLifecycleError) || source.kind === "clawhub" && (error instanceof WizardCancelledError || error instanceof WizardNavigationError)) throw error;
return {
status: "completed",
capabilityConsent,
result: {
ok: false,
error: error instanceof Error ? error.message : String(error)
}
};
}
}
async function installPluginFromOverride(params) {
const { entry, prompter, runtime } = params;
runtime.log?.(`Using plugin install override for ${sanitizeTerminalText(entry.pluginId)} from ${PLUGIN_INSTALL_OVERRIDES_ENV} (${ALLOW_PLUGIN_INSTALL_OVERRIDES_ENV}=1).`);
const installOutcome = await installPluginWithProgress({
...params,
source: params.override,
trustedSourceLinkedOfficialInstall: false
});
const displaySpec = params.override.kind === "npm" ? params.override.spec : `npm-pack:${params.override.archivePath}`;
if (installOutcome.status === "timed_out") return await reportPluginInstallTimeout(params, displaySpec);
const { result } = installOutcome;
if (!result.ok) {
const errorDetail = formatInstallErrorDetail(result.error);
await notePluginInstallFailure(prompter, displaySpec, result.error);
runtime.error?.(`Plugin install failed: ${summarizeInstallError(result.error)}`);
return incompletePluginInstall(params.cfg, entry.pluginId, "failed", errorDetail);
}
const npmTarballName = params.override.kind === "npm-pack" ? result.npmTarballName : void 0;
const install = {
pluginId: result.pluginId,
source: "npm",
spec: params.override.kind === "npm-pack" ? result.npmResolution?.resolvedSpec ?? result.manifestName ?? result.pluginId : params.override.spec,
...params.override.kind === "npm-pack" ? { sourcePath: params.override.archivePath } : {},
installPath: result.targetDir,
...result.version ? { version: result.version } : {},
...buildNpmResolutionFields(result.npmResolution),
...params.override.kind === "npm-pack" ? {
artifactKind: "npm-pack",
artifactFormat: "tgz",
...result.npmResolution?.integrity ? { npmIntegrity: result.npmResolution.integrity } : {},
...result.npmResolution?.shasum ? { npmShasum: result.npmResolution.shasum } : {},
...npmTarballName ? { npmTarballName } : {}
} : {}
};
return await finishOnboardingPluginInstall(params, {
pluginId: result.pluginId,
install: installOutcome.capabilityConsent.applyAcceptedSurface(result.pluginId, install)
});
}
/** Ensures an onboarding plugin is installed, enabled, and recorded in config. */
async function ensureOnboardingPluginInstalled(params) {
const { entry, prompter, runtime, workspaceDir } = params;
const next = params.cfg;
const onCapabilityConsent = params.onCapabilityConsent ?? createPluginCapabilityConsentPrompter(prompter);
const installOverride = resolvePluginInstallOverride({ pluginId: entry.pluginId });
if (installOverride) {
assertConfigWriteAllowedInCurrentMode();
return await withPluginLifecycleLease({}, async () => installPluginFromOverride({
...params,
override: installOverride,
onCapabilityConsent
}));
}
const allowLocal = resolveLocalPluginRoots(workspaceDir).some(hasTrustedGitWorkspace);
const bundledLocalPath = resolveBundledLocalPath({
entry,
workspaceDir
});
const localPath = bundledLocalPath ?? resolveLocalPath({
entry,
workspaceDir,
allowLocal
});
const rawClawHubSpec = entry.install.clawhubSpec?.trim();
const clawhubSpec = rawClawHubSpec && parseClawHubPluginSpec(rawClawHubSpec) ? rawClawHubSpec : null;
const rawNpmSpec = entry.install.npmSpec?.trim();
const npmSpec = rawNpmSpec && parseRegistryNpmSpec(rawNpmSpec) ? rawNpmSpec : null;
const updateChannel = resolveRegistryUpdateChannel({
configChannel: normalizeUpdateChannel(next.update?.channel),
currentVersion: VERSION
});
const clawhubSpecs = clawhubSpec ? resolveClawHubInstallSpecsForUpdateChannel({
spec: clawhubSpec,
updateChannel,
officialPackageName: entry.trustedSourceLinkedOfficialInstall ? parseClawHubPluginSpec(clawhubSpec)?.name : void 0,
coreVersion: VERSION,
versionBoundToCore: entry.versionBoundToOpenClaw
}) : null;
let npmSpecs;
const clawhubInstallSpec = clawhubSpecs?.installSpec ?? clawhubSpec;
const defaultChoice = resolveInstallDefaultChoice({
cfg: next,
entry,
localPath,
bundledLocalPath,
hasClawHubSpec: Boolean(clawhubSpec),
hasNpmSpec: Boolean(npmSpec)
});
const choice = params.promptInstall === false ? defaultChoice : await promptInstallChoice({
label: entry.label,
localPath,
defaultChoice,
prompter,
autoConfirmSingleSource: params.autoConfirmSingleSource,
clawhubSpec: bundledLocalPath ? null : clawhubInstallSpec,
npmSpec: bundledLocalPath ? null : npmSpec
});
if (choice === "skip") return incompletePluginInstall(next, entry.pluginId, "skipped");
assertConfigWriteAllowedInCurrentMode();
return await withPluginLifecycleLease({}, async () => {
const installLocal = (selectedPath) => installLocalOnboardingPlugin({
...params,
localPath: selectedPath,
bundledLocalPath,
npmSpec,
onCapabilityConsent
});
if (choice === "local" && localPath) return await installLocal(localPath);
const sources = resolvePluginInstallSources(entry.install, params.promptInstall === false ? void 0 : choice === "npm" || choice === "clawhub" ? choice : void 0);
if (sources.length === 0) return incompletePluginInstall(next, entry.pluginId, "failed", "No declared remote install source.");
if (npmSpec && sources.some((source) => source.source === "npm")) try {
npmSpecs = await resolveNpmInstallSpecsForUpdateChannel({
spec: npmSpec,
updateChannel,
officialPackageName: entry.trustedSourceLinkedOfficialInstall ? parseRegistryNpmSpec(npmSpec)?.name : void 0,
coreVersion: VERSION,
versionBoundToCore: entry.versionBoundToOpenClaw
});
} catch (error) {
if (!(error instanceof NpmChannelResolutionError)) throw error;
await notePluginInstallFailure(prompter, npmSpec, error.message);
return incompletePluginInstall(next, entry.pluginId, "failed", formatInstallErrorDetail(error.message));
}
const onFallback = async (message) => {
await prompter.note(message, t("wizard.plugins.installTitle"));
};
const { attempt: installOutcome, source: installedSource } = await installWithSourceFallback({
sources,
install: async (source) => {
const specs = source.source === "npm" ? npmSpecs : clawhubSpecs;
const attemptEntry = {
...entry,
install: {
...entry.install,
expectedIntegrity: source.expectedIntegrity
}
};
return await installWithChannelFallback({
installSpec: specs?.installSpec ?? source.spec,
...source.expectedIntegrity ? {} : { fallbackSpec: specs?.fallbackSpec },
install: (spec) => installPluginWithProgress({
...params,
entry: attemptEntry,
source: {
kind: source.source,
spec
},
onCapabilityConsent
}),
isRetryable: (attempt) => attempt.status === "completed" && !attempt.result.ok && (source.source === "npm" ? isUnavailableNpmTarget(attempt.result) : isUnavailableClawHubTarget(attempt.result)),
onFallback
});
},
result: (attempt) => attempt.status === "completed" ? attempt.result : { ok: false },
onFallback
});
if (installOutcome.status === "timed_out") return await reportPluginInstallTimeout(params, installedSource.spec);
const { result, capabilityConsent } = installOutcome;
if (result.ok) {
const spec = (installedSource.source === "npm" ? npmSpecs : clawhubSpecs)?.recordSpec ?? installedSource.spec;
const install = "clawhub" in result ? {
...buildClawHubPluginInstallRecordFields(result.clawhub),
spec,
installPath: result.targetDir
} : {
source: "npm",
spec,
installPath: result.targetDir,
version: result.version,
...buildNpmResolutionFields(result.npmResolution)
};
return await finishOnboardingPluginInstall({
cfg: next,
entry,
prompter,
runtime
}, {
pluginId: result.pluginId,
install: capabilityConsent.applyAcceptedSurface(result.pluginId, {
pluginId: result.pluginId,
...install
})
});
}
await notePluginInstallFailure(prompter, installedSource.spec, result.error);
if (localPath && isUnavailablePluginSource(installedSource.source, result)) {
if (await prompter.confirm({
message: t("wizard.plugins.useLocalPluginPathInstead", { path: sanitizeTerminalText(localPath) }),
initialValue: true
})) return await installLocal(localPath);
}
runtime.error?.(`Plugin install failed: ${summarizeInstallError(result.error)}`);
return incompletePluginInstall(next, entry.pluginId, "failed", formatInstallErrorDetail(result.error));
});
}
//#endregion
export { ensureOnboardingPluginInstalled as t };