@@ -722,6 +722,148 @@ impl CallableTrait for AgentHistoryCommand {
722722 }
723723}
724724
725+ // ── Install (deploy Status Panel to existing server) ─
726+
727+ /// `stacker agent install [--file <path>] [--json]`
728+ ///
729+ /// Deploys the Status Panel agent to an existing server that was previously
730+ /// deployed without it. Reads the project identity from stacker.yml, finds
731+ /// the corresponding project and server on the Stacker API, and triggers
732+ /// a deploy with only the statuspanel feature enabled.
733+ pub struct AgentInstallCommand {
734+ pub file : Option < String > ,
735+ pub json : bool ,
736+ }
737+
738+ impl AgentInstallCommand {
739+ pub fn new ( file : Option < String > , json : bool ) -> Self {
740+ Self { file, json }
741+ }
742+ }
743+
744+ impl CallableTrait for AgentInstallCommand {
745+ fn call ( & self ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
746+ use crate :: cli:: stacker_client:: { self , DEFAULT_VAULT_URL } ;
747+
748+ let project_dir = std:: env:: current_dir ( ) . map_err ( CliError :: Io ) ?;
749+ let config_path = match & self . file {
750+ Some ( f) => project_dir. join ( f) ,
751+ None => project_dir. join ( "stacker.yml" ) ,
752+ } ;
753+
754+ let config = crate :: cli:: config_parser:: StackerConfig :: from_file ( & config_path) ?;
755+
756+ let project_name = config
757+ . project
758+ . identity
759+ . clone ( )
760+ . unwrap_or_else ( || config. name . clone ( ) ) ;
761+
762+ let ctx = CliRuntime :: new ( "agent install" ) ?;
763+ let pb = progress:: spinner ( "Installing Status Panel agent" ) ;
764+
765+ let result: Result < stacker_client:: DeployResponse , CliError > = ctx. block_on ( async {
766+ // 1. Find the project
767+ progress:: update_message ( & pb, "Finding project..." ) ;
768+ let project = ctx
769+ . client
770+ . find_project_by_name ( & project_name)
771+ . await ?
772+ . ok_or_else ( || CliError :: ConfigValidation ( format ! (
773+ "Project '{}' not found on the Stacker server.\n \
774+ Deploy the project first with: stacker deploy --target cloud",
775+ project_name
776+ ) ) ) ?;
777+
778+ // 2. Find the server for this project
779+ progress:: update_message ( & pb, "Finding server..." ) ;
780+ let servers = ctx. client . list_servers ( ) . await ?;
781+ let server = servers
782+ . into_iter ( )
783+ . find ( |s| s. project_id == project. id )
784+ . ok_or_else ( || CliError :: ConfigValidation ( format ! (
785+ "No server found for project '{}' (id={}).\n \
786+ Deploy the project first with: stacker deploy --target cloud",
787+ project_name, project. id
788+ ) ) ) ?;
789+
790+ let cloud_id = server. cloud_id . ok_or_else ( || CliError :: ConfigValidation (
791+ "Server has no associated cloud credentials.\n \
792+ Cannot install Status Panel without cloud credentials."
793+ . to_string ( ) ,
794+ ) ) ?;
795+
796+ // 3. Build a minimal deploy form with only the statuspanel feature
797+ progress:: update_message ( & pb, "Preparing deploy payload..." ) ;
798+ let vault_url = std:: env:: var ( "STACKER_VAULT_URL" )
799+ . unwrap_or_else ( |_| DEFAULT_VAULT_URL . to_string ( ) ) ;
800+
801+ let deploy_form = serde_json:: json!( {
802+ "cloud" : {
803+ "provider" : server. cloud. clone( ) . unwrap_or_else( || "htz" . to_string( ) ) ,
804+ "save_token" : true ,
805+ } ,
806+ "server" : {
807+ "server_id" : server. id,
808+ "region" : server. region,
809+ "server" : server. server,
810+ "os" : server. os,
811+ "name" : server. name,
812+ "srv_ip" : server. srv_ip,
813+ "ssh_user" : server. ssh_user,
814+ "ssh_port" : server. ssh_port,
815+ "vault_key_path" : server. vault_key_path,
816+ "connection_mode" : "status_panel" ,
817+ } ,
818+ "stack" : {
819+ "stack_code" : project_name,
820+ "vars" : [
821+ { "key" : "vault_url" , "value" : vault_url } ,
822+ { "key" : "status_panel_port" , "value" : "5000" } ,
823+ ] ,
824+ "integrated_features" : [ "statuspanel" ] ,
825+ "extended_features" : [ ] ,
826+ "subscriptions" : [ ] ,
827+ } ,
828+ } ) ;
829+
830+ // 4. Trigger the deploy
831+ progress:: update_message ( & pb, "Deploying Status Panel..." ) ;
832+ let resp = ctx. client . deploy ( project. id , Some ( cloud_id) , deploy_form) . await ?;
833+ Ok ( resp)
834+ } ) ;
835+
836+ match result {
837+ Ok ( resp) => {
838+ progress:: finish_success ( & pb, "Status Panel agent installation triggered" ) ;
839+
840+ if self . json {
841+ println ! ( "{}" , serde_json:: to_string_pretty( & resp) . unwrap_or_default( ) ) ;
842+ } else {
843+ println ! ( "Status Panel deploy queued for project '{}'" , project_name) ;
844+ if let Some ( id) = resp. id {
845+ println ! ( "Project ID: {}" , id) ;
846+ }
847+ if let Some ( meta) = & resp. meta {
848+ if let Some ( dep_id) = meta. get ( "deployment_id" ) {
849+ println ! ( "Deployment ID: {}" , dep_id) ;
850+ }
851+ }
852+ println ! ( ) ;
853+ println ! ( "The Status Panel agent will be installed on the server." ) ;
854+ println ! ( "Once ready, use `stacker agent status` to verify connectivity." ) ;
855+ }
856+ }
857+ Err ( e) => {
858+ progress:: finish_error ( & pb, & format ! ( "Install failed: {}" , e) ) ;
859+ return Err ( Box :: new ( e) ) ;
860+ }
861+ }
862+
863+ Ok ( ( ) )
864+ }
865+ }
866+
725867// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
726868// Tests
727869// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
0 commit comments