11import * as pty from '@homebridge/node-pty-prebuilt-multiarch' ;
22import { SpawnStatus } from '@codifycli/schemas' ;
3+ import { unlinkSync , writeFileSync } from 'node:fs' ;
4+ import { tmpdir } from 'node:os' ;
5+ import { join } from 'node:path' ;
36import stripAnsi from 'strip-ansi' ;
47
58import { SpawnError } from '../common/errors.js' ;
@@ -18,6 +21,7 @@ export interface SpawnOptions {
1821 env ?: Record < string , unknown > ,
1922 interactive ?: boolean ,
2023 requiresRoot ?: boolean ,
24+ requiresSudoAskpass ?: boolean ,
2125 stdin ?: boolean ,
2226 timeout ?: number ,
2327 allowSudoInCommand ?: boolean ,
@@ -34,7 +38,7 @@ export async function spawn(cmd: string, options?: SpawnOptions, pluginName?: st
3438}
3539
3640export async function spawnSafe ( cmd : string , options ?: SpawnOptions , pluginName ?: string , password ?: string ) : Promise < SpawnResult > {
37- if ( options ?. requiresRoot && ! password ) {
41+ if ( ( options ?. requiresRoot || options ?. requiresSudoAskpass ) && ! password ) {
3842 throw new Error ( 'Password must be specified!' ) ;
3943 }
4044
@@ -43,88 +47,102 @@ export async function spawnSafe(cmd: string, options?: SpawnOptions, pluginName?
4347 }
4448
4549 if ( pluginName ) {
46- ctx . pluginStdout ( pluginName , `Running command: ${ options ?. requiresRoot ? 'sudo' : '' } ${ cmd } ` + ( options ?. cwd ? `(${ options ?. cwd } )` : '' ) )
50+ ctx . pluginStdout ( pluginName , `Running command: ${ options ?. requiresRoot ? 'sudo' : options ?. requiresSudoAskpass ? 'sudo (askpass)' : '' } ${ cmd } ` + ( options ?. cwd ? `(${ options ?. cwd } )` : '' ) )
4751 } else {
4852 ctx . log ( `Running command: ${ cmd } ` + ( options ?. cwd ? `(${ options ?. cwd } )` : '' ) + '\n' ) ;
4953 }
5054
51- return new Promise ( ( resolve ) => {
52- const output : string [ ] = [ ] ;
53- const historyIgnore = ShellUtils . getShell ( ) === Shell . ZSH ? { HISTORY_IGNORE : '*' } : { HISTIGNORE : '*' } ;
54-
55- // If TERM_PROGRAM=Apple_Terminal is set then ANSI escape characters may be included
56- // in the response.
57- const env = {
58- ...process . env , ...options ?. env ,
59- TERM_PROGRAM : 'codify' ,
60- COMMAND_MODE : 'unix2003' ,
61- COLORTERM : 'truecolor' ,
62- ...historyIgnore
63- }
64-
65- // Initial terminal dimensions
66- const initialCols = process . stdout . columns ?? 80 ;
67- const initialRows = process . stdout . rows ?? 24 ;
68-
69- const command = options ?. requiresRoot ? `sudo -k >/dev/null 2>&1; sudo -S <<< "${ password } " -E ${ ShellUtils . getDefaultShell ( ) } ${ options ?. interactive ? '-i' : '' } -c "${ cmd . replaceAll ( '"' , '\\"' ) } "` : cmd ;
70- const args = options ?. interactive ? [ '-i' , '-c' , command ] : [ '-c' , command ]
71-
72- // Run the command in a pty for interactivity
73- const mPty = pty . spawn ( ShellUtils . getDefaultShell ( ) , args , {
74- ...options ,
75- cols : initialCols ,
76- rows : initialRows ,
77- env
78- } ) ;
55+ let tmpFile : string | undefined ;
56+ if ( options ?. requiresSudoAskpass && password ) {
57+ tmpFile = join ( tmpdir ( ) , `codify-askpass-${ Date . now ( ) } .sh` ) ;
58+ const escapedPassword = password . replace ( / ' / g, "'\\''" ) ;
59+ writeFileSync ( tmpFile , `#!/bin/bash\necho '${ escapedPassword } '` , { mode : 0o700 } ) ;
60+ }
7961
80- mPty . onData ( ( data ) => {
81- if ( pluginName && ! options ?. stdin ) {
82- ctx . pluginStdout ( pluginName , data )
83- } else {
84- ctx . log ( data ) ;
62+ try {
63+ return await new Promise < SpawnResult > ( ( resolve ) => {
64+ const output : string [ ] = [ ] ;
65+ const historyIgnore = ShellUtils . getShell ( ) === Shell . ZSH ? { HISTORY_IGNORE : '*' } : { HISTIGNORE : '*' } ;
66+
67+ // If TERM_PROGRAM=Apple_Terminal is set then ANSI escape characters may be included
68+ // in the response.
69+ const env = {
70+ ...process . env , ...options ?. env ,
71+ ...( tmpFile ? { SUDO_ASKPASS : tmpFile } : { } ) ,
72+ TERM_PROGRAM : 'codify' ,
73+ COMMAND_MODE : 'unix2003' ,
74+ COLORTERM : 'truecolor' ,
75+ ...historyIgnore
8576 }
8677
87- output . push ( data . toString ( ) ) ;
88- } )
89-
90- const resizeListener = ( ) => {
91- const { columns, rows } = process . stdout ;
92- mPty . resize ( columns , rows ) ;
93- }
78+ // Initial terminal dimensions
79+ const initialCols = process . stdout . columns ?? 80 ;
80+ const initialRows = process . stdout . rows ?? 24 ;
81+
82+ const command = options ?. requiresRoot ? `sudo -k >/dev/null 2>&1; sudo -S <<< "${ password } " -E ${ ShellUtils . getDefaultShell ( ) } ${ options ?. interactive ? '-i' : '' } -c "${ cmd . replaceAll ( '"' , '\\"' ) } "` : cmd ;
83+ const args = options ?. interactive ? [ '-i' , '-c' , command ] : [ '-c' , command ]
84+
85+ // Run the command in a pty for interactivity
86+ const mPty = pty . spawn ( ShellUtils . getDefaultShell ( ) , args , {
87+ ...options ,
88+ cols : initialCols ,
89+ rows : initialRows ,
90+ env
91+ } ) ;
92+
93+ mPty . onData ( ( data ) => {
94+ if ( pluginName && ! options ?. stdin ) {
95+ ctx . pluginStdout ( pluginName , data )
96+ } else {
97+ ctx . log ( data ) ;
98+ }
99+
100+ output . push ( data . toString ( ) ) ;
101+ } )
94102
95- const stdinListener = ( data : Buffer | string ) => {
96- // console.log('stdinListener', data) ;
97- mPty . write ( data . toString ( ) ) ;
98- }
103+ const resizeListener = ( ) => {
104+ const { columns , rows } = process . stdout ;
105+ mPty . resize ( columns , rows ) ;
106+ }
99107
100- // Listen to resize events for the terminal window;
101- process . stdout . on ( 'resize' , resizeListener ) ;
102- if ( options ?. stdin ) {
103- process . stdin . on ( 'data' , stdinListener )
104- }
108+ const stdinListener = ( data : Buffer | string ) => {
109+ // console.log('stdinListener', data);
110+ mPty . write ( data . toString ( ) ) ;
111+ }
105112
106- mPty . onExit ( ( result ) => {
107- process . stdout . off ( 'resize' , resizeListener ) ;
113+ // Listen to resize events for the terminal window;
114+ process . stdout . on ( 'resize' , resizeListener ) ;
108115 if ( options ?. stdin ) {
109- process . stdin . off ( 'data' , stdinListener ) ;
116+ process . stdin . on ( 'data' , stdinListener )
110117 }
111118
112- resolve ( {
113- status : result . exitCode === 0 ? SpawnStatus . SUCCESS : SpawnStatus . ERROR ,
114- exitCode : result . exitCode ,
115- data : stripAnsi ( output . join ( '\n' ) . trim ( ) ) ,
116- } )
117- } ) ;
119+ mPty . onExit ( ( result ) => {
120+ process . stdout . off ( 'resize' , resizeListener ) ;
121+ if ( options ?. stdin ) {
122+ process . stdin . off ( 'data' , stdinListener ) ;
123+ }
118124
119- if ( options ?. timeout ) {
120- setTimeout ( ( ) => {
121- mPty . kill ( ) ;
122125 resolve ( {
123- status : SpawnStatus . ERROR ,
124- exitCode : - 1 ,
125- data : '' ,
126- } ) ;
127- } , options . timeout ) ;
126+ status : result . exitCode === 0 ? SpawnStatus . SUCCESS : SpawnStatus . ERROR ,
127+ exitCode : result . exitCode ,
128+ data : stripAnsi ( output . join ( '\n' ) . trim ( ) ) ,
129+ } )
130+ } ) ;
131+
132+ if ( options ?. timeout ) {
133+ setTimeout ( ( ) => {
134+ mPty . kill ( ) ;
135+ resolve ( {
136+ status : SpawnStatus . ERROR ,
137+ exitCode : - 1 ,
138+ data : '' ,
139+ } ) ;
140+ } , options . timeout ) ;
141+ }
142+ } ) ;
143+ } finally {
144+ if ( tmpFile ) {
145+ try { unlinkSync ( tmpFile ) ; } catch { /* best effort */ }
128146 }
129- } )
147+ }
130148}
0 commit comments