From 948ae80ae60e5a423e176ea25e02446296aaceb0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 25 Aug 2026 16:31:09 -0700 Subject: [PATCH] fix(onboard): preserve JSON errors when no terminal is available (#129437) --- docs/cli/onboard.md | 2 ++ src/commands/onboard.test.ts | 11 +++++++++-- src/commands/onboard.ts | 3 +-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/cli/onboard.md b/docs/cli/onboard.md index a79d8801a77a..51550874e65c 100644 --- a/docs/cli/onboard.md +++ b/docs/cli/onboard.md @@ -369,6 +369,8 @@ Output: `--suppress-gateway-token-output` disables the automatic Control UI hand `--json` does not imply non-interactive mode in guided or classic onboarding. +Without an interactive terminal, both onboarding modes return a structured +JSON error; add `--non-interactive --accept-risk` for automation. With `--modern`, JSON is a one-shot OpenClaw overview and exits after that single result. Use `--non-interactive` for other scripts. diff --git a/src/commands/onboard.test.ts b/src/commands/onboard.test.ts index 1b4d2bcf229e..697315f88e8a 100644 --- a/src/commands/onboard.test.ts +++ b/src/commands/onboard.test.ts @@ -270,14 +270,21 @@ describe("setupWizardCommand", () => { it.each([ ["guided", { reset: true }], ["classic", { reset: true, classic: true }], + ["guided JSON", { reset: true, json: true }], + ["classic JSON", { reset: true, classic: true, json: true }], ] as const)("rejects headless %s onboarding before reset", async (_label, options) => { const runtime = makeRuntime(); mocks.hasInteractiveOnboardingTty.mockReturnValue(false); await setupWizardCommand(options, runtime); - expect(runtime.error).toHaveBeenCalledWith( - "Onboarding needs an interactive TTY. Use `openclaw onboard --non-interactive --accept-risk ...` for automation.", + const message = + "Onboarding needs an interactive TTY. Use `openclaw onboard --non-interactive --accept-risk ...` for automation."; + expect(runtime.error).toHaveBeenCalledWith(message); + expect(vi.mocked(runtime.log).mock.calls).toEqual( + "json" in options + ? [[JSON.stringify({ ok: false, phase: "options", message }, null, 2)]] + : [], ); expect(runtime.exit).toHaveBeenCalledWith(1); expect(mocks.readConfigFileSnapshot).not.toHaveBeenCalled(); diff --git a/src/commands/onboard.ts b/src/commands/onboard.ts index 7c5e2968ade9..6ff2d205d60f 100644 --- a/src/commands/onboard.ts +++ b/src/commands/onboard.ts @@ -644,8 +644,7 @@ export async function setupWizardCommand( if (!normalizedOpts.nonInteractive && !hasInteractiveOnboardingTty()) { // Reset is destructive, so prove the selected interactive surface can run // before reading or moving any operator state. - runtime.error(t("wizard.guided.ttyRequired")); - runtime.exit(1); + rejectOption(normalizedOpts, runtime, t("wizard.guided.ttyRequired")); return; }