11import pty from '@homebridge/node-pty-prebuilt-multiarch' ;
2+ import { Ajv } from 'ajv' ;
3+ import { CommandRequestResponseData , CommandRequestResponseDataSchema , IpcMessageV2 , MessageCmd } from 'codify-schemas' ;
4+ import { nanoid } from 'nanoid' ;
25import { EventEmitter } from 'node:events' ;
36import stripAnsi from 'strip-ansi' ;
47
@@ -8,6 +11,11 @@ import { IPty, SpawnError, SpawnOptions, SpawnResult, SpawnStatus } from './inde
811
912EventEmitter . defaultMaxListeners = 1000 ;
1013
14+ const ajv = new Ajv ( {
15+ strict : true ,
16+ } ) ;
17+ const validateSudoRequestResponse = ajv . compile ( CommandRequestResponseDataSchema ) ;
18+
1119/**
1220 * The background pty is a specialized pty designed for speed. It can launch multiple tasks
1321 * in parallel by moving them to the background. It attaches unix FIFO pipes to each process
@@ -26,11 +34,15 @@ export class SequentialPty implements IPty {
2634 }
2735
2836 async spawnSafe ( cmd : string , options ?: SpawnOptions ) : Promise < SpawnResult > {
37+ // If sudo is required, we must delegate to the main codify process.
38+ if ( options ?. interactive || options ?. requiresRoot ) {
39+ return this . externalSpawn ( cmd , options ) ;
40+ }
41+
2942 console . log ( `Running command: ${ cmd } ` + ( options ?. cwd ? `(${ options ?. cwd } )` : '' ) )
3043
3144 return new Promise ( ( resolve ) => {
3245 const output : string [ ] = [ ] ;
33-
3446 const historyIgnore = Utils . getShell ( ) === Shell . ZSH ? { HISTORY_IGNORE : '*' } : { HISTIGNORE : '*' } ;
3547
3648 // If TERM_PROGRAM=Apple_Terminal is set then ANSI escape characters may be included
@@ -39,17 +51,16 @@ export class SequentialPty implements IPty {
3951 ...process . env , ...options ?. env ,
4052 TERM_PROGRAM : 'codify' ,
4153 COMMAND_MODE : 'unix2003' ,
42- COLORTERM : 'truecolor' , ...historyIgnore
54+ COLORTERM : 'truecolor' ,
55+ ...historyIgnore
4356 }
4457
4558 // Initial terminal dimensions
4659 const initialCols = process . stdout . columns ?? 80 ;
4760 const initialRows = process . stdout . rows ?? 24 ;
4861
49- const args = ( options ?. interactive ?? false ) ? [ '-i' , '-c' , cmd ] : [ '-c' , cmd ]
50-
5162 // Run the command in a pty for interactivity
52- const mPty = pty . spawn ( this . getDefaultShell ( ) , args , {
63+ const mPty = pty . spawn ( this . getDefaultShell ( ) , [ '-c' , cmd ] , {
5364 ...options ,
5465 cols : initialCols ,
5566 rows : initialRows ,
@@ -64,23 +75,16 @@ export class SequentialPty implements IPty {
6475 output . push ( data . toString ( ) ) ;
6576 } )
6677
67- const stdinListener = ( data : any ) => {
68- mPty . write ( data . toString ( ) ) ;
69- } ;
70-
7178 const resizeListener = ( ) => {
7279 const { columns, rows } = process . stdout ;
7380 mPty . resize ( columns , rows ) ;
7481 }
7582
7683 // Listen to resize events for the terminal window;
7784 process . stdout . on ( 'resize' , resizeListener ) ;
78- // Listen for user input
79- process . stdin . on ( 'data' , stdinListener ) ;
8085
8186 mPty . onExit ( ( result ) => {
8287 process . stdout . off ( 'resize' , resizeListener ) ;
83- process . stdin . off ( 'data' , stdinListener ) ;
8488
8589 resolve ( {
8690 status : result . exitCode === 0 ? SpawnStatus . SUCCESS : SpawnStatus . ERROR ,
@@ -99,6 +103,39 @@ export class SequentialPty implements IPty {
99103 }
100104 }
101105
106+ // For safety reasons, requests that require sudo or are interactive must be run via the main client
107+ async externalSpawn (
108+ cmd : string ,
109+ opts : SpawnOptions
110+ ) : Promise < SpawnResult > {
111+ return new Promise ( ( resolve ) => {
112+ const requestId = nanoid ( 8 ) ;
113+
114+ const listener = ( data : IpcMessageV2 ) => {
115+ if ( data . requestId === requestId ) {
116+ process . removeListener ( 'message' , listener ) ;
117+
118+ if ( ! validateSudoRequestResponse ( data . data ) ) {
119+ throw new Error ( `Invalid response for sudo request: ${ JSON . stringify ( validateSudoRequestResponse . errors , null , 2 ) } ` ) ;
120+ }
121+
122+ resolve ( data . data as unknown as CommandRequestResponseData ) ;
123+ }
124+ }
125+
126+ process . on ( 'message' , listener ) ;
127+
128+ process . send ! ( < IpcMessageV2 > {
129+ cmd : MessageCmd . COMMAND_REQUEST ,
130+ data : {
131+ command : cmd ,
132+ options : opts ?? { } ,
133+ } ,
134+ requestId
135+ } )
136+ } ) ;
137+ }
138+
102139 private getDefaultShell ( ) : string {
103140 return process . env . SHELL ! ;
104141 }
0 commit comments