openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
115 lines (114 loc) • 4.99 kB
JavaScript
import { n as resolveOpenClawPackageRootSync } from "./openclaw-root-CfYY-fyD.js";
import { m as shortenHomePath } from "./utils-P__uGsPB.js";
import { t as note } from "./note-DqHk3fA1.js";
import path from "node:path";
import fs from "node:fs/promises";
//#region src/commands/doctor/shared/plugin-runtime-symlinks.ts
const PLUGIN_RUNTIME_DEPS_MARKER = "plugin-runtime-deps";
const MAX_REPORTED = 6;
const DEFAULT_FS = {
readdir: (dir, options) => fs.readdir(dir, options),
lstat: (file) => fs.lstat(file),
readlink: (file) => fs.readlink(file),
stat: (file) => fs.stat(file),
rm: (file, options) => fs.rm(file, options),
unlink: (file) => fs.unlink(file)
};
/** Find global node_modules symlinks that still point at stale plugin-runtime deps. */
async function collectStalePluginRuntimeSymlinks(packageRoot = resolveOpenClawPackageRootSync({
argv1: process.argv[1],
moduleUrl: import.meta.url,
cwd: process.cwd()
}), options = {}) {
if (!packageRoot) return [];
const containingNodeModules = path.dirname(packageRoot);
if (path.basename(containingNodeModules) !== "node_modules") return [];
const fsApi = options.fs ?? DEFAULT_FS;
const stale = [];
const entries = await fsApi.readdir(containingNodeModules, { withFileTypes: true }).catch(() => []);
for (const entry of entries) {
if (entry.isDirectory() && entry.name.startsWith("@")) {
const scopeDir = path.join(containingNodeModules, entry.name);
const scopeEntries = await fsApi.readdir(scopeDir, { withFileTypes: true }).catch(() => []);
for (const scopeEntry of scopeEntries) {
const fullPath = path.join(scopeDir, scopeEntry.name);
const target = await inspectCandidate(fullPath, fsApi);
if (target) stale.push({
name: `${entry.name}/${scopeEntry.name}`,
path: fullPath,
target
});
}
continue;
}
if (!entry.isSymbolicLink()) continue;
const fullPath = path.join(containingNodeModules, entry.name);
const target = await inspectCandidate(fullPath, fsApi);
if (target) stale.push({
name: entry.name,
path: fullPath,
target
});
}
return stale.toSorted((left, right) => left.name.localeCompare(right.name));
}
function stalePluginRuntimeSymlinkToHealthFinding(item) {
return {
checkId: "core/doctor/stale-plugin-runtime-symlinks",
severity: "warning",
message: `Stale plugin-runtime symlink ${item.name} points at ${item.target}.`,
path: item.path,
target: item.path,
requirement: "stale-plugin-runtime-symlink-removed",
fixHint: "Run `openclaw doctor --fix` to remove stale plugin-runtime symlinks."
};
}
async function collectStalePluginRuntimeSymlinkHealthFindings(params = {}) {
return (await collectStalePluginRuntimeSymlinks(params.packageRoot, params)).map(stalePluginRuntimeSymlinkToHealthFinding);
}
/** Emit a doctor note describing stale plugin-runtime symlinks, if any exist. */
async function noteStalePluginRuntimeSymlinks(packageRoot, options = {}) {
const stale = await collectStalePluginRuntimeSymlinks(packageRoot, options);
if (stale.length === 0) return;
const shortenPath = options.shortenPath ?? shortenHomePath;
const lines = [
"- Plugin-runtime symlinks under the global Node prefix point at pruned",
` ${PLUGIN_RUNTIME_DEPS_MARKER} directories from a previous OpenClaw install.`,
"- Bundled plugin ESM imports can fail with ERR_MODULE_NOT_FOUND until repaired."
];
for (const item of stale.slice(0, MAX_REPORTED)) lines.push(` - ${item.name} -> ${shortenPath(item.target)}`);
if (stale.length > MAX_REPORTED) lines.push(` - ...and ${stale.length - MAX_REPORTED} more`);
lines.push("- Repair: run `openclaw doctor --fix` to remove the dangling symlinks.");
(options.noteFn ?? note)(lines.join("\n"), "Plugin-runtime symlinks");
}
/** Remove stale plugin-runtime symlinks and report changes/warnings. */
async function removeStalePluginRuntimeSymlinks(packageRoot, options = {}) {
const fsApi = options.fs ?? DEFAULT_FS;
const changes = [];
const warnings = [];
for (const item of await collectStalePluginRuntimeSymlinks(packageRoot, options)) try {
if (fsApi.unlink) await fsApi.unlink(item.path);
else await fsApi.rm(item.path, { force: true });
changes.push(`Removed stale plugin-runtime symlink: ${item.path}`);
} catch (error) {
warnings.push(`Failed to remove stale plugin-runtime symlink ${item.path}: ${String(error)}`);
}
return {
changes,
warnings
};
}
async function inspectCandidate(fullPath, fsApi) {
if (!(await fsApi.lstat(fullPath).catch(() => null))?.isSymbolicLink()) return null;
const target = await fsApi.readlink(fullPath).catch(() => null);
if (!target || !target.includes(PLUGIN_RUNTIME_DEPS_MARKER)) return null;
try {
await fsApi.stat(fullPath);
return null;
} catch (error) {
const code = error?.code;
return code === "ENOENT" || code === "ENOTDIR" ? target : null;
}
}
//#endregion
export { noteStalePluginRuntimeSymlinks as n, removeStalePluginRuntimeSymlinks as r, collectStalePluginRuntimeSymlinkHealthFindings as t };