diff --git a/src/commands/onboard-interactive.ts b/src/commands/onboard-interactive.ts index 2c534f0cfa..d0e147dc2b 100644 --- a/src/commands/onboard-interactive.ts +++ b/src/commands/onboard-interactive.ts @@ -1,6 +1,7 @@ import type { RuntimeEnv } from "../runtime.js"; import type { OnboardOptions } from "./onboard-types.js"; import { defaultRuntime } from "../runtime.js"; +import { restoreTerminalState } from "../terminal/restore.js"; import { createClackPrompter } from "../wizard/clack-prompter.js"; import { runOnboardingWizard } from "../wizard/onboarding.js"; import { WizardCancelledError } from "../wizard/prompts.js"; @@ -18,5 +19,7 @@ export async function runInteractiveOnboarding( return; } throw err; + } finally { + restoreTerminalState("onboarding finish"); } } diff --git a/src/runtime.ts b/src/runtime.ts index 819e360baa..c8eab74ec6 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -1,4 +1,5 @@ import { clearActiveProgressLine } from "./terminal/progress-line.js"; +import { restoreTerminalState } from "./terminal/restore.js"; export type RuntimeEnv = { log: typeof console.log; @@ -16,6 +17,7 @@ export const defaultRuntime: RuntimeEnv = { console.error(...args); }, exit: (code) => { + restoreTerminalState("runtime exit"); process.exit(code); throw new Error("unreachable"); // satisfies tests when mocked }, diff --git a/src/terminal/restore.ts b/src/terminal/restore.ts new file mode 100644 index 0000000000..b25baa3fde --- /dev/null +++ b/src/terminal/restore.ts @@ -0,0 +1,49 @@ +import { clearActiveProgressLine } from "./progress-line.js"; + +const RESET_SEQUENCE = "\x1b[0m\x1b[?25h\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?2004l"; + +function reportRestoreFailure(scope: string, err: unknown, reason?: string): void { + const suffix = reason ? ` (${reason})` : ""; + const message = `[terminal] restore ${scope} failed${suffix}: ${String(err)}`; + try { + process.stderr.write(`${message}\n`); + } catch (writeErr) { + try { + console.error(`[terminal] restore reporting failed${suffix}: ${String(writeErr)}`); + } catch (consoleErr) { + throw consoleErr; + } + } +} + +export function restoreTerminalState(reason?: string): void { + try { + clearActiveProgressLine(); + } catch (err) { + reportRestoreFailure("progress line", err, reason); + } + + const stdin = process.stdin; + if (stdin.isTTY && typeof stdin.setRawMode === "function") { + try { + stdin.setRawMode(false); + } catch (err) { + reportRestoreFailure("raw mode", err, reason); + } + if (typeof stdin.isPaused === "function" && stdin.isPaused()) { + try { + stdin.resume(); + } catch (err) { + reportRestoreFailure("stdin resume", err, reason); + } + } + } + + if (process.stdout.isTTY) { + try { + process.stdout.write(RESET_SEQUENCE); + } catch (err) { + reportRestoreFailure("stdout reset", err, reason); + } + } +}