sudo commands through ephemeral terminal
This commit is contained in:
@@ -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?.();
|
||||
|
||||
Reference in New Issue
Block a user