mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 05:31:23 +00:00
fix: migrate legacy state/config paths
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { ZodIssue } from "zod";
|
||||
|
||||
import type { MoltbotConfig } from "../config/config.js";
|
||||
@@ -12,6 +14,7 @@ import { formatCliCommand } from "../cli/command-format.js";
|
||||
import { note } from "../terminal/note.js";
|
||||
import { normalizeLegacyConfigValues } from "./doctor-legacy-config.js";
|
||||
import type { DoctorOptions } from "./doctor-prompter.js";
|
||||
import { autoMigrateLegacyStateDir } from "./doctor-state-migrations.js";
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
@@ -117,12 +120,50 @@ function noteOpencodeProviderOverrides(cfg: MoltbotConfig) {
|
||||
note(lines.join("\n"), "OpenCode Zen");
|
||||
}
|
||||
|
||||
function hasExplicitConfigPath(env: NodeJS.ProcessEnv): boolean {
|
||||
return Boolean(env.MOLTBOT_CONFIG_PATH?.trim() || env.CLAWDBOT_CONFIG_PATH?.trim());
|
||||
}
|
||||
|
||||
function moveLegacyConfigFile(legacyPath: string, canonicalPath: string) {
|
||||
fs.mkdirSync(path.dirname(canonicalPath), { recursive: true, mode: 0o700 });
|
||||
try {
|
||||
fs.renameSync(legacyPath, canonicalPath);
|
||||
} catch (err) {
|
||||
fs.copyFileSync(legacyPath, canonicalPath);
|
||||
fs.chmodSync(canonicalPath, 0o600);
|
||||
try {
|
||||
fs.unlinkSync(legacyPath);
|
||||
} catch {
|
||||
// Best-effort cleanup; we'll warn later if both files exist.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadAndMaybeMigrateDoctorConfig(params: {
|
||||
options: DoctorOptions;
|
||||
confirm: (p: { message: string; initialValue: boolean }) => Promise<boolean>;
|
||||
}) {
|
||||
const shouldRepair = params.options.repair === true || params.options.yes === true;
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
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");
|
||||
}
|
||||
|
||||
let snapshot = await readConfigFileSnapshot();
|
||||
if (!hasExplicitConfigPath(process.env) && snapshot.exists) {
|
||||
const basename = path.basename(snapshot.path);
|
||||
if (basename === "clawdbot.json") {
|
||||
const canonicalPath = path.join(path.dirname(snapshot.path), "moltbot.json");
|
||||
if (!fs.existsSync(canonicalPath)) {
|
||||
moveLegacyConfigFile(snapshot.path, canonicalPath);
|
||||
note(`- Config: ${snapshot.path} → ${canonicalPath}`, "Doctor changes");
|
||||
snapshot = await readConfigFileSnapshot();
|
||||
}
|
||||
}
|
||||
}
|
||||
const baseCfg = snapshot.config ?? {};
|
||||
let cfg: MoltbotConfig = baseCfg;
|
||||
let candidate = structuredClone(baseCfg) as MoltbotConfig;
|
||||
|
||||
@@ -6,8 +6,10 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { MoltbotConfig } from "../config/config.js";
|
||||
import {
|
||||
autoMigrateLegacyStateDir,
|
||||
autoMigrateLegacyState,
|
||||
detectLegacyStateMigrations,
|
||||
resetAutoMigrateLegacyStateDirForTest,
|
||||
resetAutoMigrateLegacyStateForTest,
|
||||
runLegacyStateMigrations,
|
||||
} from "./doctor-state-migrations.js";
|
||||
@@ -22,6 +24,7 @@ async function makeTempRoot() {
|
||||
|
||||
afterEach(async () => {
|
||||
resetAutoMigrateLegacyStateForTest();
|
||||
resetAutoMigrateLegacyStateDirForTest();
|
||||
if (!tempRoot) return;
|
||||
await fs.promises.rm(tempRoot, { recursive: true, force: true });
|
||||
tempRoot = null;
|
||||
@@ -323,4 +326,53 @@ describe("doctor legacy state migrations", () => {
|
||||
expect(store["main"]).toBeUndefined();
|
||||
expect(store["agent:main:main"]?.sessionId).toBe("legacy");
|
||||
});
|
||||
|
||||
it("auto-migrates legacy state dir to ~/.moltbot", async () => {
|
||||
const root = await makeTempRoot();
|
||||
const legacyDir = path.join(root, ".clawdbot");
|
||||
fs.mkdirSync(legacyDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(legacyDir, "foo.txt"), "legacy", "utf-8");
|
||||
|
||||
const result = await autoMigrateLegacyStateDir({
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
homedir: () => root,
|
||||
});
|
||||
|
||||
const targetDir = path.join(root, ".moltbot");
|
||||
expect(fs.existsSync(path.join(targetDir, "foo.txt"))).toBe(true);
|
||||
const legacyStat = fs.lstatSync(legacyDir);
|
||||
expect(legacyStat.isSymbolicLink()).toBe(true);
|
||||
expect(fs.realpathSync(legacyDir)).toBe(fs.realpathSync(targetDir));
|
||||
expect(result.migrated).toBe(true);
|
||||
});
|
||||
|
||||
it("skips state dir migration when target exists", async () => {
|
||||
const root = await makeTempRoot();
|
||||
const legacyDir = path.join(root, ".clawdbot");
|
||||
const targetDir = path.join(root, ".moltbot");
|
||||
fs.mkdirSync(legacyDir, { recursive: true });
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
|
||||
const result = await autoMigrateLegacyStateDir({
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
homedir: () => root,
|
||||
});
|
||||
|
||||
expect(result.migrated).toBe(false);
|
||||
expect(result.warnings.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("skips state dir migration when env override is set", async () => {
|
||||
const root = await makeTempRoot();
|
||||
const legacyDir = path.join(root, ".clawdbot");
|
||||
fs.mkdirSync(legacyDir, { recursive: true });
|
||||
|
||||
const result = await autoMigrateLegacyStateDir({
|
||||
env: { MOLTBOT_STATE_DIR: "/custom/state" } as NodeJS.ProcessEnv,
|
||||
homedir: () => root,
|
||||
});
|
||||
|
||||
expect(result.skipped).toBe(true);
|
||||
expect(result.migrated).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
export type { LegacyStateDetection } from "../infra/state-migrations.js";
|
||||
export {
|
||||
autoMigrateLegacyStateDir,
|
||||
autoMigrateLegacyAgentDir,
|
||||
autoMigrateLegacyState,
|
||||
detectLegacyStateMigrations,
|
||||
migrateLegacyAgentDir,
|
||||
resetAutoMigrateLegacyStateDirForTest,
|
||||
resetAutoMigrateLegacyAgentDirForTest,
|
||||
resetAutoMigrateLegacyStateForTest,
|
||||
runLegacyStateMigrations,
|
||||
|
||||
@@ -292,6 +292,12 @@ vi.mock("./onboard-helpers.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./doctor-state-migrations.js", () => ({
|
||||
autoMigrateLegacyStateDir: vi.fn().mockResolvedValue({
|
||||
migrated: false,
|
||||
skipped: false,
|
||||
changes: [],
|
||||
warnings: [],
|
||||
}),
|
||||
detectLegacyStateMigrations: vi.fn().mockResolvedValue({
|
||||
targetAgentId: "main",
|
||||
targetMainKey: "main",
|
||||
|
||||
@@ -291,6 +291,12 @@ vi.mock("./onboard-helpers.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./doctor-state-migrations.js", () => ({
|
||||
autoMigrateLegacyStateDir: vi.fn().mockResolvedValue({
|
||||
migrated: false,
|
||||
skipped: false,
|
||||
changes: [],
|
||||
warnings: [],
|
||||
}),
|
||||
detectLegacyStateMigrations: vi.fn().mockResolvedValue({
|
||||
targetAgentId: "main",
|
||||
targetMainKey: "main",
|
||||
|
||||
@@ -291,6 +291,12 @@ vi.mock("./onboard-helpers.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./doctor-state-migrations.js", () => ({
|
||||
autoMigrateLegacyStateDir: vi.fn().mockResolvedValue({
|
||||
migrated: false,
|
||||
skipped: false,
|
||||
changes: [],
|
||||
warnings: [],
|
||||
}),
|
||||
detectLegacyStateMigrations: vi.fn().mockResolvedValue({
|
||||
targetAgentId: "main",
|
||||
targetMainKey: "main",
|
||||
|
||||
@@ -291,6 +291,12 @@ vi.mock("./onboard-helpers.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./doctor-state-migrations.js", () => ({
|
||||
autoMigrateLegacyStateDir: vi.fn().mockResolvedValue({
|
||||
migrated: false,
|
||||
skipped: false,
|
||||
changes: [],
|
||||
warnings: [],
|
||||
}),
|
||||
detectLegacyStateMigrations: vi.fn().mockResolvedValue({
|
||||
targetAgentId: "main",
|
||||
targetMainKey: "main",
|
||||
|
||||
@@ -291,6 +291,12 @@ vi.mock("./onboard-helpers.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./doctor-state-migrations.js", () => ({
|
||||
autoMigrateLegacyStateDir: vi.fn().mockResolvedValue({
|
||||
migrated: false,
|
||||
skipped: false,
|
||||
changes: [],
|
||||
warnings: [],
|
||||
}),
|
||||
detectLegacyStateMigrations: vi.fn().mockResolvedValue({
|
||||
targetAgentId: "main",
|
||||
targetMainKey: "main",
|
||||
|
||||
@@ -3,19 +3,19 @@ import fs from "node:fs/promises";
|
||||
import JSON5 from "json5";
|
||||
|
||||
import { DEFAULT_AGENT_WORKSPACE_DIR, ensureAgentWorkspace } from "../agents/workspace.js";
|
||||
import { type MoltbotConfig, CONFIG_PATH, writeConfigFile } from "../config/config.js";
|
||||
import { type MoltbotConfig, createConfigIO, writeConfigFile } from "../config/config.js";
|
||||
import { formatConfigPath, logConfigUpdated } from "../config/logging.js";
|
||||
import { resolveSessionTranscriptsDir } from "../config/sessions.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { shortenHomePath } from "../utils.js";
|
||||
|
||||
async function readConfigFileRaw(): Promise<{
|
||||
async function readConfigFileRaw(configPath: string): Promise<{
|
||||
exists: boolean;
|
||||
parsed: MoltbotConfig;
|
||||
}> {
|
||||
try {
|
||||
const raw = await fs.readFile(CONFIG_PATH, "utf-8");
|
||||
const raw = await fs.readFile(configPath, "utf-8");
|
||||
const parsed = JSON5.parse(raw);
|
||||
if (parsed && typeof parsed === "object") {
|
||||
return { exists: true, parsed: parsed as MoltbotConfig };
|
||||
@@ -35,7 +35,9 @@ export async function setupCommand(
|
||||
? opts.workspace.trim()
|
||||
: undefined;
|
||||
|
||||
const existingRaw = await readConfigFileRaw();
|
||||
const io = createConfigIO();
|
||||
const configPath = io.configPath;
|
||||
const existingRaw = await readConfigFileRaw(configPath);
|
||||
const cfg = existingRaw.parsed;
|
||||
const defaults = cfg.agents?.defaults ?? {};
|
||||
|
||||
@@ -55,12 +57,12 @@ export async function setupCommand(
|
||||
if (!existingRaw.exists || defaults.workspace !== workspace) {
|
||||
await writeConfigFile(next);
|
||||
if (!existingRaw.exists) {
|
||||
runtime.log(`Wrote ${formatConfigPath()}`);
|
||||
runtime.log(`Wrote ${formatConfigPath(configPath)}`);
|
||||
} else {
|
||||
logConfigUpdated(runtime, { suffix: "(set agents.defaults.workspace)" });
|
||||
logConfigUpdated(runtime, { path: configPath, suffix: "(set agents.defaults.workspace)" });
|
||||
}
|
||||
} else {
|
||||
runtime.log(`Config OK: ${formatConfigPath()}`);
|
||||
runtime.log(`Config OK: ${formatConfigPath(configPath)}`);
|
||||
}
|
||||
|
||||
const ws = await ensureAgentWorkspace({
|
||||
|
||||
Reference in New Issue
Block a user