mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix: preserve Gateway when setup Reinstall is cancelled (#130617)
Keep replacement preparation ahead of service mutation in configure and onboarding. Let platform installers own replacement, and report surviving Gateway reachability independently of failed setup.
This commit is contained in:
committed by
GitHub
parent
a872abc978
commit
682ddcb28c
@@ -55,6 +55,7 @@ When configure starts from a provider auth choice, the default-model and model-p
|
||||
|
||||
- After local config writes, configure installs selected downloadable plugins when the chosen setup path requires them. Remote gateway config does not install local plugin packages.
|
||||
- Channel-oriented services (Slack/Discord/Matrix/Microsoft Teams) prompt for channel/room allowlists during setup. You can enter names or IDs; the wizard resolves names to IDs when possible.
|
||||
- Choosing **Reinstall** keeps the existing Gateway service in place while you select its runtime and configure validates authentication and prepares the replacement. Cancelling or failing during preparation leaves the existing service installed.
|
||||
- If you run the daemon install step, token auth requires a token. If `gateway.auth.token` is SecretRef-managed, configure validates the SecretRef but does not persist resolved plaintext token values into supervisor service environment metadata; if the SecretRef is unresolved, configure blocks daemon install with actionable remediation guidance.
|
||||
- If both `gateway.auth.token` and `gateway.auth.password` are configured and `gateway.auth.mode` is unset, configure blocks daemon install until you set the mode explicitly.
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Configure daemon tests cover daemon install prompts, progress labels, and runtime install calls.
|
||||
|
||||
import { PassThrough } from "node:stream";
|
||||
import { select as clackSelect } from "@clack/prompts";
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { maybeInstallDaemon } from "./configure.daemon.js";
|
||||
@@ -14,13 +16,14 @@ const buildGatewayInstallPlan = vi.hoisted(() => vi.fn());
|
||||
const note = vi.hoisted(() => vi.fn());
|
||||
const serviceIsLoaded = vi.hoisted(() => vi.fn(async () => false));
|
||||
const serviceInstall = vi.hoisted(() => vi.fn(async () => {}));
|
||||
const serviceUninstall = vi.hoisted(() => vi.fn(async () => {}));
|
||||
const serviceRestart = vi.hoisted(() =>
|
||||
vi.fn<() => Promise<{ outcome: "completed" } | { outcome: "scheduled" }>>(async () => ({
|
||||
outcome: "completed",
|
||||
})),
|
||||
);
|
||||
const ensureSystemdUserLingerInteractive = vi.hoisted(() => vi.fn(async () => {}));
|
||||
const select = vi.hoisted(() => vi.fn(async () => "node"));
|
||||
const select = vi.hoisted(() => vi.fn<() => Promise<string | symbol>>(async () => "node"));
|
||||
|
||||
vi.mock("../cli/progress.js", () => ({
|
||||
withProgress,
|
||||
@@ -49,11 +52,6 @@ vi.mock("./configure.shared.js", () => ({
|
||||
select,
|
||||
}));
|
||||
|
||||
vi.mock("./daemon-runtime.js", () => ({
|
||||
DEFAULT_GATEWAY_DAEMON_RUNTIME: "node",
|
||||
GATEWAY_DAEMON_RUNTIME_OPTIONS: [{ value: "node", label: "Node" }],
|
||||
}));
|
||||
|
||||
vi.mock("../daemon/service.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("../daemon/service.js")>("../daemon/service.js");
|
||||
@@ -62,15 +60,12 @@ vi.mock("../daemon/service.js", async () => {
|
||||
resolveGatewayService: vi.fn(() => ({
|
||||
isLoaded: serviceIsLoaded,
|
||||
install: serviceInstall,
|
||||
uninstall: serviceUninstall,
|
||||
restart: serviceRestart,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./onboard-helpers.js", () => ({
|
||||
guardCancel: (value: unknown) => value,
|
||||
}));
|
||||
|
||||
vi.mock("./systemd-linger.js", () => ({
|
||||
ensureSystemdUserLingerInteractive,
|
||||
}));
|
||||
@@ -81,6 +76,9 @@ describe("maybeInstallDaemon", () => {
|
||||
progressSetLabel.mockReset();
|
||||
serviceIsLoaded.mockResolvedValue(false);
|
||||
serviceInstall.mockResolvedValue(undefined);
|
||||
serviceUninstall.mockReset();
|
||||
select.mockReset();
|
||||
select.mockResolvedValue("node");
|
||||
serviceRestart.mockResolvedValue({ outcome: "completed" });
|
||||
loadConfig.mockReturnValue({});
|
||||
resolveGatewayInstallToken.mockResolvedValue({
|
||||
@@ -113,7 +111,11 @@ describe("maybeInstallDaemon", () => {
|
||||
expect(serviceInstall).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("blocks install when token SecretRef is unresolved", async () => {
|
||||
it.each([false, true])("blocks install with unresolved auth (reinstall=%s)", async (loaded) => {
|
||||
serviceIsLoaded.mockResolvedValue(loaded);
|
||||
if (loaded) {
|
||||
select.mockResolvedValueOnce("reinstall");
|
||||
}
|
||||
resolveGatewayInstallToken.mockResolvedValue({
|
||||
token: undefined,
|
||||
tokenRefConfigured: true,
|
||||
@@ -133,6 +135,64 @@ describe("maybeInstallDaemon", () => {
|
||||
);
|
||||
expect(buildGatewayInstallPlan).not.toHaveBeenCalled();
|
||||
expect(serviceInstall).not.toHaveBeenCalled();
|
||||
expect(serviceUninstall).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps the installed service when runtime selection is cancelled", async () => {
|
||||
serviceIsLoaded.mockResolvedValue(true);
|
||||
const cancelled = await clackSelect({
|
||||
message: "Runtime",
|
||||
options: [{ value: "node", label: "Node" }],
|
||||
signal: AbortSignal.abort(),
|
||||
input: new PassThrough(),
|
||||
output: new PassThrough(),
|
||||
});
|
||||
select.mockResolvedValueOnce("reinstall").mockResolvedValueOnce(cancelled);
|
||||
const runtime = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(() => {
|
||||
throw new Error("setup cancelled");
|
||||
}),
|
||||
};
|
||||
|
||||
await expect(maybeInstallDaemon({ runtime, port: 18789 })).rejects.toThrow("setup cancelled");
|
||||
|
||||
expect(runtime.exit).toHaveBeenCalledWith(1);
|
||||
expect(serviceUninstall).not.toHaveBeenCalled();
|
||||
expect(resolveGatewayInstallToken).not.toHaveBeenCalled();
|
||||
expect(serviceInstall).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps the installed service when replacement planning fails", async () => {
|
||||
serviceIsLoaded.mockResolvedValue(true);
|
||||
select.mockResolvedValueOnce("reinstall");
|
||||
buildGatewayInstallPlan.mockRejectedValueOnce(new Error("replacement plan failed"));
|
||||
|
||||
await expect(
|
||||
maybeInstallDaemon({
|
||||
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() },
|
||||
port: 18789,
|
||||
}),
|
||||
).rejects.toThrow("replacement plan failed");
|
||||
|
||||
expect(serviceUninstall).not.toHaveBeenCalled();
|
||||
expect(serviceInstall).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("hands the existing service to the replacement installer", async () => {
|
||||
serviceIsLoaded.mockResolvedValue(true);
|
||||
select.mockResolvedValueOnce("reinstall");
|
||||
|
||||
const outcome = await maybeInstallDaemon({
|
||||
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() },
|
||||
port: 18789,
|
||||
});
|
||||
|
||||
expect(outcome).toBe("succeeded");
|
||||
expect(serviceInstall).toHaveBeenCalledOnce();
|
||||
expect(serviceUninstall).not.toHaveBeenCalled();
|
||||
expect(serviceRestart).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("continues daemon install flow when service status probe throws", async () => {
|
||||
|
||||
@@ -71,19 +71,10 @@ export async function maybeInstallDaemon(params: {
|
||||
if (action === "skip") {
|
||||
return "skipped";
|
||||
}
|
||||
if (action === "reinstall") {
|
||||
await withProgress(
|
||||
{ label: "Gateway service", indeterminate: true, delayMs: 0 },
|
||||
async (progress) => {
|
||||
progress.setLabel("Uninstalling Gateway service…");
|
||||
await service.uninstall({ env: process.env, stdout: process.stdout });
|
||||
progress.setLabel("Gateway service uninstalled.");
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldInstall) {
|
||||
// Keep the old service until preparation succeeds; install owns replacement.
|
||||
let installError: string | null = null;
|
||||
if (!params.daemonRuntime) {
|
||||
if (GATEWAY_DAEMON_RUNTIME_OPTIONS.length === 1) {
|
||||
|
||||
@@ -1140,8 +1140,6 @@ export const en = {
|
||||
gatewayServiceRestarted: "Gateway service restarted.",
|
||||
gatewayServiceRestarting: "Restarting Gateway service...",
|
||||
gatewayServiceRestartScheduled: "Gateway service restart scheduled.",
|
||||
gatewayServiceUninstalled: "Gateway service uninstalled.",
|
||||
gatewayServiceUninstalling: "Uninstalling Gateway service...",
|
||||
gatewayTokenGenerate: "Generate token: {command}",
|
||||
gatewayTokenShared: "Gateway token: shared auth for the Gateway + Control UI.",
|
||||
gatewayTokenStored:
|
||||
|
||||
@@ -1103,8 +1103,6 @@ export const zh_CN = {
|
||||
gatewayServiceRestarted: "Gateway 服务已重启。",
|
||||
gatewayServiceRestarting: "正在重启 Gateway 服务...",
|
||||
gatewayServiceRestartScheduled: "Gateway 服务重启已排队。",
|
||||
gatewayServiceUninstalled: "Gateway 服务已卸载。",
|
||||
gatewayServiceUninstalling: "正在卸载 Gateway 服务...",
|
||||
gatewayTokenGenerate: "生成令牌:{command}",
|
||||
gatewayTokenShared: "Gateway 令牌:Gateway 和 Control UI 的共享认证。",
|
||||
gatewayTokenStored:
|
||||
|
||||
@@ -1104,8 +1104,6 @@ export const zh_TW = {
|
||||
gatewayServiceRestarted: "Gateway 服務已重新啟動。",
|
||||
gatewayServiceRestarting: "正在重新啟動 Gateway 服務...",
|
||||
gatewayServiceRestartScheduled: "Gateway 服務重新啟動已排程。",
|
||||
gatewayServiceUninstalled: "Gateway 服務已解除安裝。",
|
||||
gatewayServiceUninstalling: "正在解除安裝 Gateway 服務...",
|
||||
gatewayTokenGenerate: "產生權杖:{command}",
|
||||
gatewayTokenShared: "Gateway 權杖:Gateway 和 Control UI 的共享認證。",
|
||||
gatewayTokenStored:
|
||||
|
||||
@@ -1111,6 +1111,37 @@ describe("finalizeSetupWizard", () => {
|
||||
expect(gatewayServiceInstall).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([false, true])(
|
||||
"detects the surviving gateway after failed reinstall (skipHealth=%s)",
|
||||
async (skipHealth) => {
|
||||
gatewayServiceIsLoaded.mockResolvedValue(true);
|
||||
buildGatewayInstallPlan.mockRejectedValueOnce(new Error("replacement plan failed"));
|
||||
probeGatewayReachable.mockResolvedValue({ ok: true });
|
||||
const prompter = buildWizardPrompter({
|
||||
select: vi.fn().mockResolvedValueOnce("reinstall").mockResolvedValueOnce("tui"),
|
||||
});
|
||||
|
||||
await finalizeSetupWizard(
|
||||
createFinalizeArgs("quickstart", {
|
||||
opts: { installDaemon: true, skipHealth },
|
||||
prompter,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(gatewayServiceUninstall).not.toHaveBeenCalled();
|
||||
expect(gatewayServiceInstall).not.toHaveBeenCalled();
|
||||
expect(waitForGatewayReachable).not.toHaveBeenCalled();
|
||||
expect(probeGatewayReachable).toHaveBeenCalledOnce();
|
||||
expect(healthCommand).toHaveBeenCalledTimes(skipHealth ? 0 : 1);
|
||||
expect(runTui).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ boundGateway: { url: "ws://127.0.0.1:18789" } }),
|
||||
);
|
||||
expectNoteContains(prompter, "replacement plan failed", "Gateway");
|
||||
expectNoteNotContains(prompter, "Gateway: not detected");
|
||||
expect(prompter.outro).toHaveBeenCalledWith(expect.stringContaining("setup failed"));
|
||||
},
|
||||
);
|
||||
|
||||
it("reports gateway installation failure without waiting for impossible health", async () => {
|
||||
gatewayServiceInstall.mockRejectedValueOnce(new Error("service install exploded"));
|
||||
const prompter = createLaterPrompter();
|
||||
@@ -1124,10 +1155,10 @@ describe("finalizeSetupWizard", () => {
|
||||
);
|
||||
|
||||
expect(waitForGatewayReachable).not.toHaveBeenCalled();
|
||||
expect(probeGatewayReachable).not.toHaveBeenCalled();
|
||||
expect(probeGatewayReachable).toHaveBeenCalledOnce();
|
||||
expect(runtime.error).toHaveBeenCalledWith("health failed");
|
||||
expectNoteContains(prompter, "service install exploded", "Gateway");
|
||||
expectNoteContains(prompter, "Gateway: not detected (service install exploded)", "Control UI");
|
||||
expectNoteContains(prompter, "Gateway: not detected (offline)", "Control UI");
|
||||
expect(prompter.outro).toHaveBeenCalledWith(
|
||||
expect.stringContaining("managed Mock Platform Service setup failed"),
|
||||
);
|
||||
@@ -1442,6 +1473,73 @@ describe("finalizeSetupWizard", () => {
|
||||
expect(progressStop).toHaveBeenCalledWith("Gateway service restart scheduled.");
|
||||
});
|
||||
|
||||
it.each(["auth", "planning"])(
|
||||
"preserves the installed service when reinstall %s fails",
|
||||
async (failure) => {
|
||||
let installed = true;
|
||||
gatewayServiceIsLoaded.mockImplementation(async () => installed);
|
||||
gatewayServiceUninstall.mockImplementationOnce(async () => {
|
||||
installed = false;
|
||||
});
|
||||
if (failure === "auth") {
|
||||
resolveGatewayInstallToken.mockImplementationOnce(async () => ({
|
||||
token: undefined,
|
||||
tokenRefConfigured: true,
|
||||
warnings: [],
|
||||
unavailableReason: "replacement auth unavailable",
|
||||
}));
|
||||
} else {
|
||||
buildGatewayInstallPlan.mockRejectedValueOnce(new Error("replacement plan failed"));
|
||||
}
|
||||
const prompter = buildWizardPrompter({ select: vi.fn(async () => "reinstall") as never });
|
||||
|
||||
const result = await ensureGatewayServiceForOnboarding(
|
||||
createFinalizeArgs("quickstart", { opts: { installDaemon: true }, prompter }),
|
||||
);
|
||||
|
||||
expect(result.gateway.status).toBe("failed");
|
||||
expect(installed).toBe(true);
|
||||
expect(gatewayServiceInstall).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it("passes the existing service intact to the reinstall owner", async () => {
|
||||
let installed = true;
|
||||
gatewayServiceIsLoaded.mockImplementation(async () => installed);
|
||||
gatewayServiceUninstall.mockImplementationOnce(async () => {
|
||||
installed = false;
|
||||
});
|
||||
gatewayServiceInstall.mockImplementationOnce(async () => {
|
||||
expect(installed).toBe(true);
|
||||
});
|
||||
const prompter = buildWizardPrompter({ select: vi.fn(async () => "reinstall") as never });
|
||||
|
||||
const result = await ensureGatewayServiceForOnboarding(
|
||||
createFinalizeArgs("quickstart", { opts: { installDaemon: true }, prompter }),
|
||||
);
|
||||
|
||||
expect(result.gateway).toEqual({ status: "ready", action: "installed" });
|
||||
expect(gatewayServiceInstall).toHaveBeenCalledOnce();
|
||||
expect(gatewayServiceUninstall).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each(["skip", "restart"])("does not turn %s into an implicit reinstall", async (action) => {
|
||||
gatewayServiceIsLoaded.mockResolvedValueOnce(true).mockResolvedValue(false);
|
||||
const prompter = buildWizardPrompter({ select: vi.fn(async () => action) as never });
|
||||
|
||||
const result = await ensureGatewayServiceForOnboarding(
|
||||
createFinalizeArgs("quickstart", { opts: { installDaemon: true }, prompter }),
|
||||
);
|
||||
|
||||
expect(result.gateway).toEqual({
|
||||
status: "ready",
|
||||
action: action === "restart" ? "restarted" : "reused",
|
||||
});
|
||||
expect(gatewayServiceInstall).not.toHaveBeenCalled();
|
||||
expect(gatewayServiceUninstall).not.toHaveBeenCalled();
|
||||
expect(gatewayServiceRestart).toHaveBeenCalledTimes(action === "restart" ? 1 : 0);
|
||||
});
|
||||
|
||||
it("localizes finalize non-prompt notes", async () => {
|
||||
const previousLocale = process.env.OPENCLAW_LOCALE;
|
||||
process.env.OPENCLAW_LOCALE = "zh-CN";
|
||||
|
||||
@@ -182,7 +182,7 @@ function buildGatewayRecoveryProjection(params: {
|
||||
} {
|
||||
const { gateway } = params;
|
||||
const notDetected = t("wizard.finalize.gatewayNotDetected");
|
||||
if (params.reachable) {
|
||||
if (params.reachable && gateway.status !== "failed") {
|
||||
return { detail: t("wizard.finalize.gatewayReachable"), summary: t("wizard.guided.complete") };
|
||||
}
|
||||
if (gateway.status === "ready") {
|
||||
@@ -202,7 +202,10 @@ function buildGatewayRecoveryProjection(params: {
|
||||
statusCommand: formatCliCommand("openclaw gateway status --deep"),
|
||||
recoveryCommand: formatCliCommand("openclaw gateway install --force"),
|
||||
});
|
||||
return { detail, summary: `${notDetected} ${detail.replaceAll("\n", " ")}` };
|
||||
return {
|
||||
detail,
|
||||
summary: `${params.reachable ? "" : `${notDetected} `}${detail.replaceAll("\n", " ")}`,
|
||||
};
|
||||
}
|
||||
|
||||
const startGuidance =
|
||||
@@ -388,7 +391,7 @@ export async function ensureGatewayServiceForOnboarding(params: {
|
||||
}
|
||||
}
|
||||
const loaded = await service.isLoaded({ env: process.env });
|
||||
let restartWasScheduled = false;
|
||||
let shouldInstall = !loaded;
|
||||
if (loaded) {
|
||||
const action =
|
||||
(params.loadedAction === "restart" ? params.loadedAction : undefined) ??
|
||||
@@ -415,7 +418,6 @@ export async function ensureGatewayServiceForOnboarding(params: {
|
||||
restartDoneMessage = restartStatus.scheduled
|
||||
? t("wizard.finalize.gatewayServiceRestartScheduled")
|
||||
: t("wizard.finalize.gatewayServiceRestarted");
|
||||
restartWasScheduled = restartStatus.scheduled;
|
||||
gateway = {
|
||||
status: "ready",
|
||||
action: restartStatus.scheduled ? "restart-scheduled" : "restarted",
|
||||
@@ -423,21 +425,12 @@ export async function ensureGatewayServiceForOnboarding(params: {
|
||||
},
|
||||
);
|
||||
} else if (action === "reinstall") {
|
||||
await withWizardProgress(
|
||||
t("wizard.finalize.gatewayService"),
|
||||
{ doneMessage: t("wizard.finalize.gatewayServiceUninstalled") },
|
||||
async (progress) => {
|
||||
progress.update(t("wizard.finalize.gatewayServiceUninstalling"));
|
||||
await service.uninstall({ env: process.env, stdout: process.stdout });
|
||||
},
|
||||
);
|
||||
// Preserve the old definition so the install owner can replace or restore it.
|
||||
shouldInstall = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!loaded ||
|
||||
(!restartWasScheduled && loaded && !(await service.isLoaded({ env: process.env })))
|
||||
) {
|
||||
if (shouldInstall) {
|
||||
const progress = prompter.progress(t("wizard.finalize.gatewayService"));
|
||||
let installError: string | null = null;
|
||||
const installWarnings: Array<{ message: string; title?: string }> = [];
|
||||
@@ -531,9 +524,6 @@ export async function finalizeSetupWizard(
|
||||
prompter,
|
||||
runtime,
|
||||
});
|
||||
if (gateway.status === "failed") {
|
||||
gatewayProbe = { ok: false, detail: gateway.error };
|
||||
}
|
||||
|
||||
if (settings.authMode === "password") {
|
||||
try {
|
||||
@@ -573,17 +563,17 @@ export async function finalizeSetupWizard(
|
||||
basePath: undefined,
|
||||
tlsEnabled: nextConfig.gateway?.tls?.enabled === true,
|
||||
});
|
||||
// A failed installation cannot become healthy; preserve its authoritative
|
||||
// error instead of masking it behind a slow, guaranteed-to-fail probe.
|
||||
if (gateway.status !== "failed") {
|
||||
// Install/restart can briefly flap the WS; wait before checking health.
|
||||
gatewayProbe = await waitForGatewayReachable({
|
||||
url: probeLinks.wsUrl,
|
||||
token: settings.authMode === "token" ? settings.gatewayToken : undefined,
|
||||
password: settings.authMode === "password" ? resolvedGatewayPassword : undefined,
|
||||
deadlineMs: 15_000,
|
||||
});
|
||||
}
|
||||
const probeOptions = {
|
||||
url: probeLinks.wsUrl,
|
||||
token: settings.authMode === "token" ? settings.gatewayToken : undefined,
|
||||
password: settings.authMode === "password" ? resolvedGatewayPassword : undefined,
|
||||
};
|
||||
// A failed replacement may leave the old Gateway alive. Observe it once;
|
||||
// only successful install/restart needs the startup grace period.
|
||||
gatewayProbe =
|
||||
gateway.status === "failed"
|
||||
? await probeGatewayReachable(probeOptions)
|
||||
: await waitForGatewayReachable({ ...probeOptions, deadlineMs: 15_000 });
|
||||
if (gatewayProbe.ok) {
|
||||
try {
|
||||
const healthConfig: OpenClawConfig =
|
||||
@@ -684,7 +674,7 @@ export async function finalizeSetupWizard(
|
||||
basePath: controlUiBasePath,
|
||||
tlsEnabled: nextConfig.gateway?.tls?.enabled === true,
|
||||
});
|
||||
if (gateway.status !== "failed" && (opts.skipHealth || !gatewayProbe.ok)) {
|
||||
if (opts.skipHealth || (!gatewayProbe.ok && gateway.status !== "failed")) {
|
||||
gatewayProbe = await probeGatewayReachable({
|
||||
url: probeLinks.wsUrl,
|
||||
token: settings.authMode === "token" ? settings.gatewayToken : undefined,
|
||||
@@ -993,12 +983,17 @@ export async function finalizeSetupWizard(
|
||||
await prompter.note(t("wizard.finalize.whatNow"), t("wizard.finalize.whatNowTitle"));
|
||||
|
||||
await prompter.outro(
|
||||
gatewayProbe.ok && gatewayHealthCheckFailed
|
||||
? t("wizard.finalize.outroHealthCheckFailed", {
|
||||
command: formatCliCommand("openclaw health"),
|
||||
})
|
||||
: gatewayProbe.ok
|
||||
? dashboardReady
|
||||
!gatewayProbe.ok || gateway.status === "failed"
|
||||
? buildGatewayRecoveryProjection({
|
||||
gateway,
|
||||
reachable: gatewayProbe.ok,
|
||||
serviceLabel: gateway.status === "skipped" ? undefined : resolveGatewayService().label,
|
||||
}).summary
|
||||
: gatewayHealthCheckFailed
|
||||
? t("wizard.finalize.outroHealthCheckFailed", {
|
||||
command: formatCliCommand("openclaw health"),
|
||||
})
|
||||
: dashboardReady
|
||||
? t("wizard.finalize.outroDashboardLink")
|
||||
: controlUiEnabled
|
||||
? [
|
||||
@@ -1007,13 +1002,7 @@ export async function finalizeSetupWizard(
|
||||
command: formatCliCommand("openclaw dashboard"),
|
||||
}),
|
||||
].join(" ")
|
||||
: t("wizard.guided.complete")
|
||||
: buildGatewayRecoveryProjection({
|
||||
gateway,
|
||||
reachable: false,
|
||||
serviceLabel:
|
||||
gateway.status === "skipped" ? undefined : resolveGatewayService().label,
|
||||
}).summary,
|
||||
: t("wizard.guided.complete"),
|
||||
);
|
||||
|
||||
if (shouldLaunchTui) {
|
||||
|
||||
Reference in New Issue
Block a user