@@ -6,6 +6,7 @@ use crate::cli::error::CliError;
66use crate :: cli:: install_runner:: { CommandExecutor , CommandOutput , ShellExecutor } ;
77use crate :: cli:: local_compose:: resolve_local_compose_path;
88use crate :: cli:: stacker_client:: { self , DeploymentStatusInfo , ServerInfo , StackerClient } ;
9+ use crate :: console:: commands:: cli:: ssh_key:: { format_ssh_command, local_backup_private_key_path} ;
910use crate :: console:: commands:: CallableTrait ;
1011
1112const DEFAULT_CONFIG_FILE : & str = "stacker.yml" ;
@@ -79,6 +80,23 @@ struct StatusContext<'a> {
7980 live_containers : Option < & ' a [ serde_json:: Value ] > ,
8081}
8182
83+ fn emergency_ssh_command ( server : & ServerInfo ) -> Option < String > {
84+ let ip = server. srv_ip . as_deref ( ) ?;
85+ let private_key_path = local_backup_private_key_path ( server. id ) ;
86+ if !private_key_path. exists ( ) {
87+ return None ;
88+ }
89+
90+ let ssh_user = server. ssh_user . as_deref ( ) . unwrap_or ( "root" ) ;
91+ let ssh_port = server. ssh_port . unwrap_or ( 22 ) as u16 ;
92+ Some ( format_ssh_command (
93+ & private_key_path,
94+ ssh_user,
95+ ip,
96+ ssh_port,
97+ ) )
98+ }
99+
82100/// Pretty-print a deployment status with optional server/config context.
83101fn print_deployment_status_rich ( info : & DeploymentStatusInfo , json : bool , ctx : & StatusContext < ' _ > ) {
84102 if json {
@@ -120,7 +138,9 @@ fn print_deployment_status_rich(info: &DeploymentStatusInfo, json: bool, ctx: &S
120138 if let Some ( srv) = ctx. server {
121139 println ! ( "\n ── Server ─────────────────────────────────" ) ;
122140 if let Some ( ref name) = srv. name {
123- println ! ( " Name: {}" , name) ;
141+ println ! ( " Name: {} (id={})" , name, srv. id) ;
142+ } else {
143+ println ! ( " ID: {}" , srv. id) ;
124144 }
125145 if let Some ( ref ip) = srv. srv_ip {
126146 println ! ( " IP: {}" , ip) ;
@@ -138,6 +158,9 @@ fn print_deployment_status_rich(info: &DeploymentStatusInfo, json: bool, ctx: &S
138158 if let Some ( ref region) = srv. region {
139159 println ! ( " Region: {}" , region) ;
140160 }
161+ if let Some ( command) = emergency_ssh_command ( srv) {
162+ println ! ( " Emergency SSH: {}" , command) ;
163+ }
141164 }
142165
143166 if let Some ( containers) = ctx. live_containers {
@@ -216,7 +239,9 @@ fn print_deployment_status_rich(info: &DeploymentStatusInfo, json: bool, ctx: &S
216239 config. deploy. target
217240 ) ;
218241 println ! ( "\n ── Documentation ──────────────────────────" ) ;
219- println ! ( " https://try.direct/docs" ) ;
242+ println ! (
243+ " https://github.com/trydirect/stacker/blob/main/docs/STACKER_YML_REFERENCE.md"
244+ ) ;
220245 }
221246 }
222247
@@ -555,6 +580,7 @@ impl CallableTrait for StatusCommand {
555580mod tests {
556581 use super :: * ;
557582 use crate :: cli:: deployment_lock:: DeploymentLock ;
583+ use crate :: cli:: stacker_client:: ServerInfo ;
558584 use chrono:: { Duration , Utc } ;
559585
560586 #[ test]
@@ -789,6 +815,70 @@ deploy:
789815 assert_eq ! ( resolve_stacker_base_url( & creds) , "https://api.try.direct" ) ;
790816 }
791817
818+ #[ test]
819+ fn test_emergency_ssh_command_uses_local_backup_key_when_present ( ) {
820+ let temp_home = tempfile:: TempDir :: new ( ) . unwrap ( ) ;
821+ std:: env:: set_var ( "XDG_CONFIG_HOME" , temp_home. path ( ) ) ;
822+
823+ let ssh_dir = temp_home. path ( ) . join ( "stacker/ssh" ) ;
824+ std:: fs:: create_dir_all ( & ssh_dir) . unwrap ( ) ;
825+ let private_key_path = ssh_dir. join ( "server-92_ed25519" ) ;
826+ std:: fs:: write ( & private_key_path, "PRIVATE KEY" ) . unwrap ( ) ;
827+
828+ let server = ServerInfo {
829+ id : 92 ,
830+ user_id : "user" . to_string ( ) ,
831+ project_id : 7 ,
832+ cloud_id : None ,
833+ cloud : Some ( "hetzner" . to_string ( ) ) ,
834+ region : Some ( "fsn1" . to_string ( ) ) ,
835+ zone : None ,
836+ server : Some ( "cx22" . to_string ( ) ) ,
837+ os : None ,
838+ disk_type : None ,
839+ srv_ip : Some ( "178.105.133.10" . to_string ( ) ) ,
840+ ssh_port : Some ( 22 ) ,
841+ ssh_user : Some ( "root" . to_string ( ) ) ,
842+ name : Some ( "status-web" . to_string ( ) ) ,
843+ vault_key_path : None ,
844+ connection_mode : "ssh" . to_string ( ) ,
845+ key_status : "active" . to_string ( ) ,
846+ } ;
847+
848+ let command = emergency_ssh_command ( & server) . expect ( "ssh command should be available" ) ;
849+ assert ! ( command. contains( "server-92_ed25519" ) ) ;
850+ assert ! ( command. contains( "root@178.105.133.10" ) ) ;
851+ assert ! ( command. contains( " -p 22 " ) ) ;
852+ }
853+
854+ #[ test]
855+ fn test_emergency_ssh_command_is_absent_without_local_backup_key ( ) {
856+ let temp_home = tempfile:: TempDir :: new ( ) . unwrap ( ) ;
857+ std:: env:: set_var ( "XDG_CONFIG_HOME" , temp_home. path ( ) ) ;
858+
859+ let server = ServerInfo {
860+ id : 93 ,
861+ user_id : "user" . to_string ( ) ,
862+ project_id : 7 ,
863+ cloud_id : None ,
864+ cloud : Some ( "hetzner" . to_string ( ) ) ,
865+ region : Some ( "fsn1" . to_string ( ) ) ,
866+ zone : None ,
867+ server : Some ( "cx22" . to_string ( ) ) ,
868+ os : None ,
869+ disk_type : None ,
870+ srv_ip : Some ( "178.105.133.11" . to_string ( ) ) ,
871+ ssh_port : Some ( 22 ) ,
872+ ssh_user : Some ( "root" . to_string ( ) ) ,
873+ name : Some ( "status-web" . to_string ( ) ) ,
874+ vault_key_path : None ,
875+ connection_mode : "ssh" . to_string ( ) ,
876+ key_status : "active" . to_string ( ) ,
877+ } ;
878+
879+ assert ! ( emergency_ssh_command( & server) . is_none( ) ) ;
880+ }
881+
792882 #[ test]
793883 fn test_missing_remote_project_reason_mentions_active_stacker_api ( ) {
794884 let reason = missing_remote_project_reason (
0 commit comments