From 76c587973e5a025558131abc7f9f7f20b2d8081d Mon Sep 17 00:00:00 2001 From: James Grugett Date: Sat, 23 May 2026 16:23:55 -0700 Subject: [PATCH 1/2] Fix wrapper terminal reset for help output --- cli/release-staging/index.js | 27 ++++++++++++++++++--------- cli/release/index.js | 27 ++++++++++++++++++--------- freebuff/cli/release/index.js | 27 ++++++++++++++++++--------- 3 files changed, 54 insertions(+), 27 deletions(-) diff --git a/cli/release-staging/index.js b/cli/release-staging/index.js index 083e8879a9..70be887186 100644 --- a/cli/release-staging/index.js +++ b/cli/release-staging/index.js @@ -17,11 +17,9 @@ const packageName = 'codecane' * Terminal escape sequences to reset terminal state after the child process exits. * When the binary is SIGKILL'd, it can't clean up its own terminal state. * The wrapper (this process) survives and must reset these modes. - * - * Keep in sync with TERMINAL_RESET_SEQUENCES in cli/src/utils/renderer-cleanup.ts */ -const TERMINAL_RESET_SEQUENCES = - '\x1b[?1049l' + // Exit alternate screen buffer +const EXIT_ALTERNATE_SCREEN_SEQUENCE = '\x1b[?1049l' +const SAFE_TERMINAL_RESET_SEQUENCES = '\x1b[?1000l' + // Disable X10 mouse mode '\x1b[?1002l' + // Disable button event mouse mode '\x1b[?1003l' + // Disable any-event mouse mode (all motion) @@ -30,7 +28,12 @@ const TERMINAL_RESET_SEQUENCES = '\x1b[?2004l' + // Disable bracketed paste mode '\x1b[?25h' // Show cursor -function resetTerminal() { +const FULL_TERMINAL_RESET_SEQUENCES = + EXIT_ALTERNATE_SCREEN_SEQUENCE + SAFE_TERMINAL_RESET_SEQUENCES + +function resetTerminal(options = {}) { + const { exitAlternateScreen = false } = options + try { if (process.stdin.isTTY && process.stdin.setRawMode) { process.stdin.setRawMode(false) @@ -40,7 +43,13 @@ function resetTerminal() { } try { if (process.stdout.isTTY) { - process.stdout.write(TERMINAL_RESET_SEQUENCES) + // Exiting the alternate screen is only safe after an interactive child. + // Plain CLI paths like --help never enter it, and ?1049l can erase output. + process.stdout.write( + exitAlternateScreen + ? FULL_TERMINAL_RESET_SEQUENCES + : SAFE_TERMINAL_RESET_SEQUENCES, + ) } } catch { // stdout may be closed @@ -465,7 +474,7 @@ async function checkForUpdates(runningProcess, exitListener) { }, 5000) }) - resetTerminal() + resetTerminal({ exitAlternateScreen: true }) console.log(`Update available: ${currentVersion} → ${latestVersion}`) await downloadBinary(latestVersion) @@ -476,7 +485,7 @@ async function checkForUpdates(runningProcess, exitListener) { }) newChild.on('exit', (code, signal) => { - resetTerminal() + resetTerminal({ exitAlternateScreen: Boolean(signal) }) printCrashDiagnostics(code, signal) process.exit(signal ? 1 : (code || 0)) }) @@ -557,7 +566,7 @@ async function main() { }) const exitListener = (code, signal) => { - resetTerminal() + resetTerminal({ exitAlternateScreen: Boolean(signal) }) printCrashDiagnostics(code, signal) process.exit(signal ? 1 : (code || 0)) } diff --git a/cli/release/index.js b/cli/release/index.js index bf1eead545..a413a3af0d 100644 --- a/cli/release/index.js +++ b/cli/release/index.js @@ -17,11 +17,9 @@ const packageName = 'codebuff' * Terminal escape sequences to reset terminal state after the child process exits. * When the binary is SIGKILL'd, it can't clean up its own terminal state. * The wrapper (this process) survives and must reset these modes. - * - * Keep in sync with TERMINAL_RESET_SEQUENCES in cli/src/utils/renderer-cleanup.ts */ -const TERMINAL_RESET_SEQUENCES = - '\x1b[?1049l' + // Exit alternate screen buffer +const EXIT_ALTERNATE_SCREEN_SEQUENCE = '\x1b[?1049l' +const SAFE_TERMINAL_RESET_SEQUENCES = '\x1b[?1000l' + // Disable X10 mouse mode '\x1b[?1002l' + // Disable button event mouse mode '\x1b[?1003l' + // Disable any-event mouse mode (all motion) @@ -30,7 +28,12 @@ const TERMINAL_RESET_SEQUENCES = '\x1b[?2004l' + // Disable bracketed paste mode '\x1b[?25h' // Show cursor -function resetTerminal() { +const FULL_TERMINAL_RESET_SEQUENCES = + EXIT_ALTERNATE_SCREEN_SEQUENCE + SAFE_TERMINAL_RESET_SEQUENCES + +function resetTerminal(options = {}) { + const { exitAlternateScreen = false } = options + try { if (process.stdin.isTTY && process.stdin.setRawMode) { process.stdin.setRawMode(false) @@ -40,7 +43,13 @@ function resetTerminal() { } try { if (process.stdout.isTTY) { - process.stdout.write(TERMINAL_RESET_SEQUENCES) + // Exiting the alternate screen is only safe after an interactive child. + // Plain CLI paths like --help never enter it, and ?1049l can erase output. + process.stdout.write( + exitAlternateScreen + ? FULL_TERMINAL_RESET_SEQUENCES + : SAFE_TERMINAL_RESET_SEQUENCES, + ) } } catch { // stdout may be closed @@ -485,7 +494,7 @@ async function checkForUpdates(runningProcess, exitListener) { }, 5000) }) - resetTerminal() + resetTerminal({ exitAlternateScreen: true }) console.log(`Update available: ${currentVersion} → ${latestVersion}`) await downloadBinary(latestVersion) @@ -493,7 +502,7 @@ async function checkForUpdates(runningProcess, exitListener) { const newChild = spawnInstalledBinary({ detached: false }) newChild.on('exit', (code, signal) => { - resetTerminal() + resetTerminal({ exitAlternateScreen: Boolean(signal) }) printCrashDiagnostics(code, signal) process.exit(signal ? 1 : (code || 0)) }) @@ -625,7 +634,7 @@ async function main() { const child = spawnInstalledBinary() const exitListener = (code, signal) => { - resetTerminal() + resetTerminal({ exitAlternateScreen: Boolean(signal) }) printCrashDiagnostics(code, signal) process.exit(signal ? 1 : (code || 0)) } diff --git a/freebuff/cli/release/index.js b/freebuff/cli/release/index.js index ca853b83fb..9b5782c336 100644 --- a/freebuff/cli/release/index.js +++ b/freebuff/cli/release/index.js @@ -17,11 +17,9 @@ const packageName = 'freebuff' * Terminal escape sequences to reset terminal state after the child process exits. * When the binary is SIGKILL'd, it can't clean up its own terminal state. * The wrapper (this process) survives and must reset these modes. - * - * Keep in sync with TERMINAL_RESET_SEQUENCES in cli/src/utils/renderer-cleanup.ts */ -const TERMINAL_RESET_SEQUENCES = - '\x1b[?1049l' + // Exit alternate screen buffer +const EXIT_ALTERNATE_SCREEN_SEQUENCE = '\x1b[?1049l' +const SAFE_TERMINAL_RESET_SEQUENCES = '\x1b[?1000l' + // Disable X10 mouse mode '\x1b[?1002l' + // Disable button event mouse mode '\x1b[?1003l' + // Disable any-event mouse mode (all motion) @@ -30,7 +28,12 @@ const TERMINAL_RESET_SEQUENCES = '\x1b[?2004l' + // Disable bracketed paste mode '\x1b[?25h' // Show cursor -function resetTerminal() { +const FULL_TERMINAL_RESET_SEQUENCES = + EXIT_ALTERNATE_SCREEN_SEQUENCE + SAFE_TERMINAL_RESET_SEQUENCES + +function resetTerminal(options = {}) { + const { exitAlternateScreen = false } = options + try { if (process.stdin.isTTY && process.stdin.setRawMode) { process.stdin.setRawMode(false) @@ -40,7 +43,13 @@ function resetTerminal() { } try { if (process.stdout.isTTY) { - process.stdout.write(TERMINAL_RESET_SEQUENCES) + // Exiting the alternate screen is only safe after an interactive child. + // Plain CLI paths like --help never enter it, and ?1049l can erase output. + process.stdout.write( + exitAlternateScreen + ? FULL_TERMINAL_RESET_SEQUENCES + : SAFE_TERMINAL_RESET_SEQUENCES, + ) } } catch { // stdout may be closed @@ -472,7 +481,7 @@ async function checkForUpdates(runningProcess, exitListener) { }, 5000) }) - resetTerminal() + resetTerminal({ exitAlternateScreen: true }) console.log(`Update available: ${currentVersion} → ${latestVersion}`) await downloadBinary(latestVersion) @@ -480,7 +489,7 @@ async function checkForUpdates(runningProcess, exitListener) { const newChild = spawnInstalledBinary({ detached: false }) newChild.on('exit', (code, signal) => { - resetTerminal() + resetTerminal({ exitAlternateScreen: Boolean(signal) }) printCrashDiagnostics(code, signal) process.exit(signal ? 1 : (code || 0)) }) @@ -612,7 +621,7 @@ async function main() { const child = spawnInstalledBinary() const exitListener = (code, signal) => { - resetTerminal() + resetTerminal({ exitAlternateScreen: Boolean(signal) }) printCrashDiagnostics(code, signal) process.exit(signal ? 1 : (code || 0)) } From 117b482f67dffc9df32689c2d96599c59bf9b016 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Sat, 23 May 2026 16:39:41 -0700 Subject: [PATCH 2/2] Handle Windows crash resets in wrappers --- cli/release-staging/index.js | 28 +++++++++++++++++++++++++--- cli/release/index.js | 28 +++++++++++++++++++++++++--- freebuff/cli/release/index.js | 28 +++++++++++++++++++++++++--- 3 files changed, 75 insertions(+), 9 deletions(-) diff --git a/cli/release-staging/index.js b/cli/release-staging/index.js index 70be887186..9f40380085 100644 --- a/cli/release-staging/index.js +++ b/cli/release-staging/index.js @@ -56,6 +56,24 @@ function resetTerminal(options = {}) { } } +function getUnsignedExitCode(code) { + return code != null && code < 0 ? (code >>> 0) : code +} + +function isWindowsNativeCrashCode(code) { + const unsignedCode = getUnsignedExitCode(code) + return ( + process.platform === 'win32' && + (unsignedCode === 0xC000001D || + unsignedCode === 0xC0000005 || + unsignedCode === 0xC0000409) + ) +} + +function shouldExitAlternateScreen(code, signal) { + return Boolean(signal) || isWindowsNativeCrashCode(code) +} + function createConfig(packageName) { const homeDir = os.homedir() const configDir = path.join(homeDir, '.config', 'manicode') @@ -485,7 +503,9 @@ async function checkForUpdates(runningProcess, exitListener) { }) newChild.on('exit', (code, signal) => { - resetTerminal({ exitAlternateScreen: Boolean(signal) }) + resetTerminal({ + exitAlternateScreen: shouldExitAlternateScreen(code, signal), + }) printCrashDiagnostics(code, signal) process.exit(signal ? 1 : (code || 0)) }) @@ -504,7 +524,7 @@ async function checkForUpdates(runningProcess, exitListener) { function printCrashDiagnostics(code, signal) { // Windows NTSTATUS codes (unsigned DWORD) - const unsignedCode = code != null && code < 0 ? (code >>> 0) : code + const unsignedCode = getUnsignedExitCode(code) const isIllegalInstruction = signal === 'SIGILL' || (process.platform === 'win32' && unsignedCode === 0xC000001D) @@ -566,7 +586,9 @@ async function main() { }) const exitListener = (code, signal) => { - resetTerminal({ exitAlternateScreen: Boolean(signal) }) + resetTerminal({ + exitAlternateScreen: shouldExitAlternateScreen(code, signal), + }) printCrashDiagnostics(code, signal) process.exit(signal ? 1 : (code || 0)) } diff --git a/cli/release/index.js b/cli/release/index.js index a413a3af0d..f5e24e3640 100644 --- a/cli/release/index.js +++ b/cli/release/index.js @@ -56,6 +56,24 @@ function resetTerminal(options = {}) { } } +function getUnsignedExitCode(code) { + return code != null && code < 0 ? (code >>> 0) : code +} + +function isWindowsNativeCrashCode(code) { + const unsignedCode = getUnsignedExitCode(code) + return ( + process.platform === 'win32' && + (unsignedCode === 0xC000001D || + unsignedCode === 0xC0000005 || + unsignedCode === 0xC0000409) + ) +} + +function shouldExitAlternateScreen(code, signal) { + return Boolean(signal) || isWindowsNativeCrashCode(code) +} + function createConfig(packageName) { const homeDir = os.homedir() const configDir = path.join(homeDir, '.config', 'manicode') @@ -502,7 +520,9 @@ async function checkForUpdates(runningProcess, exitListener) { const newChild = spawnInstalledBinary({ detached: false }) newChild.on('exit', (code, signal) => { - resetTerminal({ exitAlternateScreen: Boolean(signal) }) + resetTerminal({ + exitAlternateScreen: shouldExitAlternateScreen(code, signal), + }) printCrashDiagnostics(code, signal) process.exit(signal ? 1 : (code || 0)) }) @@ -516,7 +536,7 @@ async function checkForUpdates(runningProcess, exitListener) { function printCrashDiagnostics(code, signal) { // Windows NTSTATUS codes (unsigned DWORD) - const unsignedCode = code != null && code < 0 ? (code >>> 0) : code + const unsignedCode = getUnsignedExitCode(code) const isIllegalInstruction = signal === 'SIGILL' || (process.platform === 'win32' && unsignedCode === 0xC000001D) @@ -634,7 +654,9 @@ async function main() { const child = spawnInstalledBinary() const exitListener = (code, signal) => { - resetTerminal({ exitAlternateScreen: Boolean(signal) }) + resetTerminal({ + exitAlternateScreen: shouldExitAlternateScreen(code, signal), + }) printCrashDiagnostics(code, signal) process.exit(signal ? 1 : (code || 0)) } diff --git a/freebuff/cli/release/index.js b/freebuff/cli/release/index.js index 9b5782c336..312e96697c 100644 --- a/freebuff/cli/release/index.js +++ b/freebuff/cli/release/index.js @@ -56,6 +56,24 @@ function resetTerminal(options = {}) { } } +function getUnsignedExitCode(code) { + return code != null && code < 0 ? (code >>> 0) : code +} + +function isWindowsNativeCrashCode(code) { + const unsignedCode = getUnsignedExitCode(code) + return ( + process.platform === 'win32' && + (unsignedCode === 0xC000001D || + unsignedCode === 0xC0000005 || + unsignedCode === 0xC0000409) + ) +} + +function shouldExitAlternateScreen(code, signal) { + return Boolean(signal) || isWindowsNativeCrashCode(code) +} + function createConfig(packageName) { const homeDir = os.homedir() const configDir = path.join(homeDir, '.config', 'manicode') @@ -489,7 +507,9 @@ async function checkForUpdates(runningProcess, exitListener) { const newChild = spawnInstalledBinary({ detached: false }) newChild.on('exit', (code, signal) => { - resetTerminal({ exitAlternateScreen: Boolean(signal) }) + resetTerminal({ + exitAlternateScreen: shouldExitAlternateScreen(code, signal), + }) printCrashDiagnostics(code, signal) process.exit(signal ? 1 : (code || 0)) }) @@ -503,7 +523,7 @@ async function checkForUpdates(runningProcess, exitListener) { function printCrashDiagnostics(code, signal) { // Windows NTSTATUS codes (unsigned DWORD) - const unsignedCode = code != null && code < 0 ? (code >>> 0) : code + const unsignedCode = getUnsignedExitCode(code) const isIllegalInstruction = signal === 'SIGILL' || (process.platform === 'win32' && unsignedCode === 0xC000001D) @@ -621,7 +641,9 @@ async function main() { const child = spawnInstalledBinary() const exitListener = (code, signal) => { - resetTerminal({ exitAlternateScreen: Boolean(signal) }) + resetTerminal({ + exitAlternateScreen: shouldExitAlternateScreen(code, signal), + }) printCrashDiagnostics(code, signal) process.exit(signal ? 1 : (code || 0)) }