openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
167 lines (166 loc) • 6.7 kB
JavaScript
import { n as defaultRuntime, r as writeRuntimeJson } from "./runtime-B4lgFmsS.js";
import { r as validatePackageExtensionEntriesForInstall } from "./package-entry-resolution-XklWAQ1C.js";
import { r as readPersistedInstalledPluginIndex } from "./installed-plugin-index-store-DyTBte5L.js";
import fs from "node:fs";
import path from "node:path";
import fs$1 from "node:fs/promises";
import crypto from "node:crypto";
//#region src/commands/doctor-post-upgrade.types.ts
/** Probe codes emitted by post-upgrade validation. */
const POST_UPGRADE_PROBE_CODES = [
"plugin.index_unavailable",
"plugin.entry_unresolved",
"plugin.manifest_drift"
];
//#endregion
//#region src/commands/doctor-post-upgrade.ts
/** Post-upgrade validation probes for persisted plugin index and package extension entries. */
function buildReport(findings) {
return {
probesRun: [...POST_UPGRADE_PROBE_CODES],
findings
};
}
function isInstallsJson(value) {
return typeof value === "object" && value !== null && Array.isArray(value.plugins) && value.plugins.every(isInstalledPluginRecord);
}
function isOptionalString(value) {
return value === void 0 || typeof value === "string";
}
function isPackageJsonRef(value) {
return value === void 0 || typeof value === "object" && value !== null && typeof value.path === "string";
}
function isSourceCheckoutPluginRecord(record) {
if (record.origin === "workspace" || record.origin === "config") return true;
return record.origin === "bundled" && isBundledSourceCheckoutPluginRoot(record.rootDir);
}
function isBundledSourceCheckoutPluginRoot(pluginRootDir) {
let current = path.resolve(pluginRootDir);
while (true) {
const extensionsDir = path.dirname(current);
if (path.basename(extensionsDir) === "extensions") {
const packageRoot = path.dirname(extensionsDir);
return fs.existsSync(path.join(packageRoot, ".git")) && fs.existsSync(path.join(packageRoot, "pnpm-workspace.yaml")) && fs.existsSync(path.join(packageRoot, "src"));
}
const next = path.dirname(current);
if (next === current) return false;
current = next;
}
}
function isInstalledPluginRecord(value) {
if (typeof value !== "object" || value === null) return false;
const record = value;
return typeof record.pluginId === "string" && typeof record.rootDir === "string" && typeof record.enabled === "boolean" && isOptionalString(record.origin) && isPackageJsonRef(record.packageJson) && isOptionalString(record.manifestPath) && isOptionalString(record.manifestHash);
}
async function readInstallsJson(installsPath) {
try {
const installsRaw = await fs$1.readFile(installsPath, "utf-8");
const installs = JSON.parse(installsRaw);
return isInstallsJson(installs) ? installs : null;
} catch {
return null;
}
}
async function readInstalledPluginIndex(params) {
if (params.installsPath) return await readInstallsJson(params.installsPath);
const index = await readPersistedInstalledPluginIndex(params.stateDir ? { stateDir: params.stateDir } : {});
return index && isInstallsJson(index) ? { plugins: [...index.plugins] } : null;
}
async function readInstalledPackageJson(rootDir, packageJsonRelPath) {
const absPath = path.join(rootDir, packageJsonRelPath);
const raw = await fs$1.readFile(absPath, "utf-8");
return JSON.parse(raw);
}
async function resolvePackageJsonRelPath(record) {
if (record.packageJson) return record.packageJson.path;
try {
await fs$1.access(path.join(record.rootDir, "package.json"));
return "package.json";
} catch {
return;
}
}
async function sha256OfFile(absPath) {
try {
const raw = await fs$1.readFile(absPath);
return crypto.createHash("sha256").update(raw).digest("hex");
} catch {
return null;
}
}
/** Runs post-upgrade plugin probes and returns structured findings for the caller to render. */
async function runPostUpgradeProbes(params) {
const findings = [];
const installs = await readInstalledPluginIndex(params);
if (!installs) {
findings.push({
level: "error",
code: "plugin.index_unavailable",
message: "Installed plugin index is missing, unreadable, or malformed. Run `openclaw plugins registry --refresh` to rebuild it before post-upgrade validation."
});
return buildReport(findings);
}
for (const record of installs.plugins) {
if (!record.enabled) continue;
const pkgRelPath = await resolvePackageJsonRelPath(record);
if (pkgRelPath) {
let pkg;
try {
pkg = await readInstalledPackageJson(record.rootDir, pkgRelPath);
} catch (err) {
process.stderr.write(`[doctor-post-upgrade] could not read package.json for ${record.pluginId} at ${record.rootDir}: ${err instanceof Error ? err.message : String(err)}\n`);
continue;
}
const entries = pkg.openclaw?.extensions ?? [];
if (entries.length > 0) {
const validation = await validatePackageExtensionEntriesForInstall({
packageDir: record.rootDir,
extensions: [...entries],
manifest: pkg,
allowSourceTypeScriptEntries: isSourceCheckoutPluginRecord(record)
});
if (!validation.ok) {
const offendingEntry = entries.find((entry) => validation.error.includes(entry));
findings.push({
level: "error",
code: "plugin.entry_unresolved",
message: `Plugin ${record.pluginId}: ${validation.error}`,
plugin: record.pluginId,
...offendingEntry ? { entry: offendingEntry } : {}
});
}
}
}
if (record.manifestPath && record.manifestHash) {
const currentHash = await sha256OfFile(record.manifestPath);
if (currentHash && currentHash !== record.manifestHash) findings.push({
level: "warn",
code: "plugin.manifest_drift",
message: `Plugin ${record.pluginId} manifest hash drifted from installs.json snapshot. Run \`openclaw plugins registry --refresh\` to re-sync.`,
plugin: record.pluginId
});
}
}
return buildReport(findings);
}
//#endregion
//#region src/commands/doctor.ts
/** Top-level doctor command wrapper, including post-upgrade probe mode. */
/** Runs doctor or the post-upgrade probe submode using the provided runtime. */
async function doctorCommand(runtime, options) {
if (options?.postUpgrade) {
const outputRuntime = runtime ?? defaultRuntime;
const report = await runPostUpgradeProbes({});
if (options.json) writeRuntimeJson(outputRuntime, report);
else {
for (const f of report.findings) outputRuntime.log(`[${f.level}] ${f.code}: ${f.message}`);
if (report.findings.length === 0) outputRuntime.log("post-upgrade: no findings");
}
const hasError = report.findings.some((f) => f.level === "error");
outputRuntime.exit(hasError ? 1 : 0);
return;
}
await (await import("./doctor-health-74nI8Gox.js")).doctorCommand(runtime, options);
}
//#endregion
export { doctorCommand as t };