openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
152 lines (151 loc) • 6.05 kB
JavaScript
import { t as resolveOpenClawPackageRoot } from "./openclaw-root-CNp1Ofdk.js";
import { r as runCommandWithTimeout } from "./exec-DubsSJS2.js";
import { t as note } from "./note-BRSJp0UF.js";
import { a as resolveControlUiDistIndexPathForRoot, r as resolveControlUiDistIndexHealth } from "./control-ui-assets-OWiFO3R_.js";
import path from "node:path";
import fs from "node:fs/promises";
//#region src/commands/doctor-ui.ts
/** Doctor checks and repairs for Control UI assets after gateway protocol changes. */
/** Detects missing or stale Control UI build artifacts relative to protocol schema changes. */
async function detectUiProtocolFreshnessIssues(opts = {}) {
const root = opts.root ?? await resolveOpenClawPackageRoot({
moduleUrl: import.meta.url,
argv1: opts.argv1 ?? process.argv[1],
cwd: opts.cwd ?? process.cwd()
});
if (!root) return [];
const schemaPath = path.join(root, "packages/gateway-protocol/src/schema.ts");
const uiIndexPath = (await resolveControlUiDistIndexHealth({
root,
argv1: opts.argv1 ?? process.argv[1]
})).indexPath ?? resolveControlUiDistIndexPathForRoot(root);
const uiSourcesPath = path.join(root, "ui/package.json");
try {
const [schemaStats, uiStats, uiSourcesStats] = await Promise.all([
fs.stat(schemaPath).catch(() => null),
fs.stat(uiIndexPath).catch(() => null),
fs.stat(uiSourcesPath).catch(() => null)
]);
if (!schemaStats) return [];
const canBuild = uiSourcesStats !== null;
if (!uiStats) return [{
kind: "missing-assets",
root,
uiIndexPath,
canBuild
}];
if (schemaStats.mtime <= uiStats.mtime) return [];
const changesSinceBuild = await (opts.collectChangesSinceBuild ?? collectProtocolSchemaChangesSince)(root, uiStats.mtime);
if (changesSinceBuild === null || changesSinceBuild.length === 0) return [];
return [{
kind: "stale-assets",
root,
uiIndexPath,
changesSinceBuild,
canBuild
}];
} catch {
return [];
}
}
async function collectProtocolSchemaChangesSince(root, uiMtime) {
const gitLog = await runCommandWithTimeout([
"git",
"-C",
root,
"log",
`--since=${uiMtime.toISOString()}`,
"--format=%h %s",
"packages/gateway-protocol/src/schema.ts"
], { timeoutMs: 5e3 }).catch(() => null);
if (!gitLog || gitLog.code !== 0) return null;
if (!gitLog.stdout.trim()) return [];
return gitLog.stdout.trim().split("\n");
}
/** Converts a UI protocol freshness issue into a doctor lint health finding. */
function uiProtocolFreshnessIssueToHealthFinding(issue) {
return {
checkId: "core/doctor/ui-protocol-freshness",
severity: "warning",
message: formatUiProtocolFreshnessIssue(issue),
path: issue.uiIndexPath,
fixHint: issue.canBuild ? issue.kind === "missing-assets" ? "Run `openclaw doctor --fix` to build Control UI assets." : "Run `openclaw doctor --fix --force` to rebuild Control UI assets, or run `pnpm ui:build`." : "Install from a source checkout with ui/ sources, then run `pnpm ui:build`."
};
}
/** Converts a UI freshness issue into the process repair effect used by lint dry runs. */
function uiProtocolFreshnessIssueToRepairEffects(issue) {
if (!issue.canBuild) return [];
return [{
kind: "process",
action: issue.kind === "missing-assets" ? "would-build-control-ui" : "would-rebuild-control-ui",
target: issue.root,
dryRunSafe: false
}];
}
function formatUiProtocolFreshnessIssue(issue) {
if (issue.kind === "missing-assets") return ["- Control UI assets are missing.", "- Run: pnpm ui:build"].join("\n");
if (issue.changesSinceBuild.length === 0) return "UI assets are older than the protocol schema.";
return `UI assets are older than the protocol schema.\nFunctional changes since last build:\n${issue.changesSinceBuild.map((line) => `- ${line}`).join("\n")}`;
}
/** Prompts to build or rebuild Control UI assets when doctor detects missing or stale output. */
async function maybeRepairUiProtocolFreshness(_runtime, prompter) {
for (const issue of await detectUiProtocolFreshnessIssues()) {
if (issue.kind === "missing-assets") {
note(formatUiProtocolFreshnessIssue(issue), "UI");
if (!issue.canBuild) {
note("Skipping UI build: ui/ sources not present.", "UI");
continue;
}
if (await prompter.confirmAutoFix({
message: "Build Control UI assets now?",
initialValue: true
})) {
note("Building Control UI assets... (this may take a moment)", "UI");
const uiScriptPath = path.join(issue.root, "scripts/ui.js");
const buildResult = await runCommandWithTimeout([
process.execPath,
uiScriptPath,
"build"
], {
cwd: issue.root,
timeoutMs: 12e4,
env: {
...process.env,
FORCE_COLOR: "1"
}
});
if (buildResult.code === 0) note("UI build complete.", "UI");
else note([`UI build failed (exit ${buildResult.code ?? "unknown"}).`, buildResult.stderr.trim() ? buildResult.stderr.trim() : null].filter(Boolean).join("\n"), "UI");
}
continue;
}
note(formatUiProtocolFreshnessIssue(issue), "UI Freshness");
if (!issue.canBuild) {
note("Skipping UI rebuild: ui/ sources not present.", "UI");
continue;
}
if (await prompter.confirmAggressiveAutoFix({
message: "Rebuild UI now? (Detected protocol mismatch requiring update)",
initialValue: true
})) {
note("Rebuilding stale UI assets... (this may take a moment)", "UI");
const uiScriptPath = path.join(issue.root, "scripts/ui.js");
const buildResult = await runCommandWithTimeout([
process.execPath,
uiScriptPath,
"build"
], {
cwd: issue.root,
timeoutMs: 12e4,
env: {
...process.env,
FORCE_COLOR: "1"
}
});
if (buildResult.code === 0) note("UI rebuild complete.", "UI");
else note([`UI rebuild failed (exit ${buildResult.code ?? "unknown"}).`, buildResult.stderr.trim() ? buildResult.stderr.trim() : null].filter(Boolean).join("\n"), "UI");
}
}
}
//#endregion
export { uiProtocolFreshnessIssueToRepairEffects as i, maybeRepairUiProtocolFreshness as n, uiProtocolFreshnessIssueToHealthFinding as r, detectUiProtocolFreshnessIssues as t };