UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

925 lines (924 loc) 33.4 kB
import { _ as uniqueStrings } from "./string-normalization-WNUDCpXX.js"; import { c as withTimeout } from "./fs-safe-aqmM_n6V.js"; import { c as isRecord, p as resolveUserPath } from "./utils-CCC-BEJH.js"; import { n as VERSION } from "./version-Crcn9X9T.js"; import { t as parseClawHubPluginSpec } from "./clawhub-spec-BilWxjDF.js"; import { i as isOpenClawOrgNpmSpec, s as parseRegistryNpmSpec } from "./npm-registry-spec-DjR-Lha9.js"; import { r as resolveDefaultPluginExtensionsDir } from "./install-paths-D-sCLR1I.js"; import { t as sanitizeTerminalText } from "./safe-text-BkQYdJi1.js"; import { n as recordPluginInstall, r as resolveNpmInstallRecordSpec, t as buildNpmResolutionInstallFields } from "./installs-6qpt1tFm.js"; import { n as assertConfigWriteAllowedInCurrentMode } from "./nix-mode-write-guard-DMwPAosm.js"; import { t as enableExplicitlySelectedPluginInConfig } from "./enable-DXODwa0_.js"; import "./with-timeout-D9Ai2tMH.js"; import { n as t } from "./i18n-7g_f4oaD.js"; import { i as resolveBundledPluginSources, n as findBundledPluginSourceInMap } from "./bundled-sources-C3sNzxQs.js"; import { a as installPluginFromNpmSpec, i as installPluginFromNpmPackArchive } from "./install-DzhyqmuT.js"; import { n as resolveBundledInstallPlanForCatalogEntry } from "./plugin-install-plan-BuLcDN4V.js"; import { t as CLAWHUB_INSTALL_ERROR_CODE } from "./clawhub-error-codes-aMj7E8hC.js"; import { l as resolveRegistryUpdateChannel, s as normalizeUpdateChannel } from "./update-channels-Cv-pxatM.js"; import { t as buildClawHubPluginInstallRecordFields } from "./clawhub-install-records-DgujSxf8.js"; import { n as resolveNpmInstallSpecsForUpdateChannel, t as resolveClawHubInstallSpecsForUpdateChannel } from "./install-channel-specs-r7CEfNGT.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 = 300 * 1e3; const ONBOARDING_PLUGIN_INSTALL_WATCHDOG_TIMEOUT_MS = 305e3; function shouldFallbackClawHubToNpm(params) { if (!isOpenClawOrgNpmSpec(params.npmSpec)) return false; return params.result.code === CLAWHUB_INSTALL_ERROR_CODE.PACKAGE_NOT_FOUND || params.result.code === CLAWHUB_INSTALL_ERROR_CODE.VERSION_NOT_FOUND || params.result.code === CLAWHUB_INSTALL_ERROR_CODE.ARTIFACT_DOWNLOAD_UNAVAILABLE || params.result.code === CLAWHUB_INSTALL_ERROR_CODE.ARTIFACT_UNAVAILABLE; } 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 isWithinBaseDirectory(baseDir, targetPath) { const relative = path.relative(baseDir, targetPath); return relative === "" || !path.isAbsolute(relative) && !relative.startsWith(`..${path.sep}`) && relative !== ".."; } 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 merged = uniqueStrings([...cfg.plugins?.load?.paths ?? [], 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; const relative = path.relative(realBase, localPath); if (relative === "" || !path.isAbsolute(relative) && !relative.startsWith(`..${path.sep}`) && relative !== "..") { const portable = relative.split(path.sep).join("/"); return portable ? `./${portable}` : "."; } } } async function recordLocalPluginInstall(params) { const sourcePath = formatPortableLocalPath(params.localPath, params.workspaceDir); const install = { pluginId: params.entry.pluginId, source: "path", ...sourcePath ? { sourcePath } : {}, ...params.npmSpec ? { spec: params.npmSpec } : {} }; return recordPluginInstall(params.cfg, install); } 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 ? isWithinBaseDirectory(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 = () => { if (entryDefault === "clawhub" && hasClawHubSpec) return "clawhub"; if (entryDefault === "npm" && hasNpmSpec) return "npm"; return hasNpmSpec ? "npm" : "clawhub"; }; 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 === "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 (safeClawHubSpec) options.push({ value: "clawhub", label: t("wizard.plugins.downloadFromClawHub", { spec: safeClawHubSpec }) }); if (safeNpmSpec) options.push({ value: "npm", label: t("wizard.plugins.downloadFromNpm", { spec: safeNpmSpec }) }); 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 realSources[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 ? `${cleaned.slice(0, 179)}…` : cleaned; } 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; } const PROGRESS_BAR_WIDTH = 16; const PROGRESS_BAR_TICK_MS = 200; const PROGRESS_BAR_DURATION_MS = 1e4; const PROGRESS_BAR_MAX_PERCENT = 99; /** * Maps a verbose install log line (e.g. `Downloading @scope/pkg@1.2.3 from * ClawHub…`, `Extracting /tmp/…/wecom-…-2026.4.23.tgz…`, `Installing to * /home/.../plugins/demo…`) to a short verb suitable for a progress label. * * Falls back to the raw message when no known verb prefix is recognised so * that unexpected log lines still surface to the user instead of being * swallowed. */ function shortenInstallLabel(message) { const trimmed = message.trim(); for (const [pattern, label] of [ [/^Downloading\b/i, "Downloading"], [/^Extracting\b/i, "Extracting"], [/^Installing\s+to\b/i, "Installing"], [/^Installing\b/i, "Installing"], [/^Resolving\b/i, "Resolving"], [/^Cloning\b/i, "Cloning"], [/^Verifying\b/i, "Verifying"], [/^Preparing\b/i, "Preparing"], [/^Linking\b/i, "Linking"], [/^Linked\b/i, "Linking"], [/^npm rejected managed npm alias overrides\b/i, "Retrying"], [/^Compatibility\b/i, "Resolving"], [/^ClawHub\b/i, "Resolving"] ]) if (pattern.test(trimmed)) return label; return trimmed; } /** * Wraps a {@link WizardProgress} so the spinner message keeps a steadily * growing ASCII bar attached to whatever the current install step label is. * * The plugin install pipeline only emits coarse `info` log lines, so without * animation the spinner can sit on the same string for many seconds with no * visible feedback. We render a deterministic left-to-right filling bar that * advances linearly over {@link PROGRESS_BAR_DURATION_MS} (default 10s) up to * {@link PROGRESS_BAR_MAX_PERCENT} (99%). If the install takes longer than the * preset duration the bar simply stays pinned at 99% — never wrapping back to * 0% — so the user always sees forward motion and a ceiling that signals * "almost there, just waiting on the last bit". * * The bare label is forwarded to `progress.update` first on every label * change so callers/tests that assert on the unadorned message continue to * observe it before any decorated frame is overlaid. */ function createAnimatedInstallProgress(progress, options = {}) { const totalMs = options.totalMs ?? PROGRESS_BAR_DURATION_MS; let currentLabel = ""; const startedAt = Date.now(); const computePercent = () => { const elapsed = Date.now() - startedAt; const raw = Math.floor(elapsed / totalMs * 100); return Math.max(0, Math.min(PROGRESS_BAR_MAX_PERCENT, raw)); }; const renderBar = () => { const percent = computePercent(); const filled = Math.round(percent / 100 * PROGRESS_BAR_WIDTH); return `[${"█".repeat(filled) + "░".repeat(Math.max(0, PROGRESS_BAR_WIDTH - filled))}] ${percent}%`; }; const decorate = (label) => { if (!label) return renderBar(); return `${label} ${renderBar()}`; }; const timer = setInterval(() => { if (currentLabel) progress.update(decorate(currentLabel)); }, PROGRESS_BAR_TICK_MS); if (typeof timer.unref === "function") timer.unref(); return { setLabel: (label) => { currentLabel = label; progress.update(label); }, stop: () => { clearInterval(timer); } }; } function logInstallWarningWithSpacing(runtime, message) { const sanitized = sanitizeTerminalText(message).trim(); if (!sanitized) return; runtime.log?.(`${sanitized}\n`); } async function installPluginFromNpmSpecWithProgress(params) { const safeLabel = sanitizeTerminalText(params.entry.label); const progress = params.prompter.progress(formatPluginInstallProgress(safeLabel)); const animated = createAnimatedInstallProgress(progress); animated.setLabel(t("wizard.plugins.preparingInstall")); const updateProgress = (message) => { const sanitized = sanitizeTerminalText(message).trim(); if (!sanitized) return; animated.setLabel(shortenInstallLabel(sanitized)); }; try { const result = await withTimeout(installPluginFromNpmSpec({ spec: params.npmSpec, mode: "update", 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: { info: updateProgress, warn: (message) => { updateProgress(message); logInstallWarningWithSpacing(params.runtime, message); } } }), ONBOARDING_PLUGIN_INSTALL_WATCHDOG_TIMEOUT_MS); animated.stop(); if (result.ok) progress.stop(formatPluginInstalled(safeLabel)); else progress.stop(formatPluginInstallFailed(safeLabel)); return { status: "completed", result }; } catch (error) { animated.stop(); if (isTimeoutError(error)) { progress.stop(formatPluginInstallTimedOut(safeLabel)); return { status: "timed_out" }; } progress.stop(formatPluginInstallFailed(safeLabel)); return { status: "completed", result: { ok: false, error: error instanceof Error ? error.message : String(error) } }; } } async function installPluginFromNpmPackArchiveWithProgress(params) { const safeLabel = sanitizeTerminalText(params.entry.label); const progress = params.prompter.progress(formatPluginInstallProgress(safeLabel)); const animated = createAnimatedInstallProgress(progress); animated.setLabel(t("wizard.plugins.preparingInstall")); const updateProgress = (message) => { const sanitized = sanitizeTerminalText(message).trim(); if (!sanitized) return; animated.setLabel(shortenInstallLabel(sanitized)); }; try { const result = await withTimeout(installPluginFromNpmPackArchive({ archivePath: params.archivePath, timeoutMs: ONBOARDING_PLUGIN_INSTALL_TIMEOUT_MS, expectedPluginId: params.entry.pluginId, expectedIntegrity: params.entry.install.expectedIntegrity, extensionsDir: resolveDefaultPluginExtensionsDir(), logger: { info: updateProgress, warn: (message) => { updateProgress(message); logInstallWarningWithSpacing(params.runtime, message); } } }), ONBOARDING_PLUGIN_INSTALL_WATCHDOG_TIMEOUT_MS); animated.stop(); progress.stop(result.ok ? formatPluginInstalled(safeLabel) : formatPluginInstallFailed(safeLabel)); return { status: "completed", result }; } catch (error) { animated.stop(); if (isTimeoutError(error)) { progress.stop(formatPluginInstallTimedOut(safeLabel)); return { status: "timed_out" }; } progress.stop(formatPluginInstallFailed(safeLabel)); throw error; } finally { animated.stop(); } } 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({ entry, npmSpec: params.override.spec, prompter, runtime, trustedSourceLinkedOfficialInstall: false }) : await installPluginFromNpmPackArchiveWithProgress({ entry, archivePath: params.override.archivePath, prompter, runtime }); 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 { cfg: params.cfg, installed: false, pluginId: entry.pluginId, status: "timed_out" }; } const { result } = installOutcome; if (!result.ok) { await prompter.note([t("wizard.plugins.installFailed", { spec: sanitizeTerminalText(displaySpec), error: summarizeInstallError(result.error) }), t("wizard.plugins.returningToSelection")].join("\n"), t("wizard.plugins.installTitle")); runtime.error?.(`Plugin install failed: ${sanitizeTerminalText(result.error)}`); return { cfg: params.cfg, installed: false, pluginId: entry.pluginId, status: "failed" }; } const enableResult = await applyPluginEnablement({ cfg: params.cfg, pluginId: result.pluginId, label: entry.label, prompter, runtime }); if (!enableResult.enabled) return { cfg: enableResult.config, installed: false, pluginId: result.pluginId, status: "failed" }; 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 { cfg: recordPluginInstall(enableResult.config, install), installed: true, pluginId: result.pluginId, status: "installed" }; } async function installPluginFromClawHubSpecWithProgress(params) { const safeLabel = sanitizeTerminalText(params.entry.label); const progress = params.prompter.progress(formatPluginInstallProgress(safeLabel)); const animated = createAnimatedInstallProgress(progress); animated.setLabel(t("wizard.plugins.preparingInstall")); const updateProgress = (message) => { const sanitized = sanitizeTerminalText(message).trim(); if (!sanitized) return; animated.setLabel(shortenInstallLabel(sanitized)); }; try { const { installPluginFromClawHub } = await import("./clawhub-CCyYIg6s.js"); const result = await withTimeout(installPluginFromClawHub({ spec: params.clawhubSpec, timeoutMs: ONBOARDING_PLUGIN_INSTALL_TIMEOUT_MS, extensionsDir: resolveDefaultPluginExtensionsDir(), expectedPluginId: params.entry.pluginId, mode: "install", logger: { info: updateProgress, warn: (message) => { updateProgress(message); logInstallWarningWithSpacing(params.runtime, message); } } }), ONBOARDING_PLUGIN_INSTALL_WATCHDOG_TIMEOUT_MS); animated.stop(); if (result.ok) progress.stop(formatPluginInstalled(safeLabel)); else progress.stop(formatPluginInstallFailed(safeLabel)); return { status: "completed", result }; } catch (error) { animated.stop(); if (isTimeoutError(error)) { progress.stop(formatPluginInstallTimedOut(safeLabel)); return { status: "timed_out" }; } progress.stop(formatPluginInstallFailed(safeLabel)); return { status: "completed", result: { ok: false, error: error instanceof Error ? error.message : String(error) } }; } } /** Ensures an onboarding plugin is installed, enabled, and recorded in config. */ async function ensureOnboardingPluginInstalled(params) { const { entry, prompter, runtime, workspaceDir } = params; let next = params.cfg; const installOverride = resolvePluginInstallOverride({ pluginId: entry.pluginId }); if (installOverride) { assertConfigWriteAllowedInCurrentMode(); return await installPluginFromOverride({ cfg: next, entry, override: installOverride, prompter, runtime }); } 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 }) : null; const npmSpecs = npmSpec ? resolveNpmInstallSpecsForUpdateChannel({ spec: npmSpec, updateChannel }) : 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 { cfg: next, installed: false, pluginId: entry.pluginId, status: "skipped" }; assertConfigWriteAllowedInCurrentMode(); if (choice === "local" && localPath) { const enableResult = await applyPluginEnablement({ cfg: next, pluginId: entry.pluginId, label: entry.label, prompter, runtime }); if (!enableResult.enabled) return { cfg: enableResult.config, installed: false, pluginId: entry.pluginId, status: "failed" }; if (pathsReferToSameDirectory(localPath, bundledLocalPath)) return { cfg: enableResult.config, installed: true, pluginId: entry.pluginId, status: "installed" }; next = addPluginLoadPath(enableResult.config, localPath); next = await recordLocalPluginInstall({ cfg: next, entry, localPath, npmSpec, workspaceDir }); return { cfg: next, installed: true, pluginId: entry.pluginId, status: "installed" }; } let shouldTryNpm = choice === "npm"; if (choice === "clawhub" && clawhubInstallSpec) { const installOutcome = await installPluginFromClawHubSpecWithProgress({ entry, clawhubSpec: clawhubInstallSpec, prompter, runtime }); if (installOutcome.status === "timed_out") { await prompter.note(formatPluginInstallTimedOutNote(sanitizeTerminalText(clawhubInstallSpec)), t("wizard.plugins.installTitle")); runtime.error?.(`Plugin install timed out after ${ONBOARDING_PLUGIN_INSTALL_TIMEOUT_MS}ms: ${sanitizeTerminalText(clawhubInstallSpec)}`); return { cfg: next, installed: false, pluginId: entry.pluginId, status: "timed_out" }; } const { result } = installOutcome; if (result.ok) { const enableResult = await applyPluginEnablement({ cfg: next, pluginId: result.pluginId, label: entry.label, prompter, runtime }); if (!enableResult.enabled) return { cfg: enableResult.config, installed: false, pluginId: result.pluginId, status: "failed" }; next = enableResult.config; next = recordPluginInstall(next, { pluginId: result.pluginId, ...buildClawHubPluginInstallRecordFields(result.clawhub), spec: clawhubSpecs?.recordSpec ?? clawhubInstallSpec, installPath: result.targetDir }); return { cfg: next, installed: true, pluginId: result.pluginId, status: "installed" }; } await prompter.note([t("wizard.plugins.installFailed", { spec: sanitizeTerminalText(clawhubInstallSpec), error: summarizeInstallError(result.error) }), t("wizard.plugins.returningToSelection")].join("\n"), t("wizard.plugins.installTitle")); if (!npmInstallSpec || !shouldFallbackClawHubToNpm({ result, npmSpec: npmInstallSpec })) { runtime.error?.(`Plugin install failed: ${sanitizeTerminalText(result.error)}`); return { cfg: next, installed: false, pluginId: entry.pluginId, status: "failed" }; } shouldTryNpm = await prompter.confirm({ message: t("wizard.plugins.useNpmPackageInstead", { spec: sanitizeTerminalText(npmInstallSpec) }), initialValue: true }); if (!shouldTryNpm) { runtime.error?.(`Plugin install failed: ${sanitizeTerminalText(result.error)}`); return { cfg: next, installed: false, pluginId: entry.pluginId, status: "failed" }; } } if (!shouldTryNpm || !npmInstallSpec) { await prompter.note(t("wizard.plugins.noRemoteInstallSource", { plugin: sanitizeTerminalText(entry.label) }), t("wizard.plugins.installTitle")); runtime.error?.(`Plugin install failed: no remote spec available for ${sanitizeTerminalText(entry.pluginId)}.`); return { cfg: next, installed: false, pluginId: entry.pluginId, status: "failed" }; } const installOutcome = await installPluginFromNpmSpecWithProgress({ entry, npmSpec: npmInstallSpec, prompter, runtime }); if (installOutcome.status === "timed_out") { await prompter.note(formatPluginInstallTimedOutNote(sanitizeTerminalText(npmInstallSpec)), t("wizard.plugins.installTitle")); runtime.error?.(`Plugin install timed out after ${ONBOARDING_PLUGIN_INSTALL_TIMEOUT_MS}ms: ${sanitizeTerminalText(npmInstallSpec)}`); return { cfg: next, installed: false, pluginId: entry.pluginId, status: "timed_out" }; } const { result } = installOutcome; if (result.ok) { const enableResult = await applyPluginEnablement({ cfg: next, pluginId: result.pluginId, label: entry.label, prompter, runtime }); if (!enableResult.enabled) return { cfg: enableResult.config, installed: false, pluginId: result.pluginId, status: "failed" }; next = enableResult.config; const install = { pluginId: result.pluginId, source: "npm", spec: resolveNpmInstallRecordSpec({ requestedSpec: npmSpecs?.recordSpec ?? npmInstallSpec, resolution: result.npmResolution, pinResolvedRegistrySpec: entry.trustedSourceLinkedOfficialInstall === true }), installPath: result.targetDir, version: result.version, ...buildNpmResolutionInstallFields(result.npmResolution) }; next = recordPluginInstall(next, install); return { cfg: next, installed: true, pluginId: result.pluginId, status: "installed" }; } await prompter.note([t("wizard.plugins.installFailed", { spec: sanitizeTerminalText(npmInstallSpec), error: summarizeInstallError(result.error) }), t("wizard.plugins.returningToSelection")].join("\n"), t("wizard.plugins.installTitle")); if (localPath) { if (await prompter.confirm({ message: t("wizard.plugins.useLocalPluginPathInstead", { path: sanitizeTerminalText(localPath) }), initialValue: true })) { const enableResult = await applyPluginEnablement({ cfg: next, pluginId: entry.pluginId, label: entry.label, prompter, runtime }); if (!enableResult.enabled) return { cfg: enableResult.config, installed: false, pluginId: entry.pluginId, status: "failed" }; if (pathsReferToSameDirectory(localPath, bundledLocalPath)) return { cfg: enableResult.config, installed: true, pluginId: entry.pluginId, status: "installed" }; next = addPluginLoadPath(enableResult.config, localPath); next = await recordLocalPluginInstall({ cfg: next, entry, localPath, npmSpec, workspaceDir }); return { cfg: next, installed: true, pluginId: entry.pluginId, status: "installed" }; } } runtime.error?.(`Plugin install failed: ${sanitizeTerminalText(result.error)}`); return { cfg: next, installed: false, pluginId: entry.pluginId, status: "failed" }; } //#endregion export { ensureOnboardingPluginInstalled as t };