openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
369 lines (368 loc) • 13.5 kB
JavaScript
import { r as resolveOpenClawPackageRootSync } from "./openclaw-root-CBoxhfke.mjs";
import { o as resolveUserPath } from "./home-dir-Dc6nN6HB.mjs";
import { a as asOptionalRecord } from "./record-coerce-TIsquoVR.mjs";
import { t as hasErrnoCode } from "./errno-FR4CRyya.mjs";
import { r as isPathInside } from "./path-guards-CH7noY0Z.mjs";
import "./errors-O33D01XW.mjs";
import { c as readRootJsonObjectSync } from "./json-files-C5mNTScg.mjs";
import { a as safeRealpathSync } from "./path-safety-6KJ2Rbnw.mjs";
import { c as resolvePluginInstallDir } from "./install-paths-R7__5hE9.mjs";
import { lstatSync, symlinkSync, unlinkSync } from "node:fs";
import path from "node:path";
import fs$1 from "node:fs/promises";
//#region src/plugins/npm-package-dirs.ts
async function listNpmPackageDirs(npmRoot, options) {
const readEntries = async (dir) => {
try {
const entries = await fs$1.readdir(dir, { withFileTypes: true });
return options.sortEntries ? entries.toSorted((left, right) => left.name.localeCompare(right.name)) : entries;
} catch (error) {
if (hasErrnoCode(error, "ENOENT")) return [];
throw error;
}
};
const nodeModulesDir = path.join(npmRoot, "node_modules");
const packageDirs = [];
for (const entry of await readEntries(nodeModulesDir)) {
if (!options.includeEntry(entry, false)) continue;
const entryPath = path.join(nodeModulesDir, entry.name);
if (entry.name.startsWith("@")) {
for (const scopedEntry of await readEntries(entryPath)) if (options.includeEntry(scopedEntry, true)) packageDirs.push(path.join(entryPath, scopedEntry.name));
} else packageDirs.push(entryPath);
}
return packageDirs;
}
//#endregion
//#region src/plugins/plugin-peer-link.ts
/** Resolve the host declaration consistently for peer, direct, and optional dependencies. */
function resolveOpenClawHostDependency(manifest) {
for (const declaration of [
"peerDependencies",
"optionalDependencies",
"dependencies"
]) {
const spec = asOptionalRecord(manifest[declaration])?.openclaw;
if (typeof spec === "string" && spec) return {
declaration,
spec
};
}
return null;
}
async function readSafePackageManifest(packageDir) {
const result = readRootJsonObjectSync({
rootDir: packageDir,
relativePath: "package.json",
boundaryLabel: "installed plugin package directory"
});
if (!result.ok) {
if (result.reason === "open" && result.failure.error?.code === "ENOENT") return null;
if (result.reason === "parse") throw new SyntaxError(result.error);
if (result.reason === "open" && result.failure.error instanceof Error) throw result.failure.error;
throw new Error(`Could not safely read package.json from ${packageDir}: ${result.reason === "open" ? result.failure.reason : result.error}`);
}
return result.value;
}
async function readPackageOpenClawLinkDependencies(packageDir, onPackageReadError) {
try {
const manifest = await readSafePackageManifest(packageDir);
const dependency = manifest ? resolveOpenClawHostDependency(manifest) : null;
return dependency ? { openclaw: dependency.spec } : {};
} catch (error) {
if (!onPackageReadError) throw error;
onPackageReadError(error, packageDir);
return;
}
}
async function listManagedNpmRootPackageDirs(npmRoot) {
return (await listNpmPackageDirs(npmRoot, { includeEntry: (entry, scoped) => entry.isDirectory() && (scoped || !entry.name.startsWith(".")) })).toSorted((a, b) => a.localeCompare(b));
}
async function safeRealpath(filePath) {
try {
return await fs$1.realpath(filePath);
} catch {
return null;
}
}
function managedPackageNameFromDir(params) {
return path.relative(path.join(params.npmRoot, "node_modules"), params.packageDir).split(path.sep).join("/");
}
function auditOpenClawPeerDependency(params) {
const packageName = params.packageName ?? (params.npmRoot ? managedPackageNameFromDir({
npmRoot: params.npmRoot,
packageDir: params.packageDir
}) : path.basename(params.packageDir));
const issue = (reason) => ({
packageName,
packageDir: params.packageDir,
reason
});
const nodeModulesDir = path.join(params.packageDir, "node_modules");
try {
const existing = lstatSync(nodeModulesDir);
if (!existing.isDirectory() || existing.isSymbolicLink()) return issue(`${nodeModulesDir} is not a real directory`);
} catch (error) {
if (error.code === "ENOENT") return issue(`missing ${path.join(nodeModulesDir, "openclaw")}`);
throw error;
}
const linkPath = path.join(nodeModulesDir, "openclaw");
const currentTarget = safeRealpathSync(linkPath);
if (!currentTarget) return issue(`missing ${linkPath}`);
const expectedTarget = safeRealpathSync(params.hostRoot) ?? params.hostRoot;
if (currentTarget !== expectedTarget) return issue(`${linkPath} points to ${currentTarget} instead of ${expectedTarget}`);
return null;
}
function auditOpenClawPeerDependencyLinkSync(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 auditOpenClawPeerDependency({
hostRoot,
packageDir: params.packageDir,
packageName
});
}
async function auditOpenClawPeerDependencyLink(params) {
return auditOpenClawPeerDependencyLinkSync(params);
}
/** Audit the installed host only when the package actually declares an OpenClaw dependency. */
async function auditDeclaredOpenClawHostDependency(params) {
const dependencies = await readPackageOpenClawLinkDependencies(params.packageDir);
if (!dependencies || !Object.hasOwn(dependencies, "openclaw")) return null;
return await auditOpenClawPeerDependencyLink(params);
}
async function ensureRealNodeModulesDir(params) {
const nodeModulesDir = path.join(params.installedDir, "node_modules");
let existing;
try {
existing = await fs$1.lstat(nodeModulesDir);
} catch (error) {
if (!hasErrnoCode(error, "ENOENT")) throw error;
}
if (!existing) {
await params.beforePersistentEffect?.();
params.beforePersistentApply?.();
await fs$1.mkdir(nodeModulesDir, { recursive: true });
existing = await fs$1.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;
}
async function linkOpenClawPeerDependency(params) {
const nodeModulesDir = await ensureRealNodeModulesDir(params);
if (!nodeModulesDir) return "skipped";
const linkPath = path.join(nodeModulesDir, "openclaw");
const expectedTarget = await safeRealpath(params.hostRoot) ?? params.hostRoot;
if (await safeRealpath(linkPath) === expectedTarget) return "unchanged";
const warn = (error) => {
params.beforePersistentApply?.();
params.logger.warn?.(`Failed to symlink peerDependency "openclaw": ${String(error)}`);
return "skipped";
};
let existing;
try {
existing = await fs$1.lstat(linkPath).catch((error) => {
if (hasErrnoCode(error, "ENOENT")) return null;
throw error;
});
if (existing && !existing.isSymbolicLink() && (!existing.isDirectory() || await readPackageName(linkPath) !== "openclaw")) {
params.logger.warn?.(`Skipping openclaw peerDependency link because ${linkPath} already exists and is not a symlink.`);
return "skipped";
}
} catch (error) {
return warn(error);
}
if (existing) {
await params.beforePersistentEffect?.();
params.beforePersistentApply?.();
try {
if (existing.isSymbolicLink()) unlinkSync(linkPath);
else await fs$1.rm(linkPath, {
recursive: true,
force: true
});
} catch (error) {
return warn(error);
}
}
await params.beforePersistentEffect?.();
params.beforePersistentApply?.();
try {
symlinkSync(params.hostRoot, linkPath, "junction");
params.logger.info?.(`Linked peerDependency "openclaw" -> ${params.hostRoot}`);
return "linked";
} catch (error) {
return warn(error);
}
}
async function readPackageName(packageDir) {
const manifest = await readSafePackageManifest(packageDir);
return typeof manifest?.name === "string" ? manifest.name : void 0;
}
/**
* Symlink the host openclaw package for plugins that declare it as a dependency.
* 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) {
if (!Object.keys(params.peerDependencies).includes("openclaw")) return {
repaired: 0,
skipped: 0
};
const hostRoot = params.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: 1
};
}
const result = await linkOpenClawPeerDependency({
...params,
hostRoot
});
return {
repaired: result === "linked" ? 1 : 0,
skipped: result === "skipped" ? 1 : 0
};
}
/**
* Repair registered package installs named by the authoritative install ledger.
* Local/path installs and symlink escapes remain developer-owned and are never mutated.
*/
async function reconcileRegisteredOpenClawHostLinks(params) {
const extensionsRoot = path.resolve(params.extensionsDir);
const extensionsRootRealPath = await safeRealpath(extensionsRoot);
if (!extensionsRootRealPath) return {
checked: 0,
repaired: 0,
skipped: 0,
issues: []
};
let checked = 0;
let repaired = 0;
let skipped = 0;
const issues = [];
for (const [pluginId, record] of Object.entries(params.installRecords).toSorted(([left], [right]) => left.localeCompare(right))) {
if (record.source !== "npm" && record.source !== "clawhub" && record.source !== "archive" || !record.installPath?.trim()) continue;
let packageDir;
let expectedPackageDir;
try {
packageDir = path.resolve(resolveUserPath(record.installPath, params.env));
expectedPackageDir = path.resolve(resolvePluginInstallDir(pluginId, extensionsRoot));
} catch {
continue;
}
if (packageDir !== expectedPackageDir) continue;
const packageRealPath = await safeRealpath(packageDir);
const expectedPackageRealPath = path.join(extensionsRootRealPath, path.relative(extensionsRoot, expectedPackageDir));
if (!packageRealPath || !isPathInside(extensionsRootRealPath, packageRealPath) || packageRealPath !== expectedPackageRealPath) continue;
const dependencies = await readPackageOpenClawLinkDependencies(packageDir, params.onPackageReadError);
if (!dependencies) {
skipped += 1;
continue;
}
if (!Object.hasOwn(dependencies, "openclaw")) continue;
checked += 1;
const issue = await auditOpenClawPeerDependencyLink({
packageDir,
packageName: pluginId
});
if (!issue) continue;
issues.push(issue);
if (params.mode !== "repair") continue;
const result = await linkOpenClawPeerDependencies({
installedDir: packageDir,
peerDependencies: dependencies,
logger: params.logger ?? {},
beforePersistentApply: params.beforePersistentApply,
beforePersistentEffect: params.beforePersistentEffect
});
repaired += result.repaired;
skipped += result.skipped;
}
return {
checked,
repaired,
skipped,
issues
};
}
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 openClawLinkDependencies = await readPackageOpenClawLinkDependencies(packageDir, params.onPackageReadError);
if (!openClawLinkDependencies) {
skipped += 1;
continue;
}
if (!Object.hasOwn(openClawLinkDependencies, "openclaw")) continue;
checked += 1;
const result = await linkOpenClawPeerDependencies({
installedDir: packageDir,
peerDependencies: openClawLinkDependencies,
logger: params.logger,
beforePersistentApply: params.beforePersistentApply,
beforePersistentEffect: params.beforePersistentEffect
});
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 openClawLinkDependencies = await readPackageOpenClawLinkDependencies(packageDir, params.onPackageReadError);
if (!openClawLinkDependencies || !Object.hasOwn(openClawLinkDependencies, "openclaw")) continue;
checked += 1;
const issue = auditOpenClawPeerDependency({
hostRoot,
npmRoot: params.npmRoot,
packageDir
});
if (issue) issues.push(issue);
}
return {
checked,
broken: issues.length,
issues
};
}
//#endregion
export { linkOpenClawPeerDependencies as a, resolveOpenClawHostDependency as c, auditOpenClawPeerDependencyLinkSync as i, listNpmPackageDirs as l, auditOpenClawPeerDependenciesInManagedNpmRoot as n, reconcileRegisteredOpenClawHostLinks as o, auditOpenClawPeerDependencyLink as r, relinkOpenClawPeerDependenciesInManagedNpmRoot as s, auditDeclaredOpenClawHostDependency as t };