This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

fix shell

+72 -11
+12 -1
src/container/shell.test.ts
··· 3 3 import fs from "node:fs/promises" 4 4 import os from "node:os" 5 5 import path from "node:path" 6 - import { closeBash } from "./shell" 6 + import { cleanOutput, closeBash } from "./shell" 7 7 import { runCommand } from "./tools" 8 8 9 9 test.afterEach(() => { ··· 18 18 ) 19 19 20 20 assert.equal(output, "cat\n0\ncat\nFRX") 21 + }) 22 + 23 + test("shell recovers when a command leaves terminal echo enabled", async () => { 24 + assert.equal(await runCommand("stty echo; echo first", 0, 5_000), "first") 25 + assert.equal(await runCommand("echo second", 0, 5_000), "second") 26 + }) 27 + 28 + test("cleanOutput collapses terminal redraw controls", () => { 29 + assert.equal(cleanOutput("one\rspinner\rdone\n"), "done\n") 30 + assert.equal(cleanOutput("abc\b \bd\n"), "abd\n") 31 + assert.equal(cleanOutput("\x1b[?25lhidden\x1b[?25h\n"), "hidden\n") 21 32 }) 22 33 23 34 test("sudo commands run as one-off commands with stdin closed", async () => {
+60 -10
src/container/shell.ts
··· 10 10 } from "./config" 11 11 import type { RunRawOptions } from "./types" 12 12 13 + function applyTerminalControls(str: string): string { 14 + let out = "" 15 + 16 + for (let i = 0; i < str.length; i += 1) { 17 + const ch = str[i] 18 + 19 + if (ch === "\r") { 20 + if (str[i + 1] === "\n") { 21 + out += "\n" 22 + i += 1 23 + } else { 24 + const lineStart = out.lastIndexOf("\n") + 1 25 + out = out.slice(0, lineStart) 26 + } 27 + continue 28 + } 29 + 30 + if (ch === "\b" || ch === "\x7f") { 31 + if (out.length > 0 && out[out.length - 1] !== "\n") out = out.slice(0, -1) 32 + continue 33 + } 34 + 35 + if (ch === "\x00") continue 36 + out += ch 37 + } 38 + 39 + return out 40 + } 41 + 13 42 /** 14 - * Strip ANSI/VT escape sequences and normalize line endings from PTY output. 15 - * PTYs emit CRLF and control sequences that we don't want in command results. 43 + * Strip ANSI/VT escape sequences and apply simple terminal line controls. 44 + * PTYs emit redraw-oriented output (CR, backspace, ANSI cursor commands) that 45 + * should not be preserved as literal command result text. 16 46 */ 17 - function cleanOutput(str: string): string { 18 - return str 19 - .replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "") // CSI sequences (colors, cursor, etc.) 47 + export function cleanOutput(str: string): string { 48 + const withoutEscapes = str 49 + .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "") // CSI sequences (colors, cursor, erase, etc.) 20 50 .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "") // OSC sequences (title, etc.) 21 51 .replace(/\x1b[^[\]]/g, "") // other 2-char ESC sequences 22 52 .replace(/(?:\x03|\^C)\s*/g, "") // Ctrl+C echo after interrupt 23 - .replace(/\r\n/g, "\n") // CRLF -> LF 24 - .replace(/\r/g, "\n") // stray CR -> LF 25 - .replace(/\x00/g, "") // null bytes 53 + 54 + return applyTerminalControls(withoutEscapes) 55 + } 56 + 57 + function removeInternalPtyControlEchoes(str: string): string { 58 + return str 59 + .split("\n") 60 + .filter((line) => line !== "stty -echo 2>/dev/null") 61 + .join("\n") 26 62 } 27 63 28 64 let bash: pty.IPty | null = null ··· 201 237 const end = cleaned.indexOf(endSentinel) 202 238 // Drop the single newline that echo adds after the start sentinel, 203 239 // then trim trailing whitespace from the command output. 204 - resolve(cleaned.slice(start, end).replace(/^\n/, "").trimEnd()) 240 + resolve(removeInternalPtyControlEchoes(cleaned.slice(start, end).replace(/^\n/, "")).trimEnd()) 205 241 } 206 242 }) 207 243 ··· 209 245 // /dev/null so accidental interactive reads do not hang forever. 210 246 // The closing } is on its own line so heredocs inside the command still 211 247 // get their correct delimiter line. 248 + // 249 + // Re-assert `stty -echo` before both sentinels. Full-screen tools, 250 + // prompts, and some spinners can leave terminal echo enabled; if that 251 + // happens, bash echoes the sentinel input lines before executing them and 252 + // completion detection fires on our own command text. 212 253 const groupedCommand = redirectStdinToDevNull ? `{ ${command}\n} < /dev/null` : `{ ${command}\n}` 213 254 214 - session.write(`echo ${startSentinel}\n${groupedCommand}\necho ${endSentinel}\n`) 255 + session.write( 256 + [ 257 + "stty -echo 2>/dev/null", 258 + `echo ${startSentinel}`, 259 + groupedCommand, 260 + "stty -echo 2>/dev/null", 261 + `echo ${endSentinel}`, 262 + "", 263 + ].join("\n"), 264 + ) 215 265 216 266 const timer = setTimeout(() => { 217 267 if (settled) return