UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

259 lines (258 loc) 13 kB
import { o as isRecord } from "./record-coerce-DHZ4bFlT.js"; import { t as formatCliCommand } from "./command-format-CKGmlpAQ.js"; import { g as shortenHomePath } from "./utils-CCC-BEJH.js"; import { p as tryReadJsonSync } from "./json-files-2umMHm0W.js"; import { a as resolveDefaultPluginNpmDir } from "./install-paths-D-sCLR1I.js"; import { o as listManagedPluginNpmRootsSync, t as loadInstalledPluginIndexInstallRecords } from "./installed-plugin-index-record-reader-CPHayOZI.js"; import { r as loadInstalledPluginIndex } from "./installed-plugin-index-BSYm7o5l.js"; import { h as refreshPluginRegistry } from "./plugin-registry-ChcJPHWl.js"; import "./installed-plugin-index-records-DXbAzXDd.js"; import { n as saveJsonFile } from "./json-file-CVAOif1i.js"; import { i as preflightPluginRegistryInstallMigration, r as migratePluginRegistryForInstall, t as DISABLE_PLUGIN_REGISTRY_MIGRATION_ENV } from "./plugin-registry-migration-4oZJfqTw.js"; import { t as note } from "./note-BRSJp0UF.js"; import { i as relinkOpenClawPeerDependenciesInManagedNpmRoot, t as auditOpenClawPeerDependenciesInManagedNpmRoot } from "./plugin-peer-link-BWRhtS5B.js"; import { t as listStaleLocalBundledPluginInstallRecords } from "./stale-local-bundled-plugin-install-records-CdghDgKF.js"; import fs from "node:fs"; import path from "node:path"; //#region src/commands/doctor-plugin-registry.ts /** Doctor repairs for stale plugin registry entries, managed npm shadows, and peer links. */ function readJsonObject(filePath) { const parsed = tryReadJsonSync(filePath); return isRecord(parsed) ? parsed : null; } function readStringMap(value) { if (!isRecord(value)) return {}; const result = {}; for (const [key, raw] of Object.entries(value)) if (typeof raw === "string" && raw.trim()) result[key] = raw.trim(); return result; } function resolveManagedPluginNpmRoot(params) { return params.stateDir ? path.join(params.stateDir, "npm") : resolveDefaultPluginNpmDir(params.env); } function listManagedPluginNpmRoots(params) { return listManagedPluginNpmRootsSync(resolveManagedPluginNpmRoot(params)); } function deleteObjectKey(record, key) { if (!Object.hasOwn(record, key)) return false; delete record[key]; return true; } function readPackageVersion(packageDir) { const version = readJsonObject(path.join(packageDir, "package.json"))?.version; return typeof version === "string" && version.trim() ? version.trim() : void 0; } function readPluginManifestId(packageDir) { const id = readJsonObject(path.join(packageDir, "openclaw.plugin.json"))?.id; return typeof id === "string" && id.trim() ? id.trim() : void 0; } function listStaleManagedNpmBundledPlugins(params) { const currentBundled = loadInstalledPluginIndex({ ...params, installRecords: {} }).plugins.filter((plugin) => plugin.origin === "bundled" && plugin.packageName); const bundledByPackage = new Map(currentBundled.map((plugin) => [plugin.packageName, plugin])); const stale = []; for (const npmRoot of listManagedPluginNpmRoots(params)) { const dependencies = readStringMap(readJsonObject(path.join(npmRoot, "package.json"))?.dependencies); for (const packageName of Object.keys(dependencies).toSorted((left, right) => left.localeCompare(right))) { if (!packageName.startsWith("@openclaw/")) continue; const bundled = bundledByPackage.get(packageName); if (!bundled) continue; const packageDir = path.join(npmRoot, "node_modules", ...packageName.split("/")); const pluginId = readPluginManifestId(packageDir); if (!pluginId || pluginId !== bundled.pluginId) continue; stale.push({ pluginId, packageName, packageDir, npmRoot, ...readPackageVersion(packageDir) ? { version: readPackageVersion(packageDir) } : {} }); } } return stale; } function loadCurrentBundledPluginSources(params) { const currentBundled = loadInstalledPluginIndex({ ...params, installRecords: {} }).plugins.filter((plugin) => plugin.origin === "bundled"); return new Map(currentBundled.map((plugin) => [plugin.pluginId, { pluginId: plugin.pluginId, localPath: plugin.rootDir, ...plugin.packageName ? { npmSpec: plugin.packageName } : {}, ...plugin.packageVersion ? { version: plugin.packageVersion } : {} }])); } async function listStaleLocalBundledPluginInstallRecordShadows(params) { return listStaleLocalBundledPluginInstallRecords({ installRecords: await loadInstalledPluginIndexInstallRecords(params), workspaceDir: params.workspaceDir, env: params.env, bundled: loadCurrentBundledPluginSources(params) }); } function removeManagedNpmDependency(params) { const npmPackageJsonPath = path.join(params.npmRoot, "package.json"); const packageJson = readJsonObject(npmPackageJsonPath) ?? {}; const dependencies = readStringMap(packageJson.dependencies); delete dependencies[params.packageName]; saveJsonFile(npmPackageJsonPath, Object.keys(dependencies).length === 0 ? (() => { const { dependencies: _dependencies, ...rest } = packageJson; return rest; })() : { ...packageJson, dependencies }); removeManagedNpmPackageLockDependency(params); fs.rmSync(params.packageDir, { recursive: true, force: true }); const scopeDir = path.dirname(params.packageDir); if (path.basename(path.dirname(scopeDir)) === "node_modules") try { fs.rmdirSync(scopeDir); } catch {} } function removeManagedNpmPackageLockDependency(params) { const packageLockPath = path.join(params.npmRoot, "package-lock.json"); const packageLock = readJsonObject(packageLockPath); if (!packageLock) return; let changed = false; const packages = packageLock.packages; if (isRecord(packages)) { const rootPackage = packages[""]; if (isRecord(rootPackage)) { const rootDependencies = readStringMap(rootPackage.dependencies); if (deleteObjectKey(rootDependencies, params.packageName)) { changed = true; if (Object.keys(rootDependencies).length === 0) delete rootPackage.dependencies; else rootPackage.dependencies = rootDependencies; } } changed = deleteObjectKey(packages, `node_modules/${params.packageName}`) || changed; } const dependencies = packageLock.dependencies; if (isRecord(dependencies)) changed = deleteObjectKey(dependencies, params.packageName) || changed; if (changed) saveJsonFile(packageLockPath, packageLock); } /** Removes managed npm packages that shadow current bundled plugins when repair is enabled. */ function maybeRepairStaleManagedNpmBundledPlugins(params) { const stale = listStaleManagedNpmBundledPlugins(params); if (stale.length === 0) return false; if (!params.prompter.shouldRepair) { note([ "Managed npm plugin packages shadow bundled plugins:", ...stale.map((plugin) => `- ${plugin.pluginId}: ${plugin.packageName}${plugin.version ? `@${plugin.version}` : ""}`), `Repair with ${formatCliCommand("openclaw doctor --fix")} to remove stale managed npm packages and rebuild the plugin registry.` ].join("\n"), "Plugin registry"); return false; } for (const plugin of stale) removeManagedNpmDependency(plugin); note(["Removed stale managed npm plugin package(s) shadowing bundled plugins:", ...stale.map((plugin) => `- ${plugin.pluginId}: ${plugin.packageName}${plugin.version ? `@${plugin.version}` : ""}`)].join("\n"), "Plugin registry"); return true; } /** Removes local install records that shadow current bundled plugin sources. */ async function maybeRepairStaleLocalBundledPluginInstallRecords(params) { const stale = await listStaleLocalBundledPluginInstallRecordShadows(params); if (stale.length === 0) return []; if (!params.prompter.shouldRepair) { note([ "Local bundled plugin install records shadow bundled plugins:", ...stale.map((record) => `- ${record.pluginId}: ${shortenHomePath(record.stalePath)}`), `Repair with ${formatCliCommand("openclaw doctor --fix")} to remove stale local install records and rebuild the plugin registry.` ].join("\n"), "Plugin registry"); return []; } note(["Removed stale local bundled plugin install record(s) shadowing bundled plugins:", ...stale.map((record) => `- ${record.pluginId}: ${shortenHomePath(record.stalePath)}`)].join("\n"), "Plugin registry"); return stale.map((record) => record.pluginId); } /** Relinks managed npm plugin packages to the current OpenClaw host packages. */ async function maybeRepairManagedNpmOpenClawPeerLinks(params) { const npmRoots = listManagedPluginNpmRoots(params); if (!params.prompter.shouldRepair) { const issues = (await Promise.all(npmRoots.map((npmRoot) => auditOpenClawPeerDependenciesInManagedNpmRoot({ npmRoot })))).flatMap((audit) => audit.issues); if (issues.length > 0) note([ "Managed npm OpenClaw host peer links need repair:", ...issues.map((issue) => `- ${issue.packageName}: ${issue.reason}`), `Repair with ${formatCliCommand("openclaw doctor --fix")} to relink managed npm plugin packages.` ].join("\n"), "Plugin registry"); return false; } const messages = []; const logger = { info: (message) => messages.push({ level: "info", message }), warn: (message) => messages.push({ level: "warn", message }) }; const repaired = (await Promise.all(npmRoots.map((npmRoot) => relinkOpenClawPeerDependenciesInManagedNpmRoot({ npmRoot, logger })))).reduce((total, result) => total + result.repaired, 0); if (repaired > 0) note(`Repaired OpenClaw host peer link(s) for ${repaired} managed npm plugin package(s).`, "Plugin registry"); const warnings = messages.filter((message) => message.level === "warn").map((message) => `- ${message.message}`); if (warnings.length > 0) note(["Could not repair all managed npm OpenClaw host peer links:", ...warnings].join("\n"), "Plugin registry"); return repaired > 0; } async function loadInstallRecordsWithoutPluginIds(params, pluginIds) { const records = await loadInstalledPluginIndexInstallRecords(params); for (const pluginId of pluginIds) delete records[pluginId]; return records; } /** * Runs plugin registry doctor repairs and refreshes the persisted plugin index when needed. * * Stale bundled shadows are removed before registry migration so the rebuilt index resolves the * current bundled source instead of an obsolete managed/local install record. */ async function maybeRepairPluginRegistryState(params) { const preflight = preflightPluginRegistryInstallMigration(params); for (const warning of preflight.deprecationWarnings) note(warning, "Plugin registry"); if (preflight.action === "disabled") { note(`${DISABLE_PLUGIN_REGISTRY_MIGRATION_ENV} is set; skipping plugin registry repair.`, "Plugin registry"); return params.config; } const migrationParams = { ...params, config: params.config }; const staleManagedNpmBundledPluginIds = listStaleManagedNpmBundledPlugins(params).map((plugin) => plugin.pluginId); const removedStaleManagedNpmBundledPlugins = maybeRepairStaleManagedNpmBundledPlugins(params); const removedStaleLocalBundledPluginIds = await maybeRepairStaleLocalBundledPluginInstallRecords(params); const repairedManagedNpmOpenClawPeerLinks = await maybeRepairManagedNpmOpenClawPeerLinks(params); const stalePluginIdsToRemove = [...new Set([...removedStaleManagedNpmBundledPlugins ? staleManagedNpmBundledPluginIds : [], ...removedStaleLocalBundledPluginIds])]; if (!params.prompter.shouldRepair) { if (preflight.action === "migrate") note(["Persisted plugin registry is missing or stale.", `Repair with ${formatCliCommand("openclaw doctor --fix")} to rebuild ${shortenHomePath(preflight.filePath)} from enabled plugins.`].join("\n"), "Plugin registry"); return params.config; } if (preflight.action === "migrate") { const result = await migratePluginRegistryForInstall({ ...migrationParams, ...stalePluginIdsToRemove.length > 0 ? { installRecords: await loadInstallRecordsWithoutPluginIds(params, stalePluginIdsToRemove) } : {} }); if (result.migrated) { const total = result.current.plugins.length; const enabled = result.current.plugins.filter((plugin) => plugin.enabled).length; note(`Plugin registry rebuilt: ${enabled}/${total} enabled plugins indexed.`, "Plugin registry"); } return params.config; } if (preflight.action === "skip-existing" || removedStaleManagedNpmBundledPlugins || removedStaleLocalBundledPluginIds.length > 0 || repairedManagedNpmOpenClawPeerLinks) { const index = await refreshPluginRegistry({ ...migrationParams, reason: "migration", ...stalePluginIdsToRemove.length > 0 ? { installRecords: await loadInstallRecordsWithoutPluginIds(params, stalePluginIdsToRemove) } : {} }); const total = index.plugins.length; const enabled = index.plugins.filter((plugin) => plugin.enabled).length; note(`Plugin registry refreshed: ${enabled}/${total} enabled plugins indexed.`, "Plugin registry"); } return params.config; } //#endregion export { maybeRepairStaleManagedNpmBundledPlugins as i, maybeRepairPluginRegistryState as n, maybeRepairStaleLocalBundledPluginInstallRecords as r, maybeRepairManagedNpmOpenClawPeerLinks as t };