openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
331 lines (330 loc) • 13.2 kB
JavaScript
import { S as createConfigIO, s as readConfigFileSnapshot } from "./io.runtime-B9iJRs3w.js";
import { E as tryResolveDefaultAgentId, m as resolveAgentWorkspaceDir } from "./agent-scope-config-DcbEhP0R.js";
import { f as resolveConfigPath, w as resolveStateDir } from "./paths-D2sRr1a_.js";
import { i as prepareSqliteReadOnlyLocationSync } from "./sqlite-readonly-location-BC9PgENz.js";
import { s as resolveOpenClawStateSqlitePath } from "./openclaw-state-db-schema-version-c1ZL6JGz.js";
import { i as withPluginInstallRoots, r as resolvePluginInstallRoots } from "./install-root-context-BSQp2WLR.js";
import "./agent-scope-DbtJyKUL.js";
import { s as maybeLoadDotEnvForConfig } from "./io.read-helpers-ZKp-UiGx.js";
import "./config-Cs0XXL3x.js";
import { i as configValidationIssuesToHealthFindings } from "./doctor-core-checks-BvtLKzuC.js";
import { t as scrubDoctorErrorMessage } from "./doctor-error-message-PVjhuNzO.js";
import { i as listExtensionHealthChecksForDoctor } from "./health-check-registry-CBs_fO63.js";
import { n as healthFindingMeetsSeverity, r as parseHealthFindingSeverity } from "./health-checks-DrfuiOOz.js";
import { n as runDoctorLintChecks, r as selectUpdateReadinessChecks, t as exitCodeFromFindings } from "./doctor-lint-flow-Dzj8Qk94.js";
import { u as isPostCoreConvergencePass } from "./update-phase-J3w3q1-f.js";
import { n as resolveBundledHealthCheckPluginStateMode, t as registerBundledHealthChecks } from "./bundled-health-checks-Pa6weB1A.js";
import { t as resolveDoctorContributionHealthChecks } from "./doctor-health-contributions-ILex_TEG.js";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
//#region src/commands/doctor-lint.ts
/** CLI entrypoint for non-mutating doctor lint health checks. */
const RUNTIME_TOOL_SCHEMA_CHECK_ID = "core/doctor/runtime-tool-schemas";
const AUTH_PROFILE_CHECK_ID = "core/doctor/auth-profiles";
var DoctorLintStateSnapshotError = class extends Error {
constructor(cause) {
super(`Doctor lint could not prepare a private plugin-state snapshot: ${scrubDoctorErrorMessage(cause)}`, { cause });
this.name = "DoctorLintStateSnapshotError";
}
};
function detectMode(opts) {
if (opts.json === true) return "json";
return process.stdout.isTTY ? "human" : "json";
}
/**
* Runs registered doctor health checks in human or JSON mode and returns the lint exit code.
*
* Invalid config is reported before regular health checks because most checks need a parsed config
* and workspace root.
*/
async function runDoctorLintCli(runtime, opts) {
const execution = await prepareDoctorLintExecution(runtime, opts);
execution.writeOutput();
return execution.exitCode;
}
/** Collect advisory doctor findings without writing output or repairing operator state. */
async function collectDoctorFindings(runtime) {
return (await prepareDoctorLintExecution(runtime, { severityMin: "info" })).findings;
}
async function prepareDoctorLintExecution(runtime, opts) {
const sevMin = opts.severityMin === void 0 ? "warning" : parseHealthFindingSeverity(opts.severityMin);
if (sevMin === null) throw new Error("Invalid --severity-min value. Expected one of: info, warning, error.");
maybeLoadDotEnvForConfig(process.env);
const sourceEnv = { ...process.env };
const updateReadiness = isPostCoreConvergencePass(sourceEnv) ? "post-plugin" : void 0;
const effectiveOpts = updateReadiness ? {
...opts,
updateReadiness
} : opts;
const pluginStateMode = resolveBundledHealthCheckPluginStateMode(effectiveOpts);
const stateView = {
pluginMetadataEnv: sourceEnv,
sourceEnv,
readConfigSnapshot: () => pluginStateMode === "direct" ? readConfigFileSnapshot({ observe: false }) : createConfigIO({
env: sourceEnv,
configPath: resolveConfigPath(sourceEnv, resolveStateDir(sourceEnv)),
observe: false,
pluginValidation: pluginStateMode === "deferred" ? "core-only" : void 0
}).readConfigFileSnapshot(),
runWithPluginStateSnapshot: async (run) => withReadOnlyPluginStateSnapshot(sourceEnv, run)
};
if (pluginStateMode !== "isolated") return await executeDoctorLint(runtime, effectiveOpts, sevMin, stateView);
try {
return await withReadOnlyPluginStateSnapshot(sourceEnv, async (pluginMetadataEnv) => executeDoctorLint(runtime, effectiveOpts, sevMin, {
...stateView,
pluginMetadataEnv,
runWithPluginStateSnapshot: async (run) => run(pluginMetadataEnv)
}));
} catch (error) {
if (!(error instanceof DoctorLintStateSnapshotError)) throw error;
return createStateSnapshotFailureExecution(runtime, effectiveOpts, sevMin, error);
}
}
async function executeDoctorLint(runtime, opts, sevMin, stateView) {
const snapshot = await stateView.readConfigSnapshot();
if (snapshot.exists && !snapshot.valid) {
const findings = configValidationIssuesToHealthFindings(snapshot.issues);
const visible = findings.filter((finding) => healthFindingMeetsSeverity(finding, sevMin));
return {
exitCode: exitCodeFromFindings(findings, sevMin),
findings: visible,
writeOutput() {
if (detectMode(opts) === "json") {
writeJsonResult({
ok: false,
checksRun: 1,
checksSkipped: 0,
findings: visible
});
return;
}
runtime.error("doctor --lint: config file exists but does not parse cleanly.");
for (const issue of snapshot.issues) {
const issuePath = issue.path || "<root>";
runtime.error(`- ${issuePath}: ${issue.message}`);
}
}
};
}
const sourceEnv = { ...stateView.sourceEnv };
const defaultAgentId = tryResolveDefaultAgentId(snapshot.config);
const ctx = {
mode: "lint",
runtime,
cfg: snapshot.config,
cwd: defaultAgentId ? resolveAgentWorkspaceDir(snapshot.config, defaultAgentId) : process.cwd(),
env: sourceEnv,
allowExecSecretRefs: opts.allowExec === true,
...snapshot.path !== void 0 ? { configPath: snapshot.path } : {}
};
registerBundledHealthChecks({
cfg: snapshot.config,
cwd: ctx.cwd,
env: stateView.pluginMetadataEnv,
runWithPluginStateSnapshot: stateView.runWithPluginStateSnapshot,
updateReadiness: opts.updateReadiness
});
const registeredExtensionChecks = listExtensionHealthChecksForDoctor([]);
const onlyRegisteredExtensionChecks = opts.onlyIds !== void 0 && opts.onlyIds.length > 0 && opts.onlyIds.every((id) => registeredExtensionChecks.some((check) => check.id === id));
const coreChecks = onlyRegisteredExtensionChecks ? [] : await resolveDoctorContributionHealthChecks();
const extensionChecks = onlyRegisteredExtensionChecks ? registeredExtensionChecks : listExtensionHealthChecksForDoctor(coreChecks);
const runWithPrivateStateSnapshot = async (run) => await stateView.runWithPluginStateSnapshot(async () => await run());
const runWithSourceState = async (run) => opts.updateReadiness ? run() : withDoctorLintStateEnv(sourceEnv, run);
const coreCtx = {
...ctx,
deep: opts.deep === true,
runWithPrivateStateSnapshot,
runWithSourceState
};
const checks = [...coreChecks.map((check) => withCoreLintContext(check, coreCtx)), ...extensionChecks];
const runOpts = {
checks: opts.updateReadiness ? selectUpdateReadinessChecks(checks, opts.updateReadiness) : checks,
includeAllChecks: opts.updateReadiness !== void 0 || opts.includeAllChecks === true,
...opts.skipIds && opts.skipIds.length > 0 ? { skipIds: opts.skipIds } : {},
...opts.onlyIds && opts.onlyIds.length > 0 ? { onlyIds: opts.onlyIds } : {}
};
const result = await runDoctorLintChecks(ctx, runOpts);
const visible = result.findings.filter((finding) => healthFindingMeetsSeverity(finding, sevMin));
const exitCode = exitCodeFromFindings(result.findings, sevMin);
return {
exitCode,
findings: visible,
writeOutput() {
if (detectMode(opts) === "json") {
writeJsonResult({
ok: exitCode === 0,
checksRun: result.checksRun,
checksSkipped: result.checksSkipped,
findings: visible
});
return;
}
process.stdout.write(`doctor --lint: ran ${result.checksRun} check(s), ${visible.length} finding(s)\n`);
if (visible.length === 0) {
process.stdout.write(" no findings\n");
return;
}
for (const f of visible) {
const where = f.path !== void 0 ? ` ${f.path}` : "";
const line = f.line !== void 0 ? `:${f.line}` : "";
process.stdout.write(` [${f.severity}] ${f.checkId}${where}${line} - ${f.message}\n`);
if (f.fixHint !== void 0) process.stdout.write(` fix: ${f.fixHint}\n`);
}
}
};
}
async function withReadOnlyPluginStateSnapshot(sourceEnv, run) {
const sourceDatabasePath = resolveOpenClawStateSqlitePath(sourceEnv);
let cleanup;
let privateRoot;
let prepared;
try {
if (fs.existsSync(sourceDatabasePath)) {
prepared = prepareSqliteReadOnlyLocationSync(sourceDatabasePath);
privateRoot = path.dirname(prepared.location);
cleanup = prepared.cleanup;
} else {
privateRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-lint-state-"));
cleanup = () => {
try {
fs.rmSync(privateRoot, {
force: true,
recursive: true
});
return true;
} catch {
return false;
}
};
}
} catch (error) {
throw new DoctorLintStateSnapshotError(error);
}
let outcome;
let runStarted = false;
try {
const privateStateDir = path.join(privateRoot, "openclaw-state");
const privateDatabasePath = resolveOpenClawStateSqlitePath({
...sourceEnv,
OPENCLAW_STATE_DIR: privateStateDir
});
fs.mkdirSync(path.dirname(privateDatabasePath), {
recursive: true,
mode: 448
});
if (prepared) for (const suffix of [
"",
"-journal",
"-shm",
"-wal"
]) {
const sourcePath = `${prepared.location}${suffix}`;
if (fs.existsSync(sourcePath)) fs.renameSync(sourcePath, `${privateDatabasePath}${suffix}`);
}
const sourceConfigPath = resolveConfigPath(sourceEnv, resolveStateDir(sourceEnv));
const privateEnv = {
...sourceEnv,
OPENCLAW_CONFIG_PATH: sourceConfigPath,
OPENCLAW_STATE_DIR: privateStateDir
};
const installRoots = resolvePluginInstallRoots(sourceEnv);
outcome = {
ok: true,
value: await withDoctorLintStateEnv(privateEnv, () => withPluginInstallRoots({
...installRoots,
stateDir: privateStateDir
}, async () => {
runStarted = true;
return await run(privateEnv);
}))
};
} catch (error) {
outcome = {
ok: false,
error
};
}
if (!cleanup()) throw new DoctorLintStateSnapshotError(/* @__PURE__ */ new Error("Temporary doctor lint state snapshot cleanup did not complete."));
if (!outcome.ok) throw runStarted ? outcome.error : new DoctorLintStateSnapshotError(outcome.error);
return outcome.value;
}
async function withDoctorLintStateEnv(env, run) {
const stateDir = resolveStateDir(env);
const overrides = {
OPENCLAW_CONFIG_PATH: resolveConfigPath(env, stateDir),
OPENCLAW_STATE_DIR: stateDir
};
const previous = Object.keys(overrides).map((key) => [key, process.env[key]]);
Object.assign(process.env, overrides);
try {
return await run();
} finally {
for (const [key, value] of previous) if (value === void 0) delete process.env[key];
else process.env[key] = value;
}
}
function createStateSnapshotFailureExecution(runtime, opts, sevMin, error) {
const finding = {
checkId: "core/doctor/lint-state-inspection",
severity: "error",
source: "doctor",
target: "plugin-state",
requirement: "read-only-plugin-state-inspection",
message: `Doctor lint could not inspect plugin state without mutating the live state database (${scrubDoctorErrorMessage(error.cause ?? error)}).`,
fixHint: "Keep the current Gateway running, resolve the state database inspection error, then rerun this check."
};
const visible = healthFindingMeetsSeverity(finding, sevMin) ? [finding] : [];
return {
exitCode: exitCodeFromFindings([finding], sevMin),
findings: visible,
writeOutput() {
if (detectMode(opts) === "json") {
writeJsonResult({
ok: false,
checksRun: 0,
checksSkipped: 0,
findings: visible
});
return;
}
runtime.error(`doctor --lint: ${finding.message}`);
runtime.error(`fix: ${finding.fixHint}`);
}
};
}
function withCoreLintContext(check, ctx) {
return {
...check,
detect(_ctx, scope) {
const detect = async () => await check.detect(ctx, scope);
if (check.id === RUNTIME_TOOL_SCHEMA_CHECK_ID) return ctx.runWithPrivateStateSnapshot(detect);
return check.id === AUTH_PROFILE_CHECK_ID ? ctx.runWithSourceState(detect) : detect();
}
};
}
function writeJsonResult(result) {
process.stdout.write(JSON.stringify({
ok: result.ok,
checksRun: result.checksRun,
checksSkipped: result.checksSkipped,
findings: result.findings.map(toJsonFinding)
}) + "\n");
}
function toJsonFinding(f) {
return {
checkId: f.checkId,
severity: f.severity,
message: f.message,
...f.source !== void 0 ? { source: f.source } : {},
...f.path !== void 0 ? { path: f.path } : {},
...f.line !== void 0 ? { line: f.line } : {},
...f.column !== void 0 ? { column: f.column } : {},
...f.ocPath !== void 0 ? { ocPath: f.ocPath } : {},
...f.target !== void 0 ? { target: f.target } : {},
...f.requirement !== void 0 ? { requirement: f.requirement } : {},
...f.fixHint !== void 0 ? { fixHint: f.fixHint } : {}
};
}
//#endregion
export { collectDoctorFindings, runDoctorLintCli };