sudo commands through ephemeral terminal

This commit is contained in:
2026-02-19 21:43:37 +00:00
parent 04c79a09f0
commit 7c0b11c5a5
6 changed files with 240 additions and 46 deletions
+38
View File
@@ -19,12 +19,14 @@ export type TerminalViewProps = {
sessionId?: string;
sandboxed?: boolean;
cwd?: string;
command?: string;
fontSize?: number;
fontFamily?: string;
theme?: TerminalTheme;
autoFocus?: boolean;
onReady?: (term: XTerm) => void;
onExit?: () => void;
onCommandDone?: (exitCode: number, output: string) => void;
onDisconnect?: () => void;
};
@@ -53,12 +55,14 @@ export const TerminalView = ({
sessionId,
sandboxed = true,
cwd,
command,
fontSize = 14,
fontFamily = 'Menlo, Monaco, "Courier New", monospace',
theme,
autoFocus = true,
onReady,
onExit,
onCommandDone,
onDisconnect,
}: TerminalViewProps) => {
const containerRef = useRef<HTMLDivElement>(null);
@@ -68,11 +72,15 @@ export const TerminalView = ({
const isMounted = useMounted();
const onReadyRef = useRef<TerminalViewProps['onReady']>(onReady);
const onExitRef = useRef<TerminalViewProps['onExit']>(onExit);
const onCommandDoneRef = useRef<TerminalViewProps['onCommandDone']>(onCommandDone);
const onDisconnectRef = useRef<TerminalViewProps['onDisconnect']>(onDisconnect);
const commandRef = useRef(command);
onReadyRef.current = onReady;
onExitRef.current = onExit;
onCommandDoneRef.current = onCommandDone;
onDisconnectRef.current = onDisconnect;
commandRef.current = command;
const background = theme?.background ?? DEFAULT_THEME.background;
const foreground = theme?.foreground ?? DEFAULT_THEME.foreground;
@@ -118,6 +126,12 @@ export const TerminalView = ({
const ws = new WebSocket(buildWsUrl(wsPath, sessionId, sandboxed, cwd));
wsRef.current = ws;
let commandSent = false;
let commandDone = false;
let commandOutput = '';
const EXIT_MARKER = '__OFFICER_EXIT_';
// eslint-disable-next-line no-control-regex
const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07/g, '');
const handleOpen = () => {
ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
@@ -128,6 +142,30 @@ export const TerminalView = ({
const msg = JSON.parse(ev.data as string);
if (msg.type === 'output') {
term.write(msg.data);
if (commandRef.current && !commandSent) {
commandSent = true;
setTimeout(() => {
if (ws.readyState === WebSocket.OPEN) {
const wrapped = onCommandDoneRef.current
? `${commandRef.current}; echo "${EXIT_MARKER}$?__"`
: commandRef.current;
ws.send(JSON.stringify({ type: 'input', data: wrapped + '\r' }));
}
}, 100);
}
if (commandSent && !commandDone && onCommandDoneRef.current) {
commandOutput += msg.data as string;
const markerMatch = stripAnsi(commandOutput).match(/__OFFICER_EXIT_(\d+)__/);
if (markerMatch) {
const exitCode = Number(markerMatch[1]);
const raw = stripAnsi(commandOutput).slice(0, markerMatch.index);
const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
const cmdLine = lines.findIndex((l) => l.includes(commandRef.current!.slice(0, 20)));
const output = lines.slice(cmdLine >= 0 ? cmdLine + 1 : 0).join('\n').trim();
commandDone = true;
onCommandDoneRef.current(exitCode, output);
}
}
} else if (msg.type === 'exit') {
term.write('\r\n[Process exited]\r\n');
onExitRef.current?.();