feat(doctor): add UI protocol freshness check
This commit is contained in:
committed by
Peter Steinberger
parent
93ae3b8405
commit
bfdbaa5ab6
84
src/commands/doctor-ui.ts
Normal file
84
src/commands/doctor-ui.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
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),
|
||||||
|
fs.stat(uiIndexPath),
|
||||||
|
]);
|
||||||
|
|
||||||
|
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?.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) {
|
||||||
|
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");
|
||||||
|
await runCommandWithTimeout(
|
||||||
|
[process.execPath, uiScriptPath, "build"],
|
||||||
|
{
|
||||||
|
cwd: root,
|
||||||
|
timeoutMs: 120_000,
|
||||||
|
env: { ...process.env, FORCE_COLOR: "1" },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
note("UI rebuild complete.", "UI");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_err) {
|
||||||
|
// If files don't exist, we can't check.
|
||||||
|
// If git fails, we silently skip.
|
||||||
|
// runtime.debug(`UI freshness check failed: ${String(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,6 +66,7 @@ import {
|
|||||||
maybeMigrateLegacyConfigFile,
|
maybeMigrateLegacyConfigFile,
|
||||||
normalizeLegacyConfigValues,
|
normalizeLegacyConfigValues,
|
||||||
} from "./doctor-legacy-config.js";
|
} from "./doctor-legacy-config.js";
|
||||||
|
import { maybeRepairUiProtocolFreshness } from "./doctor-ui.js";
|
||||||
import { createDoctorPrompter, type DoctorOptions } from "./doctor-prompter.js";
|
import { createDoctorPrompter, type DoctorOptions } from "./doctor-prompter.js";
|
||||||
import {
|
import {
|
||||||
maybeRepairSandboxImages,
|
maybeRepairSandboxImages,
|
||||||
@@ -271,9 +272,9 @@ export async function doctorCommand(
|
|||||||
options.nonInteractive === true
|
options.nonInteractive === true
|
||||||
? true
|
? true
|
||||||
: await prompter.confirm({
|
: await prompter.confirm({
|
||||||
message: "Migrate legacy config entries now?",
|
message: "Migrate legacy config entries now?",
|
||||||
initialValue: true,
|
initialValue: true,
|
||||||
});
|
});
|
||||||
if (migrate) {
|
if (migrate) {
|
||||||
// Legacy migration (2026-01-02, commit: 16420e5b) — normalize per-provider allowlists; move WhatsApp gating into whatsapp.allowFrom.
|
// Legacy migration (2026-01-02, commit: 16420e5b) — normalize per-provider allowlists; move WhatsApp gating into whatsapp.allowFrom.
|
||||||
const { config: migrated, changes } = migrateLegacyConfig(
|
const { config: migrated, changes } = migrateLegacyConfig(
|
||||||
@@ -326,9 +327,9 @@ export async function doctorCommand(
|
|||||||
: options.nonInteractive === true
|
: options.nonInteractive === true
|
||||||
? false
|
? false
|
||||||
: await prompter.confirmRepair({
|
: await prompter.confirmRepair({
|
||||||
message: "Generate and configure a gateway token now?",
|
message: "Generate and configure a gateway token now?",
|
||||||
initialValue: true,
|
initialValue: true,
|
||||||
});
|
});
|
||||||
if (shouldSetToken) {
|
if (shouldSetToken) {
|
||||||
const nextToken = randomToken();
|
const nextToken = randomToken();
|
||||||
cfg = {
|
cfg = {
|
||||||
@@ -354,9 +355,9 @@ export async function doctorCommand(
|
|||||||
options.nonInteractive === true
|
options.nonInteractive === true
|
||||||
? true
|
? true
|
||||||
: await prompter.confirm({
|
: await prompter.confirm({
|
||||||
message: "Migrate legacy state (sessions/agent/WhatsApp auth) now?",
|
message: "Migrate legacy state (sessions/agent/WhatsApp auth) now?",
|
||||||
initialValue: true,
|
initialValue: true,
|
||||||
});
|
});
|
||||||
if (migrate) {
|
if (migrate) {
|
||||||
const migrated = await runLegacyStateMigrations({
|
const migrated = await runLegacyStateMigrations({
|
||||||
detected: legacyState,
|
detected: legacyState,
|
||||||
@@ -478,13 +479,11 @@ export async function doctorCommand(
|
|||||||
note(
|
note(
|
||||||
[
|
[
|
||||||
`Eligible: ${skillsReport.skills.filter((s) => s.eligible).length}`,
|
`Eligible: ${skillsReport.skills.filter((s) => s.eligible).length}`,
|
||||||
`Missing requirements: ${
|
`Missing requirements: ${skillsReport.skills.filter(
|
||||||
skillsReport.skills.filter(
|
(s) => !s.eligible && !s.disabled && !s.blockedByAllowlist,
|
||||||
(s) => !s.eligible && !s.disabled && !s.blockedByAllowlist,
|
).length
|
||||||
).length
|
|
||||||
}`,
|
}`,
|
||||||
`Blocked by allowlist: ${
|
`Blocked by allowlist: ${skillsReport.skills.filter((s) => s.blockedByAllowlist).length
|
||||||
skillsReport.skills.filter((s) => s.blockedByAllowlist).length
|
|
||||||
}`,
|
}`,
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
"Skills status",
|
"Skills status",
|
||||||
@@ -494,10 +493,10 @@ export async function doctorCommand(
|
|||||||
config: cfg,
|
config: cfg,
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
logger: {
|
logger: {
|
||||||
info: () => {},
|
info: () => { },
|
||||||
warn: () => {},
|
warn: () => { },
|
||||||
error: () => {},
|
error: () => { },
|
||||||
debug: () => {},
|
debug: () => { },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (pluginRegistry.plugins.length > 0) {
|
if (pluginRegistry.plugins.length > 0) {
|
||||||
@@ -513,9 +512,9 @@ export async function doctorCommand(
|
|||||||
`Errors: ${errored.length}`,
|
`Errors: ${errored.length}`,
|
||||||
errored.length > 0
|
errored.length > 0
|
||||||
? `- ${errored
|
? `- ${errored
|
||||||
.slice(0, 10)
|
.slice(0, 10)
|
||||||
.map((p) => p.id)
|
.map((p) => p.id)
|
||||||
.join("\n- ")}${errored.length > 10 ? "\n- ..." : ""}`
|
.join("\n- ")}${errored.length > 10 ? "\n- ..." : ""}`
|
||||||
: null,
|
: null,
|
||||||
].filter((line): line is string => Boolean(line));
|
].filter((line): line is string => Boolean(line));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user