fix(doctor): add headless flags + auto-migrate sessions
This commit is contained in:
@@ -346,8 +346,46 @@ async function runSandboxScript(
|
||||
|
||||
type DoctorOptions = {
|
||||
workspaceSuggestions?: boolean;
|
||||
yes?: boolean;
|
||||
nonInteractive?: boolean;
|
||||
};
|
||||
|
||||
type DoctorPrompter = {
|
||||
confirm: (params: Parameters<typeof confirm>[0]) => Promise<boolean>;
|
||||
confirmSkipInNonInteractive: (
|
||||
params: Parameters<typeof confirm>[0],
|
||||
) => Promise<boolean>;
|
||||
select: <T>(params: Parameters<typeof select>[0], fallback: T) => Promise<T>;
|
||||
};
|
||||
|
||||
function createDoctorPrompter(params: {
|
||||
runtime: RuntimeEnv;
|
||||
options: DoctorOptions;
|
||||
}): DoctorPrompter {
|
||||
const yes = params.options.yes === true;
|
||||
const requestedNonInteractive = params.options.nonInteractive === true;
|
||||
const isTty = Boolean(process.stdin.isTTY);
|
||||
const nonInteractive = requestedNonInteractive || (!isTty && !yes);
|
||||
|
||||
const canPrompt = isTty && !yes && !nonInteractive;
|
||||
const confirmDefault = async (p: Parameters<typeof confirm>[0]) => {
|
||||
if (!canPrompt) return Boolean(p.initialValue ?? false);
|
||||
return guardCancel(await confirm(p), params.runtime) === true;
|
||||
};
|
||||
|
||||
return {
|
||||
confirm: confirmDefault,
|
||||
confirmSkipInNonInteractive: async (p) => {
|
||||
if (nonInteractive) return false;
|
||||
return confirmDefault(p);
|
||||
},
|
||||
select: async <T>(p: Parameters<typeof select>[0], fallback: T) => {
|
||||
if (!canPrompt) return fallback;
|
||||
return guardCancel(await select(p), params.runtime) as T;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const MEMORY_SYSTEM_PROMPT = [
|
||||
"Memory system not found in workspace.",
|
||||
"Paste this into your agent:",
|
||||
@@ -463,6 +501,7 @@ type SandboxImageCheck = {
|
||||
async function handleMissingSandboxImage(
|
||||
params: SandboxImageCheck,
|
||||
runtime: RuntimeEnv,
|
||||
prompter: DoctorPrompter,
|
||||
) {
|
||||
const exists = await dockerImageExists(params.image);
|
||||
if (exists) return;
|
||||
@@ -477,13 +516,10 @@ async function handleMissingSandboxImage(
|
||||
|
||||
let built = false;
|
||||
if (params.buildScript) {
|
||||
const build = guardCancel(
|
||||
await confirm({
|
||||
message: `Build ${params.label} sandbox image now?`,
|
||||
initialValue: true,
|
||||
}),
|
||||
runtime,
|
||||
);
|
||||
const build = await prompter.confirmSkipInNonInteractive({
|
||||
message: `Build ${params.label} sandbox image now?`,
|
||||
initialValue: true,
|
||||
});
|
||||
if (build) {
|
||||
built = await runSandboxScript(params.buildScript, runtime);
|
||||
}
|
||||
@@ -496,13 +532,10 @@ async function handleMissingSandboxImage(
|
||||
const legacyExists = await dockerImageExists(legacyImage);
|
||||
if (!legacyExists) return;
|
||||
|
||||
const fallback = guardCancel(
|
||||
await confirm({
|
||||
message: `Switch config to legacy image ${legacyImage}?`,
|
||||
initialValue: false,
|
||||
}),
|
||||
runtime,
|
||||
);
|
||||
const fallback = await prompter.confirmSkipInNonInteractive({
|
||||
message: `Switch config to legacy image ${legacyImage}?`,
|
||||
initialValue: false,
|
||||
});
|
||||
if (!fallback) return;
|
||||
|
||||
params.updateConfig(legacyImage);
|
||||
@@ -511,6 +544,7 @@ async function handleMissingSandboxImage(
|
||||
async function maybeRepairSandboxImages(
|
||||
cfg: ClawdbotConfig,
|
||||
runtime: RuntimeEnv,
|
||||
prompter: DoctorPrompter,
|
||||
): Promise<ClawdbotConfig> {
|
||||
const sandbox = cfg.agent?.sandbox;
|
||||
const mode = sandbox?.mode ?? "off";
|
||||
@@ -542,6 +576,7 @@ async function maybeRepairSandboxImages(
|
||||
},
|
||||
},
|
||||
runtime,
|
||||
prompter,
|
||||
);
|
||||
|
||||
if (sandbox.browser?.enabled) {
|
||||
@@ -556,6 +591,7 @@ async function maybeRepairSandboxImages(
|
||||
},
|
||||
},
|
||||
runtime,
|
||||
prompter,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -717,6 +753,7 @@ async function maybeMigrateLegacyConfigFile(runtime: RuntimeEnv) {
|
||||
async function maybeMigrateLegacyGatewayService(
|
||||
cfg: ClawdbotConfig,
|
||||
runtime: RuntimeEnv,
|
||||
prompter: DoctorPrompter,
|
||||
) {
|
||||
const legacyServices = await findLegacyGatewayServices(process.env);
|
||||
if (legacyServices.length === 0) return;
|
||||
@@ -728,13 +765,10 @@ async function maybeMigrateLegacyGatewayService(
|
||||
"Legacy Clawdis services detected",
|
||||
);
|
||||
|
||||
const migrate = guardCancel(
|
||||
await confirm({
|
||||
message: "Migrate legacy Clawdis services to Clawdbot now?",
|
||||
initialValue: true,
|
||||
}),
|
||||
runtime,
|
||||
);
|
||||
const migrate = await prompter.confirmSkipInNonInteractive({
|
||||
message: "Migrate legacy Clawdis services to Clawdbot now?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (!migrate) return;
|
||||
|
||||
try {
|
||||
@@ -764,23 +798,20 @@ async function maybeMigrateLegacyGatewayService(
|
||||
return;
|
||||
}
|
||||
|
||||
const install = guardCancel(
|
||||
await confirm({
|
||||
message: "Install Clawdbot gateway service now?",
|
||||
initialValue: true,
|
||||
}),
|
||||
runtime,
|
||||
);
|
||||
const install = await prompter.confirmSkipInNonInteractive({
|
||||
message: "Install Clawdbot gateway service now?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (!install) return;
|
||||
|
||||
const daemonRuntime = guardCancel(
|
||||
await select({
|
||||
const daemonRuntime = await prompter.select<GatewayDaemonRuntime>(
|
||||
{
|
||||
message: "Gateway daemon runtime",
|
||||
options: GATEWAY_DAEMON_RUNTIME_OPTIONS,
|
||||
initialValue: DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||
}),
|
||||
runtime,
|
||||
) as GatewayDaemonRuntime;
|
||||
},
|
||||
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||
);
|
||||
const devMode =
|
||||
process.argv[1]?.includes(`${path.sep}src${path.sep}`) &&
|
||||
process.argv[1]?.endsWith(".ts");
|
||||
@@ -811,6 +842,7 @@ export async function doctorCommand(
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
options: DoctorOptions = {},
|
||||
) {
|
||||
const prompter = createDoctorPrompter({ runtime, options });
|
||||
printWizardHeader(runtime);
|
||||
intro("Clawdbot doctor");
|
||||
|
||||
@@ -833,13 +865,10 @@ export async function doctorCommand(
|
||||
.join("\n"),
|
||||
"Legacy config keys detected",
|
||||
);
|
||||
const migrate = guardCancel(
|
||||
await confirm({
|
||||
message: "Migrate legacy config entries now?",
|
||||
initialValue: true,
|
||||
}),
|
||||
runtime,
|
||||
);
|
||||
const migrate = await prompter.confirm({
|
||||
message: "Migrate legacy config entries now?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (migrate) {
|
||||
// Legacy migration (2026-01-02, commit: 16420e5b) — normalize per-provider allowlists; move WhatsApp gating into whatsapp.allowFrom.
|
||||
const { config: migrated, changes } = migrateLegacyConfig(
|
||||
@@ -863,13 +892,10 @@ export async function doctorCommand(
|
||||
const legacyState = await detectLegacyStateMigrations({ cfg });
|
||||
if (legacyState.preview.length > 0) {
|
||||
note(legacyState.preview.join("\n"), "Legacy state detected");
|
||||
const migrate = guardCancel(
|
||||
await confirm({
|
||||
message: "Migrate legacy state (sessions/agent/WhatsApp auth) now?",
|
||||
initialValue: true,
|
||||
}),
|
||||
runtime,
|
||||
);
|
||||
const migrate = await prompter.confirm({
|
||||
message: "Migrate legacy state (sessions/agent/WhatsApp auth) now?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (migrate) {
|
||||
const migrated = await runLegacyStateMigrations({
|
||||
detected: legacyState,
|
||||
@@ -883,13 +909,17 @@ export async function doctorCommand(
|
||||
}
|
||||
}
|
||||
|
||||
cfg = await maybeRepairSandboxImages(cfg, runtime);
|
||||
cfg = await maybeRepairSandboxImages(cfg, runtime, prompter);
|
||||
|
||||
await maybeMigrateLegacyGatewayService(cfg, runtime);
|
||||
await maybeMigrateLegacyGatewayService(cfg, runtime, prompter);
|
||||
|
||||
await noteSecurityWarnings(cfg);
|
||||
|
||||
if (process.platform === "linux" && resolveMode(cfg) === "local") {
|
||||
if (
|
||||
options.nonInteractive !== true &&
|
||||
process.platform === "linux" &&
|
||||
resolveMode(cfg) === "local"
|
||||
) {
|
||||
const service = resolveGatewayService();
|
||||
let loaded = false;
|
||||
try {
|
||||
@@ -901,7 +931,7 @@ export async function doctorCommand(
|
||||
await ensureSystemdUserLingerInteractive({
|
||||
runtime,
|
||||
prompter: {
|
||||
confirm: async (p) => guardCancel(await confirm(p), runtime) === true,
|
||||
confirm: async (p) => prompter.confirm(p),
|
||||
note,
|
||||
},
|
||||
reason:
|
||||
@@ -955,13 +985,10 @@ export async function doctorCommand(
|
||||
"Gateway",
|
||||
);
|
||||
}
|
||||
const restart = guardCancel(
|
||||
await confirm({
|
||||
message: "Restart gateway daemon now?",
|
||||
initialValue: true,
|
||||
}),
|
||||
runtime,
|
||||
);
|
||||
const restart = await prompter.confirmSkipInNonInteractive({
|
||||
message: "Restart gateway daemon now?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (restart) {
|
||||
await service.restart({ stdout: process.stdout });
|
||||
await sleep(1500);
|
||||
|
||||
Reference in New Issue
Block a user