1+ // Copyright 2025, Command Line Inc.
2+ // SPDX-License-Identifier: Apache-2.0
3+
4+ // Always block (TUIs / pagers / multiplexers / known interactive UIs)
5+ const ALWAYS_BLOCK = [
6+ // multiplexers
7+ "tmux" , "screen" , "byobu" , "dtach" , "abduco" , "tmate" ,
8+ // editors/pagers
9+ "vim" , "nvim" , "emacs" , "nano" , "less" , "more" , "man" , "most" , "view" ,
10+ // TUIs / tools
11+ "htop" , "top" , "btop" , "fzf" , "ranger" , "mc" , "nnn" , "k9s" , "nmtui" , "alsamixer" ,
12+ "tig" , "gdb" , "lldb" ,
13+ // mail/irc
14+ "mutt" , "neomutt" , "alpine" , "weechat" , "irssi" ,
15+ // dialog UIs
16+ "dialog" , "whiptail" ,
17+ // DB shells
18+ "psql" , "mysql" , "sqlite3" , "mongo" , "redis-cli" ,
19+ ] ;
20+
21+ // Bare REPLs only block when no args
22+ const BARE_REPLS = [
23+ "python" , "python3" , "python2" , "node" , "ruby" , "perl" , "php" , "lua" , "ipython" , "bpython" , "irb" ,
24+ ] ;
25+
26+ // Shells: block only if interactive/new shell
27+ const SHELLS = [
28+ "bash" , "sh" , "zsh" , "fish" , "ksh" , "mksh" , "dash" , "ash" , "tcsh" , "csh" ,
29+ "xonsh" , "elvish" , "nu" , "nushell" , "pwsh" , "powershell" , "cmd" ,
30+ ] ;
31+
32+ // Wrappers to skip
33+ const WRAPPERS = [
34+ "sudo" , "doas" , "pkexec" , "rlwrap" , "env" , "time" , "nice" , "nohup" ,
35+ "chrt" , "stdbuf" , "script" , "scriptreplay" , "sshpass" ,
36+ ] ;
37+
38+ function looksInteractiveShellArgs ( args : string [ ] ) : boolean {
39+ return (
40+ args . length === 0 ||
41+ args . includes ( "-i" ) ||
42+ args . includes ( "--login" ) ||
43+ args . includes ( "-l" ) ||
44+ args . includes ( "-s" )
45+ ) ;
46+ }
47+
48+ function isNonInteractiveShellExec ( args : string [ ] ) : boolean {
49+ return (
50+ args . includes ( "-c" ) ||
51+ args . some ( ( a ) => a === "-Command" || a . startsWith ( "-Command" ) ) ||
52+ args . some ( ( a ) => a . endsWith ( ".sh" ) || a . includes ( "/" ) )
53+ ) ;
54+ }
55+
56+ function isAttachLike ( cmd : string , args : string [ ] ) : boolean {
57+ if ( cmd === "docker" || cmd === "podman" ) {
58+ if ( args [ 0 ] === "attach" ) return true ;
59+ if ( args [ 0 ] === "exec" ) return args . some ( ( a ) => a === "-it" || a === "-i" || a === "-t" ) ;
60+ }
61+ if ( cmd === "kubectl" || cmd === "k3s" || cmd === "oc" ) {
62+ if ( args [ 0 ] === "attach" ) return true ;
63+ if ( args [ 0 ] === "exec" ) return args . some ( ( a ) => a === "-it" || a === "-i" || a === "-t" ) ;
64+ }
65+ if ( cmd === "lxc" && args [ 0 ] === "exec" ) return args . some ( ( a ) => a === "-t" || a === "-T" ) ;
66+ return false ;
67+ }
68+
69+ function isSshInteractive ( args : string [ ] ) : boolean {
70+ const hasForcedTty = args . includes ( "-t" ) || args . includes ( "-tt" ) ;
71+ const hasRemoteCmd = args . some ( ( a ) => ! a . startsWith ( "-" ) && a . includes ( " " ) ) ;
72+ return hasForcedTty || ! hasRemoteCmd ;
73+ }
74+
75+ export function getBlockingCommand ( lastCommand : string | null , inAltBuffer : boolean ) : string | null {
76+ if ( ! lastCommand ) return null ;
77+
78+ let words = lastCommand . trim ( ) . split ( / \s + / ) ;
79+ if ( words . length === 0 ) return null ;
80+
81+ while ( words . length && WRAPPERS . includes ( words [ 0 ] ) ) {
82+ words . shift ( ) ;
83+ }
84+ if ( ! words . length ) return null ;
85+
86+ const first = words [ 0 ] . split ( "/" ) . pop ( ) ! ;
87+ const args = words . slice ( 1 ) ;
88+
89+ if ( inAltBuffer ) return first ;
90+
91+ if ( ALWAYS_BLOCK . includes ( first ) ) return first ;
92+
93+ if ( isAttachLike ( first , args ) ) return first ;
94+
95+ if ( first === "ssh" || first === "mosh" || first === "telnet" || first === "rlogin" ) {
96+ if ( isSshInteractive ( args ) ) return first ;
97+ return null ;
98+ }
99+
100+ if ( first === "su" || first === "machinectl" || first === "chroot" || first === "nsenter" || first === "lxc" ) {
101+ if ( ! args . length || SHELLS . includes ( args [ args . length - 1 ] ?. split ( "/" ) . pop ( ) || "" ) ) return first ;
102+ return null ;
103+ }
104+
105+ if ( SHELLS . includes ( first ) ) {
106+ if ( looksInteractiveShellArgs ( args ) ) return first ;
107+ if ( isNonInteractiveShellExec ( args ) ) return null ;
108+ return null ;
109+ }
110+
111+ if ( BARE_REPLS . includes ( first ) ) {
112+ if ( args . length === 0 ) return first ;
113+ return null ;
114+ }
115+
116+ return null ;
117+ }
0 commit comments