UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

277 lines (276 loc) 9.12 kB
import { n as resolveOpenClawPackageRootSync } from "./openclaw-root-CNp1Ofdk.js"; import { o as hasErrnoCode } from "./errors-BXgSefBE.js"; import path from "node:path"; import fs from "node:fs/promises"; //#region src/plugins/plugin-peer-link.ts function readStringRecord(value) { if (typeof value !== "object" || value === null || Array.isArray(value)) return {}; const record = {}; for (const [key, raw] of Object.entries(value)) if (typeof raw === "string") record[key] = raw; return record; } async function readPackagePeerDependencies(packageDir) { try { const raw = await fs.readFile(path.join(packageDir, "package.json"), "utf8"); return readStringRecord(JSON.parse(raw).peerDependencies); } catch (error) { if (error.code === "ENOENT") return {}; throw error; } } async function listManagedNpmRootPackageDirs(npmRoot) { const nodeModulesDir = path.join(npmRoot, "node_modules"); let entries; try { entries = await fs.readdir(nodeModulesDir, { withFileTypes: true }); } catch (error) { if (error.code === "ENOENT") return []; throw error; } const packageDirs = []; for (const entry of entries) { if (!entry.isDirectory() || entry.name === ".bin") continue; const entryPath = path.join(nodeModulesDir, entry.name); if (entry.name.startsWith("@")) { const scopedEntries = await fs.readdir(entryPath, { withFileTypes: true }).catch((error) => { if (error.code === "ENOENT") return []; throw error; }); for (const scopedEntry of scopedEntries) if (scopedEntry.isDirectory()) packageDirs.push(path.join(entryPath, scopedEntry.name)); continue; } if (!entry.name.startsWith(".")) packageDirs.push(entryPath); } return packageDirs.toSorted((a, b) => a.localeCompare(b)); } async function safeRealpath(filePath) { try { return await fs.realpath(filePath); } catch { return null; } } function managedPackageNameFromDir(params) { return path.relative(path.join(params.npmRoot, "node_modules"), params.packageDir).split(path.sep).join("/"); } async function auditOpenClawPeerDependency(params) { const packageName = params.packageName ?? (params.npmRoot ? managedPackageNameFromDir({ npmRoot: params.npmRoot, packageDir: params.packageDir }) : path.basename(params.packageDir)); const nodeModulesDir = path.join(params.packageDir, "node_modules"); try { const existing = await fs.lstat(nodeModulesDir); if (!existing.isDirectory() || existing.isSymbolicLink()) return { packageName, packageDir: params.packageDir, reason: `${nodeModulesDir} is not a real directory` }; } catch (error) { if (error.code === "ENOENT") return { packageName, packageDir: params.packageDir, reason: `missing ${path.join(nodeModulesDir, "openclaw")}` }; throw error; } const linkPath = path.join(nodeModulesDir, "openclaw"); const currentTarget = await safeRealpath(linkPath); if (!currentTarget) return { packageName, packageDir: params.packageDir, reason: `missing ${linkPath}` }; const expectedTarget = await safeRealpath(params.hostRoot) ?? params.hostRoot; if (currentTarget !== expectedTarget) return { packageName, packageDir: params.packageDir, reason: `${linkPath} points to ${currentTarget} instead of ${expectedTarget}` }; return null; } async function auditOpenClawPeerDependencyLink(params) { const packageName = params.packageName ?? path.basename(params.packageDir); const hostRoot = resolveOpenClawPackageRootSync({ argv1: process.argv[1], moduleUrl: import.meta.url, cwd: process.cwd() }); if (!hostRoot) return { packageName, packageDir: params.packageDir, reason: "could not locate openclaw package root" }; return await auditOpenClawPeerDependency({ hostRoot, packageDir: params.packageDir, packageName }); } async function ensureRealNodeModulesDir(params) { const nodeModulesDir = path.join(params.installedDir, "node_modules"); try { const existing = await fs.lstat(nodeModulesDir); if (!existing.isDirectory() || existing.isSymbolicLink()) { params.logger.warn?.(`Skipping openclaw peerDependency link because ${nodeModulesDir} is not a real directory.`); return null; } return nodeModulesDir; } catch (error) { if (error.code !== "ENOENT") throw error; } await fs.mkdir(nodeModulesDir, { recursive: true }); const created = await fs.lstat(nodeModulesDir); if (!created.isDirectory() || created.isSymbolicLink()) { params.logger.warn?.(`Skipping openclaw peerDependency link because ${nodeModulesDir} is not a real directory.`); return null; } return nodeModulesDir; } async function linkOpenClawPeerDependency(params) { const nodeModulesDir = await ensureRealNodeModulesDir({ installedDir: params.installedDir, logger: params.logger }); if (!nodeModulesDir) return "skipped"; const linkPath = path.join(nodeModulesDir, params.peerName); const expectedTarget = await safeRealpath(params.hostRoot) ?? params.hostRoot; if (await safeRealpath(linkPath) === expectedTarget) return "unchanged"; try { const existing = await fs.lstat(linkPath).catch((err) => { if (hasErrnoCode(err, "ENOENT")) return null; throw err; }); if (existing) { if (!existing.isSymbolicLink()) { if (params.peerName === "openclaw" && existing.isDirectory()) { if (await readPackageName(linkPath) === "openclaw") { await fs.rm(linkPath, { recursive: true, force: true }); await fs.symlink(params.hostRoot, linkPath, "junction"); params.logger.info?.(`Linked peerDependency "${params.peerName}" -> ${params.hostRoot}`); return "linked"; } } params.logger.warn?.(`Skipping openclaw peerDependency link because ${linkPath} already exists and is not a symlink.`); return "skipped"; } await fs.unlink(linkPath); } await fs.symlink(params.hostRoot, linkPath, "junction"); params.logger.info?.(`Linked peerDependency "${params.peerName}" -> ${params.hostRoot}`); return "linked"; } catch (err) { params.logger.warn?.(`Failed to symlink peerDependency "${params.peerName}": ${String(err)}`); return "skipped"; } } async function readPackageName(packageDir) { try { const raw = await fs.readFile(path.join(packageDir, "package.json"), "utf8"); const parsed = JSON.parse(raw); return typeof parsed.name === "string" ? parsed.name : void 0; } catch (error) { if (error.code === "ENOENT") return; throw error; } } /** * Symlink the host openclaw package for plugins that declare it as a peer. * Plugin package managers still own third-party dependencies; this only wires * the host SDK package into the plugin-local Node graph. */ async function linkOpenClawPeerDependencies(params) { const peers = Object.keys(params.peerDependencies).filter((name) => name === "openclaw"); if (peers.length === 0) return { repaired: 0, skipped: 0 }; const hostRoot = resolveOpenClawPackageRootSync({ argv1: process.argv[1], moduleUrl: import.meta.url, cwd: process.cwd() }); if (!hostRoot) { params.logger.warn?.("Could not locate openclaw package root to symlink peerDependencies; plugin may fail to resolve openclaw at runtime."); return { repaired: 0, skipped: peers.length }; } let repaired = 0; let skipped = 0; for (const peerName of peers) { const result = await linkOpenClawPeerDependency({ hostRoot, installedDir: params.installedDir, peerName, logger: params.logger }); if (result === "linked") repaired += 1; else if (result === "skipped") skipped += 1; } return { repaired, skipped }; } async function relinkOpenClawPeerDependenciesInManagedNpmRoot(params) { let checked = 0; let attempted = 0; let repaired = 0; let skipped = 0; for (const packageDir of await listManagedNpmRootPackageDirs(params.npmRoot)) { const peerDependencies = await readPackagePeerDependencies(packageDir); if (!Object.hasOwn(peerDependencies, "openclaw")) continue; checked += 1; const result = await linkOpenClawPeerDependencies({ installedDir: packageDir, peerDependencies, logger: params.logger }); attempted += 1; repaired += result.repaired; skipped += result.skipped; } return { checked, attempted, repaired, skipped }; } async function auditOpenClawPeerDependenciesInManagedNpmRoot(params) { const hostRoot = resolveOpenClawPackageRootSync({ argv1: process.argv[1], moduleUrl: import.meta.url, cwd: process.cwd() }); if (!hostRoot) return { checked: 0, broken: 0, issues: [] }; let checked = 0; const issues = []; for (const packageDir of await listManagedNpmRootPackageDirs(params.npmRoot)) { const peerDependencies = await readPackagePeerDependencies(packageDir); if (!Object.hasOwn(peerDependencies, "openclaw")) continue; checked += 1; const issue = await auditOpenClawPeerDependency({ hostRoot, npmRoot: params.npmRoot, packageDir }); if (issue) issues.push(issue); } return { checked, broken: issues.length, issues }; } //#endregion export { relinkOpenClawPeerDependenciesInManagedNpmRoot as i, auditOpenClawPeerDependencyLink as n, linkOpenClawPeerDependencies as r, auditOpenClawPeerDependenciesInManagedNpmRoot as t };