77use std:: collections:: { BTreeMap , HashMap } ;
88use std:: io:: Write as _;
99use std:: path:: { Path , PathBuf } ;
10+ use std:: sync:: Arc ;
11+ use std:: sync:: atomic:: { AtomicBool , Ordering } ;
1012
1113use anyhow:: { Context , anyhow, bail} ;
1214use clap:: Args ;
15+ use crossterm:: terminal;
1316use serde:: Deserialize ;
1417use sha2:: { Digest , Sha256 } ;
1518use tracing:: debug;
@@ -235,6 +238,19 @@ pub async fn cmd_run(args: DevRunArgs) -> anyhow::Result<()> {
235238 eprintln ! ( "VM ready." ) ;
236239
237240 // Run setup commands if needed.
241+ // When --fresh, force re-run by clearing any cached hashes.
242+ if args. fresh {
243+ if let Ok ( path) = host_setup_hash_path ( & sandbox_id) {
244+ let _ = std:: fs:: remove_file ( path) ;
245+ }
246+ let container_id = resolve_container ( & mut client, & sandbox_id) . await ?;
247+ let _ = exec_quiet (
248+ & mut client,
249+ & container_id,
250+ "rm -f /run/vz-oci/volumes/.vz-setup-hash" ,
251+ )
252+ . await ;
253+ }
238254 run_setup_if_needed ( & mut client, & sandbox_id, & config) . await ?;
239255 }
240256
@@ -245,6 +261,11 @@ pub async fn cmd_run(args: DevRunArgs) -> anyhow::Result<()> {
245261 let shell_command = args. command . join ( " " ) ;
246262 let mut env_map = config. env . clone ( ) ;
247263
264+ // Ensure HOME is always set — many tools (rustup, npm, etc.) depend on it.
265+ if !env_map. contains_key ( "HOME" ) {
266+ env_map. insert ( "HOME" . to_string ( ) , "/root" . to_string ( ) ) ;
267+ }
268+
248269 // Auto-detect Rust projects and set CARGO_TARGET_DIR to persistent disk
249270 // so build artifacts survive VM restarts.
250271 if !env_map. contains_key ( "CARGO_TARGET_DIR" ) && project_dir. join ( "Cargo.toml" ) . exists ( ) {
@@ -295,7 +316,69 @@ pub async fn cmd_run(args: DevRunArgs) -> anyhow::Result<()> {
295316 let execution_payload = execution
296317 . execution
297318 . ok_or_else ( || anyhow ! ( "daemon missing execution payload" ) ) ?;
298- let execution_id = execution_payload. execution_id ;
319+ let execution_id = execution_payload. execution_id . clone ( ) ;
320+
321+ // For interactive mode: enable raw terminal and forward stdin to the PTY.
322+ let stdin_stop = Arc :: new ( AtomicBool :: new ( false ) ) ;
323+ let stdin_handle = if args. interactive {
324+ terminal:: enable_raw_mode ( ) . context ( "failed to enable raw mode" ) ?;
325+
326+ let stop = Arc :: clone ( & stdin_stop) ;
327+ let exec_id = execution_id. clone ( ) ;
328+ let mut stdin_client = client. clone ( ) ;
329+ Some ( tokio:: task:: spawn_blocking ( move || {
330+ use crossterm:: event:: { self , Event , KeyCode , KeyEventKind , KeyModifiers } ;
331+ while !stop. load ( Ordering :: Relaxed ) {
332+ if !event:: poll ( std:: time:: Duration :: from_millis ( 100 ) ) . unwrap_or ( false ) {
333+ continue ;
334+ }
335+ let Ok ( ev) = event:: read ( ) else { break } ;
336+ let bytes = match ev {
337+ Event :: Key ( key)
338+ if matches ! ( key. kind, KeyEventKind :: Press | KeyEventKind :: Repeat ) =>
339+ {
340+ match key. code {
341+ KeyCode :: Char ( c)
342+ if key. modifiers . contains ( KeyModifiers :: CONTROL ) =>
343+ {
344+ vec ! [ c as u8 & 0x1f ]
345+ }
346+ KeyCode :: Char ( c) => {
347+ let mut buf = [ 0u8 ; 4 ] ;
348+ c. encode_utf8 ( & mut buf) ;
349+ buf[ ..c. len_utf8 ( ) ] . to_vec ( )
350+ }
351+ KeyCode :: Enter => vec ! [ b'\r' ] ,
352+ KeyCode :: Backspace => vec ! [ 0x7f ] ,
353+ KeyCode :: Tab => vec ! [ b'\t' ] ,
354+ KeyCode :: Esc => vec ! [ 0x1b ] ,
355+ KeyCode :: Up => vec ! [ 0x1b , b'[' , b'A' ] ,
356+ KeyCode :: Down => vec ! [ 0x1b , b'[' , b'B' ] ,
357+ KeyCode :: Right => vec ! [ 0x1b , b'[' , b'C' ] ,
358+ KeyCode :: Left => vec ! [ 0x1b , b'[' , b'D' ] ,
359+ KeyCode :: Home => vec ! [ 0x1b , b'[' , b'H' ] ,
360+ KeyCode :: End => vec ! [ 0x1b , b'[' , b'F' ] ,
361+ KeyCode :: Delete => vec ! [ 0x1b , b'[' , b'3' , b'~' ] ,
362+ _ => continue ,
363+ }
364+ }
365+ Event :: Paste ( text) => text. into_bytes ( ) ,
366+ _ => continue ,
367+ } ;
368+
369+ let rt = tokio:: runtime:: Handle :: current ( ) ;
370+ let _ = rt. block_on ( stdin_client. write_exec_stdin (
371+ runtime_v2:: WriteExecStdinRequest {
372+ execution_id : exec_id. clone ( ) ,
373+ data : bytes,
374+ metadata : None ,
375+ } ,
376+ ) ) ;
377+ }
378+ } ) )
379+ } else {
380+ None
381+ } ;
299382
300383 let mut stream = client
301384 . stream_exec_output ( runtime_v2:: StreamExecOutputRequest {
@@ -317,21 +400,31 @@ pub async fn cmd_run(args: DevRunArgs) -> anyhow::Result<()> {
317400 let _ = std:: io:: stdout ( ) . flush ( ) ;
318401 }
319402 Some ( runtime_v2:: exec_output_event:: Payload :: Stderr ( bytes) ) => {
320- // Filter the harmless getcwd() warning from the shell.
321- // The kernel's getcwd() syscall fails with stacked
322- // overlay+VirtioFS mounts but CWD is actually correct.
323403 write_filtered_stderr ( & bytes) ;
324404 }
325405 Some ( runtime_v2:: exec_output_event:: Payload :: ExitCode ( code) ) => {
326406 exit_code = code;
327407 }
328408 Some ( runtime_v2:: exec_output_event:: Payload :: Error ( error) ) => {
409+ if args. interactive {
410+ stdin_stop. store ( true , Ordering :: Relaxed ) ;
411+ let _ = terminal:: disable_raw_mode ( ) ;
412+ }
329413 bail ! ( "execution error: {error}" ) ;
330414 }
331415 None => { }
332416 }
333417 }
334418
419+ // Clean up interactive mode.
420+ if args. interactive {
421+ stdin_stop. store ( true , Ordering :: Relaxed ) ;
422+ let _ = terminal:: disable_raw_mode ( ) ;
423+ if let Some ( handle) = stdin_handle {
424+ let _ = tokio:: time:: timeout ( std:: time:: Duration :: from_secs ( 2 ) , handle) . await ;
425+ }
426+ }
427+
335428 if exit_code != 0 {
336429 std:: process:: exit ( exit_code) ;
337430 }
@@ -704,7 +797,9 @@ async fn exec_streaming(
704797 "-c" . to_string( ) ,
705798 format!( "cd / && {command}" ) ,
706799 ] ,
707- env_override : HashMap :: new ( ) ,
800+ env_override : HashMap :: from ( [
801+ ( "HOME" . to_string ( ) , "/root" . to_string ( ) ) ,
802+ ] ) ,
708803 timeout_secs : 3600 ,
709804 pty_mode : runtime_v2:: create_execution_request:: PtyMode :: Disabled as i32 ,
710805 } )
@@ -731,8 +826,7 @@ async fn exec_streaming(
731826 let _ = std:: io:: stdout ( ) . flush ( ) ;
732827 }
733828 Some ( runtime_v2:: exec_output_event:: Payload :: Stderr ( bytes) ) => {
734- let _ = std:: io:: stderr ( ) . write_all ( & bytes) ;
735- let _ = std:: io:: stderr ( ) . flush ( ) ;
829+ write_filtered_stderr ( & bytes) ;
736830 }
737831 Some ( runtime_v2:: exec_output_event:: Payload :: ExitCode ( code) ) => {
738832 exit_code = Some ( code) ;
0 commit comments