UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

161 lines (160 loc) 7.28 kB
import { t as formatCliCommand } from "./command-format-CKGmlpAQ.js"; import { t as isContainerEnvironment } from "./container-environment-CNsJSTpY.js"; import { t as buildWorkspaceSkillStatus } from "./status-Dlj2eT9o.js"; import { t as resolveBrewExecutable } from "./brew-BuAbPCrG.js"; import { n as t } from "./i18n-7g_f4oaD.js"; import { t as detectBinary } from "./detect-binary-ihWW1rG_.js"; import { d as resolveNodeManagerOptions } from "./onboard-helpers-DXyUiHQ-.js"; import { n as patchSkillConfigEntry, t as installSkill } from "./install-CLxRzDc9.js"; //#region src/commands/onboard-skills.ts /** * Interactive skill dependency setup for onboarding. * * It reports workspace skill readiness, offers safe dependency installs, and * records per-skill API keys entered during setup. */ function summarizeInstallFailure(message) { const cleaned = message.replace(/^Install failed(?:\s*\([^)]*\))?\s*:?\s*/i, "").trim(); if (!cleaned) return; const maxLen = 140; return cleaned.length > maxLen ? `${cleaned.slice(0, maxLen - 1)}…` : cleaned; } function formatSkillHint(skill) { const desc = skill.description?.trim(); const installLabel = skill.install[0]?.label?.trim(); const combined = desc && installLabel ? `${desc}${installLabel}` : desc || installLabel; if (!combined) return "install"; const maxLen = 90; return combined.length > maxLen ? `${combined.slice(0, maxLen - 1)}…` : combined; } function isBrewOnlyInstallableSkill(skill) { return skill.install.length > 0 && skill.missing.bins.length > 0 && skill.install.every((option) => option.kind === "brew"); } /** Runs the interactive skills setup step and returns the updated config. */ async function setupSkills(cfg, workspaceDir, runtime, prompter) { const report = buildWorkspaceSkillStatus(workspaceDir, { config: cfg }); const eligible = report.skills.filter((s) => s.eligible); const unsupportedOs = report.skills.filter((s) => !s.disabled && !s.blockedByAllowlist && s.missing.os.length > 0); const missing = report.skills.filter((s) => !s.eligible && !s.disabled && !s.blockedByAllowlist && s.missing.os.length === 0); const blocked = report.skills.filter((s) => s.blockedByAllowlist); await prompter.note([ `Eligible: ${eligible.length}`, `Missing requirements: ${missing.length}`, `Unsupported on this OS: ${unsupportedOs.length}`, `Blocked by allowlist: ${blocked.length}` ].join("\n"), t("wizard.skills.statusTitle")); if (!await prompter.confirm({ message: t("wizard.skills.configure"), initialValue: true })) return cfg; const baseInstallable = missing.filter((skill) => skill.install.length > 0 && skill.missing.bins.length > 0); let brewAvailable; const detectBrewOnce = async () => { brewAvailable ??= await detectBinary("brew") || resolveBrewExecutable() !== void 0; return brewAvailable; }; const inLinuxContainer = process.platform === "linux" && isContainerEnvironment(); let installable = baseInstallable; if (inLinuxContainer && baseInstallable.length > 0 && !await detectBrewOnce()) { const hiddenBrewOnly = baseInstallable.filter(isBrewOnlyInstallableSkill); installable = baseInstallable.filter((skill) => !isBrewOnlyInstallableSkill(skill)); if (hiddenBrewOnly.length > 0) await prompter.note([t("wizard.skills.containerBrewHidden"), t("wizard.skills.containerBrewManual")].join("\n"), t("wizard.skills.containerInstallsTitle")); } let next = cfg; if (installable.length === 0 && missing.length === 0) { await prompter.note([ "No missing skill dependencies to install.", `To inspect available skills, run: ${formatCliCommand("openclaw skills list --verbose")}`, `To check skill status, run: ${formatCliCommand("openclaw skills check")}` ].join("\n"), t("wizard.skills.allReadyTitle") ?? "All skills ready"); return next; } if (installable.length > 0) { const selected = (await prompter.multiselect({ message: t("wizard.skills.installDeps"), options: [{ value: "__skip__", label: t("common.skipForNow"), hint: t("wizard.skills.skipDepsHint") }, ...installable.map((skill) => ({ value: skill.name, label: `${skill.emoji ?? "🧩"} ${skill.name}`, hint: formatSkillHint(skill) }))] })).filter((name) => name !== "__skip__"); const selectedSkills = selected.map((name) => installable.find((s) => s.name === name)).filter((item) => Boolean(item)); if (process.platform !== "win32" && selectedSkills.some((skill) => skill.install.some((option) => option.kind === "brew")) && !await detectBrewOnce()) { await prompter.note(["Many skill dependencies are shipped via Homebrew.", "Without brew, you'll need to build from source or download releases manually."].join("\n"), t("wizard.skills.homebrewRecommendedTitle")); if (await prompter.confirm({ message: t("wizard.skills.homebrewCommand"), initialValue: true })) await prompter.note(["Run:", "/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""].join("\n"), t("wizard.skills.homebrewInstallTitle")); } if (selectedSkills.some((skill) => skill.install.some((option) => option.kind === "node"))) { const nodeManager = await prompter.select({ message: t("wizard.skills.nodeManager"), options: resolveNodeManagerOptions() }); next = { ...next, skills: { ...next.skills, install: { ...next.skills?.install, nodeManager } } }; } for (const name of selected) { const target = installable.find((s) => s.name === name); if (!target || target.install.length === 0) continue; const installId = target.install[0]?.id; if (!installId) continue; const spin = prompter.progress(t("wizard.skills.installing", { name })); const result = await installSkill({ workspaceDir, skillName: target.name, installId, config: next }); const warnings = result.warnings ?? []; if (result.ok) { spin.stop(warnings.length > 0 ? t("wizard.skills.installedWithWarnings", { name }) : t("wizard.skills.installed", { name })); for (const warning of warnings) runtime.log(warning); continue; } const code = result.code == null ? "" : ` (exit ${result.code})`; const detail = summarizeInstallFailure(result.message); spin.stop(t("wizard.skills.installFailed", { name, code, detail: detail ? ` - ${detail}` : "" })); for (const warning of warnings) runtime.log(warning); if (result.stderr) runtime.log(result.stderr.trim()); else if (result.stdout) runtime.log(result.stdout.trim()); runtime.log(`Tip: run \`${formatCliCommand("openclaw doctor")}\` to review skills + requirements.`); runtime.log(t("wizard.skills.docsLine")); } } for (const skill of missing) { if (!skill.primaryEnv || skill.missing.env.length === 0) continue; if (!await prompter.confirm({ message: t("wizard.skills.setEnv", { env: skill.primaryEnv, name: skill.name }), initialValue: false })) continue; const apiKey = await prompter.text({ message: t("wizard.skills.enterEnv", { env: skill.primaryEnv }), validate: (value) => value?.trim() ? void 0 : t("common.required"), sensitive: true }); next = patchSkillConfigEntry(next, skill.skillKey, { apiKey }); } return next; } //#endregion export { setupSkills as t };