143 lines
4.8 KiB
TypeScript
143 lines
4.8 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { resolveClawdbotPackageRoot } from "../infra/clawdbot-root.js";
|
|
import { runCommandWithTimeout } from "../process/exec.js";
|
|
import type { RuntimeEnv } from "../runtime.js";
|
|
import { note } from "../terminal/note.js";
|
|
import type { DoctorPrompter } from "./doctor-prompter.js";
|
|
|
|
export async function maybeRepairUiProtocolFreshness(
|
|
_runtime: RuntimeEnv,
|
|
prompter: DoctorPrompter,
|
|
) {
|
|
const root = await resolveClawdbotPackageRoot({
|
|
moduleUrl: import.meta.url,
|
|
argv1: process.argv[1],
|
|
cwd: process.cwd(),
|
|
});
|
|
|
|
if (!root) return;
|
|
|
|
const schemaPath = path.join(root, "src/gateway/protocol/schema.ts");
|
|
const uiIndexPath = path.join(root, "dist/control-ui/index.html");
|
|
|
|
try {
|
|
const [schemaStats, uiStats] = await Promise.all([
|
|
fs.stat(schemaPath).catch(() => null),
|
|
fs.stat(uiIndexPath).catch(() => null),
|
|
]);
|
|
|
|
if (schemaStats && !uiStats) {
|
|
note(["- Control UI assets are missing.", "- Run: pnpm ui:build"].join("\n"), "UI");
|
|
|
|
// In slim/docker environments we may not have the UI source tree. Trying
|
|
// to build would fail (and spam logs), so skip the interactive repair.
|
|
const uiSourcesPath = path.join(root, "ui/package.json");
|
|
const uiSourcesExist = await fs.stat(uiSourcesPath).catch(() => null);
|
|
if (!uiSourcesExist) {
|
|
note("Skipping UI build: ui/ sources not present.", "UI");
|
|
return;
|
|
}
|
|
|
|
const shouldRepair = await prompter.confirmRepair({
|
|
message: "Build Control UI assets now?",
|
|
initialValue: true,
|
|
});
|
|
|
|
if (shouldRepair) {
|
|
note("Building Control UI assets... (this may take a moment)", "UI");
|
|
const uiScriptPath = path.join(root, "scripts/ui.js");
|
|
const buildResult = await runCommandWithTimeout([process.execPath, uiScriptPath, "build"], {
|
|
cwd: root,
|
|
timeoutMs: 120_000,
|
|
env: { ...process.env, FORCE_COLOR: "1" },
|
|
});
|
|
if (buildResult.code === 0) {
|
|
note("UI build complete.", "UI");
|
|
} else {
|
|
const details = [
|
|
`UI build failed (exit ${buildResult.code ?? "unknown"}).`,
|
|
buildResult.stderr.trim() ? buildResult.stderr.trim() : null,
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n");
|
|
note(details, "UI");
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!schemaStats || !uiStats) return;
|
|
|
|
if (schemaStats.mtime > uiStats.mtime) {
|
|
const uiMtimeIso = uiStats.mtime.toISOString();
|
|
// Find changes since the UI build
|
|
const gitLog = await runCommandWithTimeout(
|
|
[
|
|
"git",
|
|
"-C",
|
|
root,
|
|
"log",
|
|
`--since=${uiMtimeIso}`,
|
|
"--format=%h %s",
|
|
"src/gateway/protocol/schema.ts",
|
|
],
|
|
{ timeoutMs: 5000 },
|
|
).catch(() => null);
|
|
|
|
if (gitLog && gitLog.code === 0 && gitLog.stdout.trim()) {
|
|
note(
|
|
`UI assets are older than the protocol schema.\nFunctional changes since last build:\n${gitLog.stdout
|
|
.trim()
|
|
.split("\n")
|
|
.map((l) => `- ${l}`)
|
|
.join("\n")}`,
|
|
"UI Freshness",
|
|
);
|
|
|
|
const shouldRepair = await prompter.confirmAggressive({
|
|
message: "Rebuild UI now? (Detected protocol mismatch requiring update)",
|
|
initialValue: true,
|
|
});
|
|
|
|
if (shouldRepair) {
|
|
const uiSourcesPath = path.join(root, "ui/package.json");
|
|
const uiSourcesExist = await fs.stat(uiSourcesPath).catch(() => null);
|
|
if (!uiSourcesExist) {
|
|
note("Skipping UI rebuild: ui/ sources not present.", "UI");
|
|
return;
|
|
}
|
|
|
|
note("Rebuilding stale UI assets... (this may take a moment)", "UI");
|
|
// Use scripts/ui.js to build, assuming node is available as we are running in it.
|
|
// We use the same node executable to run the script.
|
|
const uiScriptPath = path.join(root, "scripts/ui.js");
|
|
const buildResult = await runCommandWithTimeout(
|
|
[process.execPath, uiScriptPath, "build"],
|
|
{
|
|
cwd: root,
|
|
timeoutMs: 120_000,
|
|
env: { ...process.env, FORCE_COLOR: "1" },
|
|
},
|
|
);
|
|
if (buildResult.code === 0) {
|
|
note("UI rebuild complete.", "UI");
|
|
} else {
|
|
const details = [
|
|
`UI rebuild failed (exit ${buildResult.code ?? "unknown"}).`,
|
|
buildResult.stderr.trim() ? buildResult.stderr.trim() : null,
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n");
|
|
note(details, "UI");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
// If files don't exist, we can't check.
|
|
// If git fails, we silently skip.
|
|
// runtime.debug(`UI freshness check failed: ${String(err)}`);
|
|
}
|
|
}
|