import fs from "node:fs/promises" ;
import path from "node:path" ;
import { readConfigFileSnapshot, recoverConfigFromJsonRootSuffix } from "../config/io.js" ;
import { formatConfigIssueLines } from "../config/issue-format.js" ;
import type { OpenClawConfig } from "../config/types.openclaw.js" ;
import { note } from "../terminal/note.js" ;
import { resolveHomeDir } from "../utils.js" ;
import { noteIncludeConfinementWarning } from "./doctor-config-analysis.js" ;
async function maybeMigrateLegacyConfig(): Promise<string[]> {
const changes: string[] = [];
const home = resolveHomeDir();
if (!home) {
return changes;
}
const targetDir = path.join(home, ".openclaw" );
const targetPath = path.join(targetDir, "openclaw.json" );
try {
await fs.access(targetPath);
return changes;
} catch {
// missing config
}
const legacyCandidates = [path.join(home, ".clawdbot" , "clawdbot.json" )];
let legacyPath: string | null = null ;
for (const candidate of legacyCandidates) {
try {
await fs.access(candidate);
legacyPath = candidate;
break ;
} catch {
// continue
}
}
if (!legacyPath) {
return changes;
}
await fs.mkdir(targetDir, { recursive: true });
try {
await fs.copyFile(legacyPath, targetPath, fs.constants.COPYFILE_EXCL);
changes.push(`Migrated legacy config: ${legacyPath} -> ${targetPath}`);
} catch {
// If it already exists, skip silently.
}
return changes;
}
export type DoctorConfigPreflightResult = {
snapshot: Awaited<ReturnType<typeof readConfigFileSnapshot>>;
baseConfig: OpenClawConfig;
};
export async function runDoctorConfigPreflight(
options: {
migrateState?: boolean ;
migrateLegacyConfig?: boolean ;
repairPrefixedConfig?: boolean ;
invalidConfigNote?: string | false ;
} = {},
): Promise<DoctorConfigPreflightResult> {
if (options.migrateState !== false ) {
const { autoMigrateLegacyStateDir } = await import ("./doctor-state-migrations.js" );
const stateDirResult = await autoMigrateLegacyStateDir({ env: process.env });
if (stateDirResult.changes.length > 0 ) {
note(stateDirResult.changes.map((entry) => `- ${entry}`).join("\n" ), "Doctor changes" );
}
if (stateDirResult.warnings.length > 0 ) {
note(stateDirResult.warnings.map((entry) => `- ${entry}`).join("\n" ), "Doctor warnings" );
}
}
if (options.migrateLegacyConfig !== false ) {
const legacyConfigChanges = await maybeMigrateLegacyConfig();
if (legacyConfigChanges.length > 0 ) {
note(legacyConfigChanges.map((entry) => `- ${entry}`).join("\n" ), "Doctor changes" );
}
}
let snapshot = await readConfigFileSnapshot();
if (
options.repairPrefixedConfig === true &&
snapshot.exists &&
!snapshot.valid &&
(await recoverConfigFromJsonRootSuffix(snapshot))
) {
note("Removed non-JSON prefix from openclaw.json; original saved as .clobbered.*." , "Config" );
snapshot = await readConfigFileSnapshot();
}
const invalidConfigNote =
options.invalidConfigNote ?? "Config invalid; doctor will run with best-effort config." ;
if (
invalidConfigNote &&
snapshot.exists &&
!snapshot.valid &&
snapshot.legacyIssues.length === 0
) {
note(invalidConfigNote, "Config" );
noteIncludeConfinementWarning(snapshot);
}
const warnings = snapshot.warnings ?? [];
if (warnings.length > 0 ) {
note(formatConfigIssueLines(warnings, "-" ).join("\n" ), "Config warnings" );
}
return {
snapshot,
baseConfig: snapshot.sourceConfig ?? snapshot.config ?? {},
};
}
Messung V0.5 in Prozent C=97 H=96 G=96
¤ Dauer der Verarbeitung: 0.12 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland