openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
837 lines (836 loc) • 34.1 kB
JavaScript
import { r as stripAnsi } from "./ansi-DrXAcdMD.js";
import "./src-vebZIeLe.js";
import { t as expectDefined } from "./expect-CyE8FADM.js";
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { y as uniqueStrings } from "./string-normalization-DsCfAx8q.js";
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js";
import { c as resolveUserPath } from "./home-dir-BPhrG-aM.js";
import { k as withTimeout } from "./fs-safe-B6pvPGnf.js";
import { r as isPathInside } from "./path-guards-Cp-mGr3-.js";
import "./utils-P__uGsPB.js";
import { n as sanitizeTerminalText } from "./safe-text-BGBqp1a4.js";
import { t as clearPluginMetadataLifecycleCaches } from "./plugin-metadata-lifecycle-C6m-q5Au.js";
import { t as VERSION } from "./version-v1kuAkGj.js";
import { o as parseRegistryNpmSpec } from "./npm-registry-spec-CM_p1_uq.js";
import { a as resolveDefaultPluginExtensionsDir } from "./install-paths-ChnxikBv.js";
import { t as parseClawHubPluginSpec } from "./clawhub-spec-Er3Np6VI.js";
import { A as installWithSourceFallback, F as resolvePluginInstallSources, L as isUnavailableClawHubTarget, M as resolveClawHubInstallSpecsForUpdateChannel, P as resolveNpmInstallSpecsForUpdateChannel, j as isUnavailablePluginSource, k as installWithChannelFallback } from "./official-external-plugin-catalog-Dzu7dwBN.js";
import { s as clearLoadInstalledPluginIndexInstallRecordsCache } from "./installed-plugin-index-record-reader-SXWwf_BU.js";
import { c as normalizeUpdateChannel, d as resolveRegistryUpdateChannel } from "./update-channels-BcfztK6k.js";
import { n as isUnavailableNpmTarget } from "./install-types-DY_kphq4.js";
import { t as ManagedPluginLifecycleError } from "./management-lifecycle-error-BlJhejU6.js";
import { t as enableExplicitlySelectedPluginInConfig } from "./enable-3OwjFbBV.js";
import { n as assertConfigWriteAllowedInCurrentMode } from "./nix-mode-write-guard-uwcnAyQN.js";
import { f as recordPluginInstall, l as buildNpmResolutionInstallFields } from "./installed-plugin-index-records-C06Xozeq.js";
import "./with-timeout-C7lEewUq.js";
import { n as t } from "./i18n-hynzGFbD.js";
import { a as resolveBundledPluginSources, n as findBundledPluginSourceInMap } from "./bundled-sources-CU4qjQGy.js";
import { t as withPluginLifecycleLease } from "./plugin-lifecycle-lease-BtAFH872.js";
import { t as createPluginCapabilityConsentPrompter } from "./plugin-capability-consent-CaBGom29.js";
import { n as WizardCancelledError, r as WizardNavigationError } from "./prompts-DLsO8MlU.js";
import { r as prepareManagedPluginArtifactConsentHandler, t as capturePluginCapabilityConsentHandlerErrors } from "./capability-consent-DAQVy0Fy.js";
import { t as invalidatePluginRuntimeDiscoveryAfterConfigMutation } from "./registry-refresh-CvO7IUdQ.js";
import { i as installPluginFromNpmPackArchive, r as installPluginFromNpmSpec } from "./install-BezM8CtS.js";
import { t as resolveBundledInstallPlanForCatalogEntry } from "./plugin-install-plan-5wcyV5nl.js";
import { t as buildClawHubPluginInstallRecordFields } from "./clawhub-install-records-Dy2deHKG.js";
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: path.resolve(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
/**
* Onboarding plugin installation flow.
*
* It selects local, ClawHub, npm, or override install sources; records durable
* install metadata; and enables plugins requested by setup workflows.
*/
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 }
};
}
async function markOnboardingPluginInstalled(params) {
clearLoadInstalledPluginIndexInstallRecordsCache();
clearPluginMetadataLifecycleCaches();
await invalidatePluginRuntimeDiscoveryAfterConfigMutation({ logger: { warn: (message) => params.runtime.log(message) } });
return {
cfg: params.cfg,
installed: true,
pluginId: params.pluginId,
status: "installed"
};
}
function readInstallFailureWarning(result) {
if (result.ok || !("warning" in result) || typeof result.warning !== "string") return;
return result.warning;
}
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 match = /^gitdir:\s*(.+)$/i.exec(content);
if (!match) return null;
const gitDir = match[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 hasGitWorkspace(workspaceDir) {
const roots = [process.cwd()];
if (workspaceDir && workspaceDir !== process.cwd()) roots.push(workspaceDir);
return roots.some((root) => hasTrustedGitWorkspace(root));
}
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 = [process.cwd()];
if (params.workspaceDir && params.workspaceDir !== process.cwd()) bases.push(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 resolveNpmSpecForOnboarding(install) {
const npmSpec = install.npmSpec?.trim();
if (!npmSpec) return null;
return parseRegistryNpmSpec(npmSpec) ? npmSpec : null;
}
function resolveClawHubSpecForOnboarding(install) {
const clawhubSpec = install.clawhubSpec?.trim();
if (!clawhubSpec) return null;
return parseClawHubPluginSpec(clawhubSpec) ? clawhubSpec : null;
}
function resolveInstallDefaultChoice(params) {
const { cfg, entry, localPath, bundledLocalPath, hasClawHubSpec, hasNpmSpec } = params;
const hasRemoteSpec = hasClawHubSpec || hasNpmSpec;
const entryDefault = entry.install.defaultChoice;
const remoteDefault = () => resolvePluginInstallSources(entry.install)[0]?.source ?? "skip";
if (!hasRemoteSpec) return localPath ? "local" : "skip";
if (!localPath) return remoteDefault();
if (bundledLocalPath) return "local";
const updateChannel = cfg.update?.channel;
if (updateChannel === "dev") return "local";
if (updateChannel === "stable" || updateChannel === "extended-stable" || updateChannel === "beta") return remoteDefault();
if (entryDefault === "local") return "local";
return remoteDefault();
}
async function promptInstallChoice(params) {
const rawClawHubSpec = resolveClawHubSpecForOnboarding(params.entry.install);
const rawNpmSpec = resolveNpmSpecForOnboarding(params.entry.install);
const clawhubSpec = params.bundledLocalPath ? null : params.effectiveClawHubSpec ?? rawClawHubSpec;
const npmSpec = params.bundledLocalPath ? null : params.effectiveNpmSpec ?? rawNpmSpec;
const safeLabel = sanitizeTerminalText(params.entry.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) {
const realSources = [];
if (safeClawHubSpec) realSources.push("clawhub");
if (safeNpmSpec) realSources.push("npm");
if (params.localPath) realSources.push("local");
if (realSources.length === 1) return expectDefined(realSources[0], "real sources entry at 0");
}
options.push({
value: "skip",
label: t("common.skipForNow")
});
const initialValue = params.defaultChoice === "local" && !params.localPath ? clawhubSpec ? "clawhub" : npmSpec ? "npm" : "skip" : params.defaultChoice === "clawhub" && !clawhubSpec ? npmSpec ? "npm" : params.localPath ? "local" : "skip" : params.defaultChoice === "npm" && !npmSpec ? clawhubSpec ? "clawhub" : params.localPath ? "local" : "skip" : params.defaultChoice;
return await params.prompter.select({
message: t("wizard.plugins.installPluginPrompt", { plugin: safeLabel }),
options,
initialValue
});
}
function formatDurationLabel(timeoutMs) {
if (timeoutMs % 6e4 === 0) {
const minutes = timeoutMs / 6e4;
return t(minutes === 1 ? "common.minute" : "common.minutes", { count: minutes });
}
const seconds = Math.round(timeoutMs / 1e3);
return t(seconds === 1 ? "common.second" : "common.seconds", { count: seconds });
}
function formatPluginInstallProgress(label) {
return t("wizard.plugins.installingPlugin", { plugin: label });
}
function formatPluginInstalled(label) {
return t("wizard.plugins.installedPlugin", { plugin: label });
}
function formatPluginInstallFailed(label) {
return t("wizard.plugins.installFailedShort", { plugin: label });
}
function formatPluginInstallTimedOut(label) {
return t("wizard.plugins.installTimedOutShort", { plugin: label });
}
function formatPluginInstallTimedOutNote(spec) {
return [t("wizard.plugins.installTimedOut", {
spec,
duration: formatDurationLabel(ONBOARDING_PLUGIN_INSTALL_TIMEOUT_MS)
}), t("wizard.plugins.returningToSelection")].join("\n");
}
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 applyPluginEnablement(params) {
const enableResult = enableExplicitlySelectedPluginInConfig(params.cfg, params.pluginId);
if (enableResult.enabled) return enableResult;
const safeLabel = sanitizeTerminalText(params.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(params.pluginId)} is disabled (${reason}).`);
return enableResult;
}
async function finishOnboardingPluginInstall(params) {
const enableResult = await applyPluginEnablement(params);
if (!enableResult.enabled) return incompletePluginInstall(enableResult.config, params.pluginId, "failed");
return await markOnboardingPluginInstalled({
cfg: params.install ? recordPluginInstall(enableResult.config, params.install) : await params.prepareConfig?.(enableResult.config) ?? enableResult.config,
pluginId: params.pluginId,
runtime: params.runtime
});
}
async function installLocalOnboardingPlugin(params) {
const consent = capturePluginCapabilityConsentHandlerErrors(params.onCapabilityConsent);
try {
return await finishOnboardingPluginInstall({
cfg: params.cfg,
pluginId: params.entry.pluginId,
label: params.entry.label,
prompter: params.prompter,
runtime: params.runtime,
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 logInstallWarningWithSpacing(runtime, message) {
const sanitized = sanitizeTerminalText(message).trim();
if (!sanitized) return;
runtime.log?.(`${sanitized}\n`);
}
function logInstallWarningWithLineBreaks(runtime, message) {
const sanitized = message.split("\n").map((line) => sanitizeTerminalText(line)).join("\n").trim();
if (!sanitized) return;
runtime.log?.(`${sanitized}\n`);
}
function isReviewRequiredClawHubTrustWarning(message) {
return stripAnsi(message).startsWith("Warning\n");
}
function isClawHubTrustWarning(message) {
const plain = stripAnsi(message);
return isReviewRequiredClawHubTrustWarning(message) || plain.startsWith("Blocked\n") || plain.startsWith("Review\n");
}
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 runOnboardingPluginInstallWithProgress(params) {
const consent = capturePluginCapabilityConsentHandlerErrors(params.onCapabilityConsent);
const capabilityConsent = await prepareManagedPluginArtifactConsentHandler({
config: params.cfg,
source: "npm",
spec: params.spec,
expectedIntegrity: params.entry.install.expectedIntegrity,
onCapabilityConsent: consent.onCapabilityConsent,
beforePersistentEffect: params.beforePersistentEffect
});
const safeLabel = sanitizeTerminalText(params.entry.label);
const progress = params.prompter.progress(formatPluginInstallProgress(safeLabel));
progress.update(t("wizard.plugins.preparingInstall"));
const updateProgress = (message) => {
const sanitized = sanitizeTerminalText(message).trim();
if (!sanitized) return;
progress.update(sanitized);
};
try {
const result = await runInstallWatchdog((signal) => params.install({
info: updateProgress,
warn: (message) => {
updateProgress(message);
logInstallWarningWithSpacing(params.runtime, message);
}
}, signal, capabilityConsent.onBeforePluginArtifactCommit));
progress.stop(result.ok ? formatPluginInstalled(safeLabel) : formatPluginInstallFailed(safeLabel));
consent.rethrowCallbackError();
return {
status: "completed",
result,
capabilityConsent
};
} catch (error) {
progress.stop(isTimeoutError(error) ? formatPluginInstallTimedOut(safeLabel) : formatPluginInstallFailed(safeLabel));
consent.rethrowCallbackError();
if (isTimeoutError(error)) return { status: "timed_out" };
if (params.rethrowUnexpectedErrors && !(error instanceof ManagedPluginLifecycleError)) throw error;
return {
status: "completed",
capabilityConsent,
result: {
ok: false,
error: error instanceof Error ? error.message : String(error)
}
};
}
}
async function installPluginFromNpmSpecWithProgress(params) {
return await runOnboardingPluginInstallWithProgress({
...params,
spec: params.npmSpec,
install: (logger, signal, onBeforePluginArtifactCommit) => installPluginFromNpmSpec({
spec: params.npmSpec,
mode: "update",
config: params.cfg,
timeoutMs: ONBOARDING_PLUGIN_INSTALL_TIMEOUT_MS,
expectedPluginId: params.entry.pluginId,
expectedIntegrity: params.entry.install.expectedIntegrity,
...params.trustedSourceLinkedOfficialInstall ?? params.entry.trustedSourceLinkedOfficialInstall ? { trustedSourceLinkedOfficialInstall: true } : {},
extensionsDir: resolveDefaultPluginExtensionsDir(),
logger,
signal,
onBeforePluginArtifactCommit
})
});
}
async function installPluginFromNpmPackArchiveWithProgress(params) {
return await runOnboardingPluginInstallWithProgress({
...params,
spec: `npm-pack:${params.archivePath}`,
install: (logger, signal, onBeforePluginArtifactCommit) => installPluginFromNpmPackArchive({
archivePath: params.archivePath,
timeoutMs: ONBOARDING_PLUGIN_INSTALL_TIMEOUT_MS,
config: params.cfg,
expectedPluginId: params.entry.pluginId,
expectedIntegrity: params.entry.install.expectedIntegrity,
extensionsDir: resolveDefaultPluginExtensionsDir(),
logger,
signal,
onBeforePluginArtifactCommit
}),
rethrowUnexpectedErrors: true
});
}
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 = params.override.kind === "npm" ? await installPluginFromNpmSpecWithProgress({
cfg: params.cfg,
entry,
npmSpec: params.override.spec,
prompter,
runtime,
onCapabilityConsent: params.onCapabilityConsent,
beforePersistentEffect: params.beforePersistentEffect,
trustedSourceLinkedOfficialInstall: false
}) : await installPluginFromNpmPackArchiveWithProgress({
cfg: params.cfg,
entry,
archivePath: params.override.archivePath,
prompter,
runtime,
onCapabilityConsent: params.onCapabilityConsent,
beforePersistentEffect: params.beforePersistentEffect
});
const displaySpec = params.override.kind === "npm" ? params.override.spec : `npm-pack:${params.override.archivePath}`;
if (installOutcome.status === "timed_out") {
await prompter.note(formatPluginInstallTimedOutNote(sanitizeTerminalText(displaySpec)), t("wizard.plugins.installTitle"));
runtime.error?.(`Plugin install timed out after ${ONBOARDING_PLUGIN_INSTALL_TIMEOUT_MS}ms: ${sanitizeTerminalText(displaySpec)}`);
return incompletePluginInstall(params.cfg, entry.pluginId, "timed_out");
}
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 = params.override.kind === "npm-pack" ? {
pluginId: result.pluginId,
source: "npm",
spec: result.npmResolution?.resolvedSpec ?? result.manifestName ?? result.pluginId,
sourcePath: params.override.archivePath,
installPath: result.targetDir,
...result.version ? { version: result.version } : {},
...buildNpmResolutionInstallFields(result.npmResolution),
artifactKind: "npm-pack",
artifactFormat: "tgz",
...result.npmResolution?.integrity ? { npmIntegrity: result.npmResolution.integrity } : {},
...result.npmResolution?.shasum ? { npmShasum: result.npmResolution.shasum } : {},
...npmTarballName ? { npmTarballName } : {}
} : {
pluginId: result.pluginId,
source: "npm",
spec: params.override.spec,
installPath: result.targetDir,
...result.version ? { version: result.version } : {},
...buildNpmResolutionInstallFields(result.npmResolution)
};
return await finishOnboardingPluginInstall({
cfg: params.cfg,
pluginId: result.pluginId,
label: entry.label,
prompter,
runtime,
install: installOutcome.capabilityConsent.applyAcceptedSurface(result.pluginId, install)
});
}
async function installPluginFromClawHubSpecWithProgress(params) {
const consent = capturePluginCapabilityConsentHandlerErrors(params.onCapabilityConsent);
const capabilityConsent = await prepareManagedPluginArtifactConsentHandler({
config: params.cfg,
source: "clawhub",
spec: params.clawhubSpec,
expectedIntegrity: params.entry.install.expectedIntegrity,
onCapabilityConsent: consent.onCapabilityConsent,
beforePersistentEffect: params.beforePersistentEffect
});
const safeLabel = sanitizeTerminalText(params.entry.label);
const progress = params.prompter.progress(formatPluginInstallProgress(safeLabel));
progress.update(t("wizard.plugins.preparingInstall"));
const updateProgress = (message) => {
const sanitized = sanitizeTerminalText(message).trim();
if (!sanitized) return;
progress.update(sanitized);
};
let renderedTrustWarning = false;
const renderTrustWarning = (message) => {
logInstallWarningWithLineBreaks(params.runtime, message);
renderedTrustWarning = true;
};
try {
const { installPluginFromClawHub } = await import("./clawhub-B48ycaJg.js");
const result = await installPluginFromClawHub({
spec: params.clawhubSpec,
expectedIntegrity: params.entry.install.expectedIntegrity,
timeoutMs: ONBOARDING_PLUGIN_INSTALL_TIMEOUT_MS,
config: params.cfg,
extensionsDir: resolveDefaultPluginExtensionsDir(),
expectedPluginId: params.entry.pluginId,
mode: "install",
onBeforePluginArtifactCommit: capabilityConsent.onBeforePluginArtifactCommit,
logger: {
info: updateProgress,
warn: (message) => {
updateProgress(message);
if (isReviewRequiredClawHubTrustWarning(message)) return;
if (isClawHubTrustWarning(message)) {
renderTrustWarning(message);
return;
}
logInstallWarningWithSpacing(params.runtime, message);
}
}
});
const failureWarning = readInstallFailureWarning(result);
if (failureWarning && !renderedTrustWarning) {
progress.stop("Review ClawHub warning");
renderTrustWarning(failureWarning);
}
if (result.ok) progress.stop(formatPluginInstalled(safeLabel));
else progress.stop(formatPluginInstallFailed(safeLabel));
consent.rethrowCallbackError();
return {
result,
capabilityConsent
};
} catch (error) {
progress.stop(formatPluginInstallFailed(safeLabel));
consent.rethrowCallbackError();
if (error instanceof WizardCancelledError || error instanceof WizardNavigationError) throw error;
return {
result: {
ok: false,
error: error instanceof Error ? error.message : String(error)
},
capabilityConsent
};
}
}
/** 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({
cfg: next,
entry,
override: installOverride,
prompter,
runtime,
onCapabilityConsent,
beforePersistentEffect: params.beforePersistentEffect
}));
}
const allowLocal = hasGitWorkspace(workspaceDir);
const bundledLocalPath = entry.preferRemoteInstall ? null : resolveBundledLocalPath({
entry,
workspaceDir
});
const localPath = bundledLocalPath ?? (entry.preferRemoteInstall ? null : resolveLocalPath({
entry,
workspaceDir,
allowLocal
}));
const clawhubSpec = resolveClawHubSpecForOnboarding(entry.install);
const npmSpec = resolveNpmSpecForOnboarding(entry.install);
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;
const npmSpecs = npmSpec ? resolveNpmInstallSpecsForUpdateChannel({
spec: npmSpec,
updateChannel,
officialPackageName: entry.trustedSourceLinkedOfficialInstall ? parseRegistryNpmSpec(npmSpec)?.name : void 0,
coreVersion: VERSION,
versionBoundToCore: entry.versionBoundToOpenClaw
}) : null;
const clawhubInstallSpec = clawhubSpecs?.installSpec ?? clawhubSpec;
const npmInstallSpec = npmSpecs?.installSpec ?? npmSpec;
const defaultChoice = resolveInstallDefaultChoice({
cfg: next,
entry,
localPath,
bundledLocalPath,
hasClawHubSpec: Boolean(clawhubSpec),
hasNpmSpec: Boolean(npmSpec)
});
const choice = params.promptInstall === false ? defaultChoice : await promptInstallChoice({
entry,
localPath,
bundledLocalPath,
defaultChoice,
prompter,
autoConfirmSingleSource: params.autoConfirmSingleSource,
effectiveClawHubSpec: clawhubInstallSpec,
effectiveNpmSpec: npmInstallSpec
});
if (choice === "skip") return incompletePluginInstall(next, entry.pluginId, "skipped");
assertConfigWriteAllowedInCurrentMode();
return await withPluginLifecycleLease({}, async () => {
if (choice === "local" && localPath) return await installLocalOnboardingPlugin({
cfg: next,
entry,
localPath,
bundledLocalPath,
npmSpec,
workspaceDir,
prompter,
runtime,
onCapabilityConsent,
beforePersistentEffect: params.beforePersistentEffect
});
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.");
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: async (spec) => source.source === "clawhub" ? {
status: "completed",
...await installPluginFromClawHubSpecWithProgress({
cfg: next,
entry: attemptEntry,
clawhubSpec: spec,
prompter,
runtime,
onCapabilityConsent,
beforePersistentEffect: params.beforePersistentEffect
})
} : await installPluginFromNpmSpecWithProgress({
cfg: next,
entry: attemptEntry,
npmSpec: spec,
prompter,
runtime,
onCapabilityConsent,
beforePersistentEffect: params.beforePersistentEffect
}),
isRetryable: (attempt) => attempt.status === "completed" && !attempt.result.ok && (source.source === "npm" ? isUnavailableNpmTarget(attempt.result) : isUnavailableClawHubTarget(attempt.result)),
onFallback: async (message) => {
await prompter.note(message, t("wizard.plugins.installTitle"));
}
});
},
result: (attempt) => attempt.status === "completed" ? attempt.result : { ok: false },
onFallback: async (message) => {
await prompter.note(message, t("wizard.plugins.installTitle"));
}
});
if (installOutcome.status === "timed_out") {
await prompter.note(formatPluginInstallTimedOutNote(sanitizeTerminalText(installedSource.spec)), t("wizard.plugins.installTitle"));
runtime.error?.(`Plugin install timed out after ${ONBOARDING_PLUGIN_INSTALL_TIMEOUT_MS}ms: ${sanitizeTerminalText(installedSource.spec)}`);
return incompletePluginInstall(next, entry.pluginId, "timed_out");
}
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,
...buildNpmResolutionInstallFields(result.npmResolution)
};
return await finishOnboardingPluginInstall({
cfg: next,
pluginId: result.pluginId,
label: entry.label,
prompter,
runtime,
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 installLocalOnboardingPlugin({
cfg: next,
entry,
localPath,
bundledLocalPath,
npmSpec,
workspaceDir,
prompter,
runtime,
onCapabilityConsent,
beforePersistentEffect: params.beforePersistentEffect
});
}
runtime.error?.(`Plugin install failed: ${summarizeInstallError(result.error)}`);
return incompletePluginInstall(next, entry.pluginId, "failed", formatInstallErrorDetail(result.error));
});
}
//#endregion
export { ensureOnboardingPluginInstalled as t };