diff --git a/cli/src/main/scala/acab/devcon0/dtos/FederationMemberUpdateInputVariables.scala b/cli/src/main/scala/acab/devcon0/dtos/FederationMemberUpdateInputVariables.scala
index 9c5f287fd37fcfb32ba1b5308391cf428821fc3f..7580c214842fabe5a7b4c72cb1de1cbd037da03b 100644
--- a/cli/src/main/scala/acab/devcon0/dtos/FederationMemberUpdateInputVariables.scala
+++ b/cli/src/main/scala/acab/devcon0/dtos/FederationMemberUpdateInputVariables.scala
@@ -1,7 +1,3 @@
-package acab.devcon0.dtosFederationMemberUpdateInputVariables
-
-import acab.devcon0.dtos.TrileCli.Environment
-
-import java.net.InetAddress
+package acab.devcon0.dtos
 
 case class FederationMemberUpdateInputVariables(federationName: String, nickname: String)
diff --git a/cli/src/main/scala/acab/devcon0/program/commands/InstallFederationControllerCommand.scala b/cli/src/main/scala/acab/devcon0/program/commands/InstallFederationControllerCommand.scala
index e4ffd99dea21ef173ea3bf602ac6837e572f46ab..625e9932ad3433fa66a69b77773039075e64882e 100644
--- a/cli/src/main/scala/acab/devcon0/program/commands/InstallFederationControllerCommand.scala
+++ b/cli/src/main/scala/acab/devcon0/program/commands/InstallFederationControllerCommand.scala
@@ -3,9 +3,14 @@ package acab.devcon0.program.commands
 import acab.devcon0.dtos.FederationControllerInstallInputVariables
 import acab.devcon0.dtos.TrileCli.Environment
 import acab.devcon0.dtos.TrileCli.Environment.*
+import acab.devcon0.program.commands.InstallFederationMemberCommand.runtime
 import acab.devcon0.program.opts.{TrileOptsFederation, TrileOptsFederationController}
-import acab.devcon0.services.command.installer.InstallControllerCommandHandler
+import acab.devcon0.services.command.install.{InstallControllerCommandHandler, InstallMemberCommandHandler}
+import acab.devcon0.services.shell.ShellExecutor.attemptTapPrintProgressFinal
 import acab.devcon0.services.shell.{Clear, PrintLn}
+import cats.effect.IO
+import cats.effect.std.Supervisor
+import cats.effect.unsafe.IORuntime
 import cats.implicits.*
 import com.monovore.decline.*
 
@@ -13,6 +18,8 @@ import scala.io.StdIn
 
 object InstallFederationControllerCommand {
 
+  private implicit val runtime: IORuntime = cats.effect.unsafe.IORuntime.global
+
   def apply(): Command[Unit] =
     Command(name = "controller", header = "Run the installation for a controller.") {
       (
@@ -44,7 +51,7 @@ object InstallFederationControllerCommand {
               environment,
               federationName,
               networkAddress,
-              generateSSL = certbotFolderAbsolutePathOptional.isEmpty,
+              generateSSL = environment.equals(Environment.Production) && certbotFolderAbsolutePathOptional.isEmpty,
               stagingSSL = if sslStaging then "1" else "0",
               certbotFolderAbsolutePathOptional = certbotFolderAbsolutePathOptional,
               ipfsClusterReplicaFactorMax,
@@ -65,11 +72,17 @@ object InstallFederationControllerCommand {
   private def triggerInstallation(
       federationControllerInstallInputVariables: FederationControllerInstallInputVariables
   ): Unit = {
-    println("")
-    println("Creating the configuration & boot the containers, good luck!")
-    InstallControllerCommandHandler(federationControllerInstallInputVariables)
-    println("")
-    PrintLn.green("Installation completed. Enjoy.")
+    Supervisor[IO](await = true)
+      .use(supervisor => {
+        for
+          _ <- IO(println("Up to install the controller, good luck!"))
+          _ <- IO(println())
+          _ <- InstallControllerCommandHandler(federationControllerInstallInputVariables)
+        yield ()
+      })
+      .attemptTap(attemptTapPrintProgressFinal("Controller installation", _))
+      .unsafeToFuture()(runtime)
+      .wait()
   }
 
   private def printInputVariables(inputVariables: FederationControllerInstallInputVariables): Unit = {
diff --git a/cli/src/main/scala/acab/devcon0/program/commands/InstallFederationMemberCommand.scala b/cli/src/main/scala/acab/devcon0/program/commands/InstallFederationMemberCommand.scala
index 2a1f82bccc6e7eb382f9d78dc68b4c296fb6ee6e..3d8729c8d0731003ade86e56082a2d3aa6aa5b70 100644
--- a/cli/src/main/scala/acab/devcon0/program/commands/InstallFederationMemberCommand.scala
+++ b/cli/src/main/scala/acab/devcon0/program/commands/InstallFederationMemberCommand.scala
@@ -4,10 +4,16 @@ import acab.devcon0.dtos.FederationMemberInstallInputVariables
 import acab.devcon0.dtos.TrileCli.Environment
 import acab.devcon0.dtos.TrileCli.Environment.*
 import acab.devcon0.dtos.TrileCli.EnvironmentVariables.*
+import acab.devcon0.program.commands.UpdateAllCommand.runtime
 import acab.devcon0.program.opts.{TrileOptsFederation, TrileOptsFederationMember}
 import acab.devcon0.services.EnvVarsFileLoader
-import acab.devcon0.services.command.installer.InstallMemberCommandHandler
+import acab.devcon0.services.command.install.InstallMemberCommandHandler
+import acab.devcon0.services.command.update.UpdateFederationCommandHandler
+import acab.devcon0.services.shell.ShellExecutor.attemptTapPrintProgressFinal
 import acab.devcon0.services.shell.{Clear, PrintLn}
+import cats.effect.IO
+import cats.effect.std.Supervisor
+import cats.effect.unsafe.IORuntime
 import cats.implicits.*
 import com.monovore.decline.*
 
@@ -16,6 +22,8 @@ import scala.util.Try
 
 object InstallFederationMemberCommand {
 
+  private implicit val runtime: IORuntime = cats.effect.unsafe.IORuntime.global
+
   def apply(): Command[Unit] =
     Command(name = "member", header = "Run the installation for a member.") {
       (
@@ -66,14 +74,20 @@ object InstallFederationMemberCommand {
       fedvarsMap: TrileFederationEnvVars,
       federationMemberInstallInputVariables: FederationMemberInstallInputVariables
   ): Unit = {
-    println("")
-    println("Create the configuration & boot the containers, good luck!")
-    InstallMemberCommandHandler(
-      inputVariables = federationMemberInstallInputVariables,
-      fedvarsAsMap = fedvarsMap
-    )
-    println("")
-    PrintLn.green("Installation completed. Enjoy.")
+    Supervisor[IO](await = true)
+      .use(supervisor => {
+        for
+          _ <- IO(println("Up to install the member, good luck!"))
+          _ <- IO(println())
+          _ <- InstallMemberCommandHandler(
+            inputVariables = federationMemberInstallInputVariables,
+            fedvarsAsMap = fedvarsMap
+          )
+        yield ()
+      })
+      .attemptTap(attemptTapPrintProgressFinal("Member installation", _))
+      .unsafeToFuture()(runtime)
+      .wait()
   }
 
   private def printInputVariables(inputVariables: FederationMemberInstallInputVariables): Unit = {
diff --git a/cli/src/main/scala/acab/devcon0/program/commands/UpdateAllCommand.scala b/cli/src/main/scala/acab/devcon0/program/commands/UpdateAllCommand.scala
index 03509a32f57c9b4108cfe2ae6da27081b7744024..46ba344176182e9f200ae7c960a2c635b16df5fe 100644
--- a/cli/src/main/scala/acab/devcon0/program/commands/UpdateAllCommand.scala
+++ b/cli/src/main/scala/acab/devcon0/program/commands/UpdateAllCommand.scala
@@ -2,15 +2,21 @@ package acab.devcon0.program.commands
 
 import acab.devcon0.dtos.FederationUpdateInputVariables
 import acab.devcon0.program.opts.{TrileOptsFederation, TrileOptsFederationController, TrileOptsFederationMember}
-import acab.devcon0.services.command.installer.UpdateFederationCommandHandler
-import acab.devcon0.services.shell.{Clear, PrintLn}
-import cats.implicits.catsSyntaxTuple2Semigroupal
+import acab.devcon0.services.command.update.UpdateFederationCommandHandler
+import acab.devcon0.services.shell.ShellExecutor.attemptTapPrintProgressFinal
+import acab.devcon0.services.shell.{Clear, PrintLn, ShellExecutor}
+import cats.effect.IO
+import cats.effect.std.Supervisor
+import cats.effect.unsafe.IORuntime
+import cats.implicits.{catsSyntaxMonadError, catsSyntaxTuple2Semigroupal}
 import com.monovore.decline.*
 
 import scala.io.StdIn
 
 object UpdateAllCommand {
 
+  private implicit val runtime: IORuntime = cats.effect.unsafe.IORuntime.global
+
   def apply(): Command[Unit] =
     Command(name = "all", header = "Updates controllers & members present in the machine.") {
       (
@@ -37,12 +43,18 @@ object UpdateAllCommand {
     val inputVariables: FederationUpdateInputVariables = FederationUpdateInputVariables(
       federationName = federationName
     )
-    printInputVariables(inputVariables)
-    println("")
-    println("Up to update the controller and every member, good luck!")
-    UpdateFederationCommandHandler(inputVariables)
-    println("")
-    PrintLn.green("Update completed. Enjoy.")
+
+    Supervisor[IO](await = true)
+      .use(supervisor => {
+        for
+          _ <- IO(println("Up to update the controller and every member, good luck!"))
+          _ <- IO(println())
+          _ <- UpdateFederationCommandHandler(inputVariables)
+        yield ()
+      })
+      .attemptTap(attemptTapPrintProgressFinal("Federation update", _))
+      .unsafeToFuture()(runtime)
+      .wait()
   }
 
   private def printInputVariables(inputVariables: FederationUpdateInputVariables): Unit = {
@@ -55,10 +67,7 @@ object UpdateAllCommand {
   private def greet(): Unit = {
     Clear()
     PrintLn.cyan(s"Welcome to trile! You are up to update a federation.")
-    PrintLn.cyan(
-      s"You can pass your values as arguments. use 'update controller all --help' to see the list. " +
-        s"In order to complete the installation, all values need to be provided."
-    )
+    PrintLn.cyan("Check 'update controller --help' to see the list of options.")
   }
 
   private def readConfirmation(): Boolean = {
diff --git a/cli/src/main/scala/acab/devcon0/program/commands/UpdateFederationControllerCommand.scala b/cli/src/main/scala/acab/devcon0/program/commands/UpdateFederationControllerCommand.scala
index 5a2aa4182ad46efde94aa5576627314367ee5532..58c765c6c1bfe7d0d576bf9a999eac50a4dc952c 100644
--- a/cli/src/main/scala/acab/devcon0/program/commands/UpdateFederationControllerCommand.scala
+++ b/cli/src/main/scala/acab/devcon0/program/commands/UpdateFederationControllerCommand.scala
@@ -1,16 +1,21 @@
 package acab.devcon0.program.commands
 
 import acab.devcon0.dtos.FederationControllerUpdateInputVariables
-import acab.devcon0.program.opts.{TrileOptsFederation, TrileOptsFederationController, TrileOptsFederationMember}
-import acab.devcon0.services.command.installer.UpdateControllerCommandHandler
-import acab.devcon0.services.shell.{Clear, PrintLn}
-import cats.implicits.catsSyntaxTuple2Semigroupal
+import acab.devcon0.program.opts.TrileOptsFederation
+import acab.devcon0.services.command.update.UpdateControllerCommandHandler
+import acab.devcon0.services.shell.{Clear, PrintLn, ShellExecutor}
+import cats.effect.IO
+import cats.effect.std.Supervisor
+import cats.effect.unsafe.IORuntime
+import cats.implicits.{catsSyntaxMonadError, catsSyntaxTuple2Semigroupal}
 import com.monovore.decline.*
 
 import scala.io.StdIn
 
 object UpdateFederationControllerCommand {
 
+  private implicit val runtime: IORuntime = cats.effect.unsafe.IORuntime.global
+
   def apply(): Command[Unit] =
     Command(name = "controller", header = "Updates a controller installation.") {
       (
@@ -39,11 +44,17 @@ object UpdateFederationControllerCommand {
   }
 
   private def triggerUpdate(inputVariables: FederationControllerUpdateInputVariables): Unit = {
-    println("")
-    println("Updating the configuration & boot the containers, good luck!")
-    UpdateControllerCommandHandler(inputVariables)
-    println("")
-    PrintLn.green("Update completed. Enjoy.")
+    Supervisor[IO](await = true)
+      .use(supervisor => {
+        for
+          _ <- IO(println("Update is up to start. Good luck!"))
+          _ <- IO(println())
+          _ <- UpdateControllerCommandHandler(inputVariables)
+        yield ()
+      })
+      .attemptTap(ShellExecutor.attemptTapPrintProgressFinal("Controller update", _))
+      .unsafeToFuture()(runtime)
+      .wait()
   }
 
   private def printInputVariables(inputVariables: FederationControllerUpdateInputVariables): Unit = {
@@ -56,10 +67,7 @@ object UpdateFederationControllerCommand {
   private def greet(): Unit = {
     Clear()
     PrintLn.cyan(s"Welcome to trile! You are up to update a federation controller.")
-    PrintLn.cyan(
-      s"You can pass your values as arguments. use 'update controller --help' to see the list. " +
-        s"In order to complete the installation, all values need to be provided."
-    )
+    PrintLn.cyan("Check 'update controller --help' to see the list of options.")
   }
 
   private def readConfirmation(): Boolean = {
diff --git a/cli/src/main/scala/acab/devcon0/program/commands/UpdateFederationMemberCommand.scala b/cli/src/main/scala/acab/devcon0/program/commands/UpdateFederationMemberCommand.scala
index cc755edf032a9be2368a2379550f5b161187b0e1..09fa9a5b8b77b83d4087a9aa09350ee228c58015 100644
--- a/cli/src/main/scala/acab/devcon0/program/commands/UpdateFederationMemberCommand.scala
+++ b/cli/src/main/scala/acab/devcon0/program/commands/UpdateFederationMemberCommand.scala
@@ -1,10 +1,12 @@
 package acab.devcon0.program.commands
 
-import acab.devcon0.dtosFederationMemberUpdateInputVariables.FederationMemberUpdateInputVariables
-import acab.devcon0.program.commands.UpdateFederationControllerCommand.{readConfirmation, triggerUpdate}
+import acab.devcon0.dtos.FederationMemberUpdateInputVariables
 import acab.devcon0.program.opts.{TrileOptsFederation, TrileOptsFederationMember}
-import acab.devcon0.services.command.installer.UpdateMemberCommandHandler
-import acab.devcon0.services.shell.{Clear, PrintLn}
+import acab.devcon0.services.command.update.UpdateMemberCommandHandler
+import acab.devcon0.services.shell.{Clear, PrintLn, ShellExecutor}
+import cats.effect.IO
+import cats.effect.std.Supervisor
+import cats.effect.unsafe.IORuntime
 import cats.implicits.catsSyntaxTuple3Semigroupal
 import com.monovore.decline.*
 
@@ -12,6 +14,8 @@ import scala.io.StdIn
 
 object UpdateFederationMemberCommand {
 
+  private implicit val runtime: IORuntime = cats.effect.unsafe.IORuntime.global
+
   def apply(): Command[Unit] =
     Command(name = "member", header = "Updates a member installation.") {
       (
@@ -43,16 +47,24 @@ object UpdateFederationMemberCommand {
   }
 
   private def triggerUpdate(inputVariables: FederationMemberUpdateInputVariables): Unit = {
-    println("")
-    println("Updating the configuration & boot the containers, good luck!")
-    UpdateMemberCommandHandler(inputVariables)
-    println("")
-    PrintLn.green("Update completed. Enjoy.")
+    Supervisor[IO](await = true)
+      .use(supervisor => {
+        for
+          _ <- IO(println("Update is up to start. Good luck!"))
+          _ <- IO(println())
+          _ <- UpdateMemberCommandHandler(inputVariables)
+          _ <- IO(println())
+          _ <- ShellExecutor.printProgressOk("Update")
+        yield ()
+      })
+      .handleErrorWith(_ => ShellExecutor.printProgressError("Member update"))
+      .unsafeToFuture()(runtime)
+      .wait()
   }
 
   private def printInputVariables(inputVariables: FederationMemberUpdateInputVariables): Unit = {
     println()
-    println(s"These are you input values. Please review them: ")
+    println(s"These are you input values")
     println(s"- Federation name: ${inputVariables.federationName}")
     println(s"- Nickname: ${inputVariables.nickname}")
     println()
@@ -61,10 +73,7 @@ object UpdateFederationMemberCommand {
   private def greet(): Unit = {
     Clear()
     PrintLn.cyan(s"Welcome to trile! You are up to update a federation member.")
-    PrintLn.cyan(
-      s"You can pass your values as arguments. use 'update controller --help' to see the list. " +
-        s"In order to complete the installation, all values need to be provided."
-    )
+    PrintLn.cyan("Check 'update controller --help' to see the list of options.")
   }
 
   private def readConfirmation(): Boolean = {
diff --git a/cli/src/main/scala/acab/devcon0/services/FederationControllerService.scala b/cli/src/main/scala/acab/devcon0/services/FederationControllerService.scala
index 00e005a9e524546afa92237712adf44fb1adabed..74d2072bbbeb8708d4652513ba5547d9e5d34646 100644
--- a/cli/src/main/scala/acab/devcon0/services/FederationControllerService.scala
+++ b/cli/src/main/scala/acab/devcon0/services/FederationControllerService.scala
@@ -3,7 +3,9 @@ package acab.devcon0.services
 import acab.devcon0.dtos.TrileCli
 import acab.devcon0.dtos.TrileCli.Environment
 import acab.devcon0.dtos.aliases.P2pPeerId
+import acab.devcon0.services.shell.ShellExecutor
 import cats.effect.IO
+import cats.implicits.catsSyntaxMonadError
 import sttp.client4.curl.CurlBackend
 import sttp.client4.{SyncBackend, UriContext, quickRequest}
 import sttp.model.{StatusCode, Uri}
@@ -17,6 +19,7 @@ object FederationControllerService {
   def getPeerId(environment: Environment, networkAddress: P2pPeerId): IO[P2pPeerId] = {
     val uri: Uri = getUri(environment, networkAddress)
     getP2pPeerIdInner(uri, 300)
+      .attemptTap(ShellExecutor.attemptTapPrintProgressFinal("Fetching P2P peer id", _))
   }
 
   private def getUri(environment: Environment, networkAddress: P2pPeerId): Uri = {
diff --git a/cli/src/main/scala/acab/devcon0/services/command/installer/InstallControllerCommandHandler.scala b/cli/src/main/scala/acab/devcon0/services/command/install/InstallControllerCommandHandler.scala
similarity index 83%
rename from cli/src/main/scala/acab/devcon0/services/command/installer/InstallControllerCommandHandler.scala
rename to cli/src/main/scala/acab/devcon0/services/command/install/InstallControllerCommandHandler.scala
index 72af4804245b9704cfe4b249ec65e3ae4390dd10..1b0e0f995d30584a9489a735bb6373f1e27fb93c 100644
--- a/cli/src/main/scala/acab/devcon0/services/command/installer/InstallControllerCommandHandler.scala
+++ b/cli/src/main/scala/acab/devcon0/services/command/install/InstallControllerCommandHandler.scala
@@ -1,4 +1,4 @@
-package acab.devcon0.services.command.installer
+package acab.devcon0.services.command.install
 
 import acab.devcon0.dtos.*
 import acab.devcon0.dtos.TrileCli.Component.*
@@ -8,7 +8,7 @@ import acab.devcon0.dtos.TrileCli.{Component, Defaults, Environment, File}
 import acab.devcon0.services.shell.*
 import acab.devcon0.services.{FederationControllerEnvironmentVariables, FederationControllerService}
 import acab.devcon0.{files, services}
-import cats.effect.unsafe.IORuntime
+import cats.effect.IO
 import ujson.Value.Value
 
 import java.nio.charset.StandardCharsets
@@ -17,29 +17,30 @@ import scala.io.Source
 
 object InstallControllerCommandHandler {
 
-  private implicit val runtime: IORuntime = cats.effect.unsafe.IORuntime.global
-
-  def apply(inputVariables: FederationControllerInstallInputVariables): Unit = {
+  def apply(inputVariables: FederationControllerInstallInputVariables): IO[Unit] = {
     val environmentVariables: TrileFederationEnvVars = FederationControllerEnvironmentVariables(inputVariables)
     val configurationFolderAbsolutePath              = getConfigurationFolderAbsolutePath(environmentVariables)
     val dockerComposeAbsolutePath                    = getDockerComposeAbsolutePath(configurationFolderAbsolutePath)
 
-    generateConfigurationFiles(inputVariables, environmentVariables)
-    DockerComposeUp(dockerComposeAbsolutePath)
-    if inputVariables.generateSSL then generateSslCertificate(environmentVariables)
-    generateConfigurationFilesPostSsl(inputVariables, environmentVariables)
-    DockerComposeUp(dockerComposeAbsolutePath)
-    generateFedVars(inputVariables, environmentVariables)
-    generateInstallationEnvVars(environmentVariables)
+    generateConfigurationFiles(inputVariables, environmentVariables) >>
+      DockerComposeUp(dockerComposeAbsolutePath) >>
+      IO.unit.flatMap(_ =>
+        if inputVariables.generateSSL then generateSslCertificate(environmentVariables)
+        else IO.unit
+      ) >>
+      generateConfigurationFilesPostSsl(inputVariables, environmentVariables) >>
+      DockerComposeUp(dockerComposeAbsolutePath) >>
+      generateFedVars(inputVariables, environmentVariables) >>
+      generateInstallationEnvVars(environmentVariables)
   }
 
   private def generateFedVars(
       inputVariables: FederationControllerInstallInputVariables,
       environmentVariablesMap: TrileFederationEnvVars
-  ): Unit = {
+  ): IO[Unit] = {
     val networkAddress: String   = environmentVariablesMap(TFC_NETWORK_ADDRESS)
     val environment: Environment = inputVariables.environment
-    (for peerId <- FederationControllerService.getPeerId(environment, networkAddress)
+    for peerId <- FederationControllerService.getPeerId(environment, networkAddress)
     yield {
       val configurationFolderAbsolutePath = getConfigurationFolderAbsolutePath(environmentVariablesMap)
       val fedvarsAbsolutePath             = s"$configurationFolderAbsolutePath/.fedvars"
@@ -59,10 +60,10 @@ object InstallControllerCommandHandler {
 
       val fedVarsContent: String = map.view.toList.map { case (key, value) => s"$key=$value" }.mkString("\n")
       Files.write(Paths.get(fedvarsAbsolutePath), fedVarsContent.getBytes(StandardCharsets.UTF_8))
-    }).unsafeToFuture()(runtime).wait()
+    }
   }
 
-  private def generateInstallationEnvVars(envVars: TrileFederationEnvVars): Unit = {
+  private def generateInstallationEnvVars(envVars: TrileFederationEnvVars): IO[Unit] = IO {
     val configurationFolderAbsolutePath = getConfigurationFolderAbsolutePath(envVars)
     val installationEnvVarsAbsolutePath = s"$configurationFolderAbsolutePath/.installationEnvVars"
 
@@ -117,7 +118,7 @@ object InstallControllerCommandHandler {
   private def generateConfigurationFiles(
       inputVariables: FederationControllerInstallInputVariables,
       envVars: TrileFederationEnvVars
-  ): Unit = {
+  ): IO[Unit] = {
     val environment                                = inputVariables.environment
     val configFolderAbsolutePath                   = getConfigurationFolderAbsolutePath(envVars)
     val ipfsConfigurationFolderAbsolutePath        = getIpfsConfigurationFolderAbsolutePath(envVars)
@@ -135,30 +136,30 @@ object InstallControllerCommandHandler {
     val nginxPreSslConfTemplateContent         = files.Loader.get(NginxPreSslConf, FederationController, environment)
     val sslCertificateGeneratorTemplateContent = files.Loader.get(SslCertificate, FederationController, environment)
 
-    SudoRm(configFolderAbsolutePath)
-    MkdirP(configFolderAbsolutePath)
-    MkdirP(certbotConfigurationFolderAbsolutePath)
-    MkdirP(nginxFolderAbsolutePath)
-    MkdirP(ipfsConfigurationFolderAbsolutePath)
-    MkdirP(ipfsClusterConfigurationFolderAbsolutePath)
-    IpfsGenerateSwarmKey(swarmKeyAbsolutePath)
-
-    val envVarsPreBoot = envVars
-      .updated(TFC_REVERSE_PROXY_CONFIGURATION, nginxPreSslConfigurationFileAbsolutePath)
-      .updated(TRILE_FEDERATION_IPFS_SWARM_KEY_VALUE, getIpfsSwarmKeyValue(envVars))
-
-    EnvSust(envVars, dockerComposeTemplateContent, dockerComposeAbsolutePath)
-    EnvSust(envVars, nginxConfTemplateContent, nginxConfigurationFileAbsolutePath)
-    EnvSust(envVars, nginxPreSslConfTemplateContent, nginxPreSslConfigurationFileAbsolutePath)
-    EnvSust(envVars, sslCertificateGeneratorTemplateContent, sslCertificateGeneratorFileAbsolutePath)
-    EnvSust(envVarsPreBoot, dockerComposeTemplateContent, dockerComposeAbsolutePath)
-    ChmodX(sslCertificateGeneratorFileAbsolutePath)
+    for
+      _ <- SudoRm(configFolderAbsolutePath)
+      _ <- MkdirP(configFolderAbsolutePath)
+      _ <- MkdirP(certbotConfigurationFolderAbsolutePath)
+      _ <- MkdirP(nginxFolderAbsolutePath)
+      _ <- MkdirP(ipfsConfigurationFolderAbsolutePath)
+      _ <- MkdirP(ipfsClusterConfigurationFolderAbsolutePath)
+      _ <- IpfsGenerateSwarmKey(swarmKeyAbsolutePath)
+      envVarsPreBoot = envVars
+        .updated(TFC_REVERSE_PROXY_CONFIGURATION, nginxPreSslConfigurationFileAbsolutePath)
+        .updated(TRILE_FEDERATION_IPFS_SWARM_KEY_VALUE, getIpfsSwarmKeyValue(envVars))
+      _ <- EnvSust(envVars, dockerComposeTemplateContent, dockerComposeAbsolutePath)
+      _ <- EnvSust(envVars, nginxConfTemplateContent, nginxConfigurationFileAbsolutePath)
+      _ <- EnvSust(envVars, nginxPreSslConfTemplateContent, nginxPreSslConfigurationFileAbsolutePath)
+      _ <- EnvSust(envVarsPreBoot, dockerComposeTemplateContent, dockerComposeAbsolutePath)
+      _ <- EnvSust(envVars, sslCertificateGeneratorTemplateContent, sslCertificateGeneratorFileAbsolutePath)
+      _ <- ChmodX(sslCertificateGeneratorFileAbsolutePath)
+    yield ()
   }
 
   private def generateConfigurationFilesPostSsl(
       inputVariables: FederationControllerInstallInputVariables,
       envVars: TrileFederationEnvVars
-  ): Unit = {
+  ): IO[Unit] = {
     val environment                     = inputVariables.environment
     val configurationFolderAbsolutePath = getConfigurationFolderAbsolutePath(envVars)
     val dockerComposeAbsolutePath       = getDockerComposeAbsolutePath(configurationFolderAbsolutePath)
@@ -168,7 +169,7 @@ object InstallControllerCommandHandler {
     EnvSust(envVarsPostSsl, dockerComposeTemplateContent, dockerComposeAbsolutePath)
   }
 
-  private def generateSslCertificate(envVars: TrileFederationEnvVars): Unit = {
+  private def generateSslCertificate(envVars: TrileFederationEnvVars): IO[Unit] = {
     val configurationFolderAbsolutePath         = getConfigurationFolderAbsolutePath(envVars)
     val sslCertificateGeneratorFileAbsolutePath = getSslCertificateAbsolutePath(configurationFolderAbsolutePath)
 
diff --git a/cli/src/main/scala/acab/devcon0/services/command/installer/InstallMemberCommandHandler.scala b/cli/src/main/scala/acab/devcon0/services/command/install/InstallMemberCommandHandler.scala
similarity index 81%
rename from cli/src/main/scala/acab/devcon0/services/command/installer/InstallMemberCommandHandler.scala
rename to cli/src/main/scala/acab/devcon0/services/command/install/InstallMemberCommandHandler.scala
index 81f8b2ec28f833fc00e10b648cb3599c922b84f1..e9e55edc90c45f6552ac155ffdf0f84b4b9d968f 100644
--- a/cli/src/main/scala/acab/devcon0/services/command/installer/InstallMemberCommandHandler.scala
+++ b/cli/src/main/scala/acab/devcon0/services/command/install/InstallMemberCommandHandler.scala
@@ -1,4 +1,4 @@
-package acab.devcon0.services.command.installer
+package acab.devcon0.services.command.install
 
 import acab.devcon0.dtos.TrileCli.Component.*
 import acab.devcon0.dtos.TrileCli.EnvironmentVariables.*
@@ -8,6 +8,7 @@ import acab.devcon0.dtos.{FederationMemberInstallInputVariables, TrileCli}
 import acab.devcon0.services.FederationMemberEnvironmentVariables
 import acab.devcon0.services.shell.{DockerComposeUp, EnvSust, MkdirP, SudoRm}
 import acab.devcon0.{files, services}
+import cats.effect.IO
 
 import java.nio.charset.StandardCharsets
 import java.nio.file.{Files, Paths}
@@ -17,7 +18,7 @@ object InstallMemberCommandHandler {
   def apply(
       inputVariables: FederationMemberInstallInputVariables,
       fedvarsAsMap: TrileFederationEnvVars
-  ): Unit = {
+  ): IO[Unit] = {
     val environmentVariables: TrileFederationEnvVars = FederationMemberEnvironmentVariables(
       inputVariables = inputVariables,
       fedvars = fedvarsAsMap
@@ -32,18 +33,18 @@ object InstallMemberCommandHandler {
     val swarmKeyTemplateContent                    = files.Loader.get(SwarmKey, FederationMember, environment)
     val dockerComposeTemplateContent               = files.Loader.get(DockerCompose, FederationMember, environment)
 
-    SudoRm(configurationFolderAbsolutePath)
-    MkdirP(configurationFolderAbsolutePath)
-    MkdirP(ipfsConfigurationFolderAbsolutePath)
-    MkdirP(ipfsClusterConfigurationFolderAbsolutePath)
-    MkdirP(federationMemberSharingFolder)
-    EnvSust(environmentVariables, swarmKeyTemplateContent, swarmKeyAbsolutePath)
-    EnvSust(environmentVariables, dockerComposeTemplateContent, dockerComposeAbsolutePath)
-    DockerComposeUp(dockerComposeAbsolutePath)
-    generateInstallationEnvVars(environmentVariables)
+    SudoRm(configurationFolderAbsolutePath) >>
+      MkdirP(configurationFolderAbsolutePath) >>
+      MkdirP(ipfsConfigurationFolderAbsolutePath) >>
+      MkdirP(ipfsClusterConfigurationFolderAbsolutePath) >>
+      MkdirP(federationMemberSharingFolder) >>
+      EnvSust(environmentVariables, swarmKeyTemplateContent, swarmKeyAbsolutePath) >>
+      EnvSust(environmentVariables, dockerComposeTemplateContent, dockerComposeAbsolutePath) >>
+      DockerComposeUp(dockerComposeAbsolutePath) >>
+      generateInstallationEnvVars(environmentVariables)
   }
 
-  private def generateInstallationEnvVars(envVars: TrileFederationEnvVars): Unit = {
+  private def generateInstallationEnvVars(envVars: TrileFederationEnvVars): IO[Unit] = IO {
     val configurationFolderAbsolutePath = getConfigurationFolderAbsolutePath(envVars)
     val installationEnvVarsAbsolutePath = s"$configurationFolderAbsolutePath/.installationEnvVars"
 
diff --git a/cli/src/main/scala/acab/devcon0/services/command/update/UpdateControllerCommandHandler.scala b/cli/src/main/scala/acab/devcon0/services/command/update/UpdateControllerCommandHandler.scala
index fa4ec147695611f07d17653ed9c66c552b507263..a3d61e03d112dee7111b4a9420849901e85a3ae5 100644
--- a/cli/src/main/scala/acab/devcon0/services/command/update/UpdateControllerCommandHandler.scala
+++ b/cli/src/main/scala/acab/devcon0/services/command/update/UpdateControllerCommandHandler.scala
@@ -1,4 +1,4 @@
-package acab.devcon0.services.command.installer
+package acab.devcon0.services.command.update
 
 import acab.devcon0.dtos.*
 import acab.devcon0.dtos.TrileCli.Component.*
@@ -8,11 +8,12 @@ import acab.devcon0.dtos.TrileCli.{Component, Defaults, Environment, File}
 import acab.devcon0.services.{EnvVarsFileLoader, PathSanitizer}
 import acab.devcon0.services.shell.{DockerComposeUp, EnvSust}
 import acab.devcon0.{files, services}
+import cats.effect.IO
 import ujson.Value.Value
 
 object UpdateControllerCommandHandler {
 
-  def apply(inputVariables: FederationControllerUpdateInputVariables): Unit = {
+  def apply(inputVariables: FederationControllerUpdateInputVariables): IO[Unit] = {
     val userHomeAbsolutePath: String = PathSanitizer.expandHome(Defaults.userHomeAbsolutePath)
     val configurationPath: String    = s"$userHomeAbsolutePath/.config/trile"
     val installationPath: String     = s"$configurationPath/federations/${inputVariables.federationName}/controller"
@@ -21,11 +22,11 @@ object UpdateControllerCommandHandler {
     val finalEnvVars: TrileFederationEnvVars         = environmentVariables ++ fedVars
     val dockerComposeAbsolutePath                    = getDockerComposeAbsolutePath(installationPath)
 
-    updateConfigurationFiles(finalEnvVars, installationPath)
-    DockerComposeUp(dockerComposeAbsolutePath)
+    updateConfigurationFiles(finalEnvVars, installationPath) >>
+      DockerComposeUp(dockerComposeAbsolutePath)
   }
 
-  private def updateConfigurationFiles(envVars: TrileFederationEnvVars, installationPath: String): Unit = {
+  private def updateConfigurationFiles(envVars: TrileFederationEnvVars, installationPath: String): IO[Unit] = {
     val environment                  = Environment.valueOf(envVars(TRILE_ENVIRONMENT))
     val dockerComposeAbsolutePath    = getDockerComposeAbsolutePath(installationPath)
     val dockerComposeTemplateContent = files.Loader.get(DockerCompose, FederationController, environment)
diff --git a/cli/src/main/scala/acab/devcon0/services/command/update/UpdateFederationCommandHandler.scala b/cli/src/main/scala/acab/devcon0/services/command/update/UpdateFederationCommandHandler.scala
index 6dcb1307e2b177db5c1ad58b2ee34746af6421fe..1799faf0e55490ad2e8ff82f9d46215544bb9646 100644
--- a/cli/src/main/scala/acab/devcon0/services/command/update/UpdateFederationCommandHandler.scala
+++ b/cli/src/main/scala/acab/devcon0/services/command/update/UpdateFederationCommandHandler.scala
@@ -1,13 +1,14 @@
-package acab.devcon0.services.command.installer
+package acab.devcon0.services.command.update
 
 import acab.devcon0.dtos.*
-import acab.devcon0.dtosFederationMemberUpdateInputVariables.FederationMemberUpdateInputVariables
 import acab.devcon0.services.FederationMemberNicknameLister
-import acab.devcon0.services.shell.PrintLn
+import acab.devcon0.services.shell.{PrintLn, ShellExecutor}
+import cats.Traverse.ops.toAllTraverseOps
+import cats.effect.IO
 
 object UpdateFederationCommandHandler {
 
-  def apply(inputVariables: FederationUpdateInputVariables): Unit = {
+  def apply(inputVariables: FederationUpdateInputVariables): IO[Unit] = {
 
     val federationName: String                  = inputVariables.federationName
     val federationMemberNicknames: List[String] = FederationMemberNicknameLister(federationName)
@@ -15,19 +16,24 @@ object UpdateFederationCommandHandler {
       federationName = federationName
     )
 
-    PrintLn.cyan("Updating the controller.")
-    UpdateControllerCommandHandler(controllerInputVariables)
-    PrintLn.green("Controller updated")
-
-    PrintLn.cyan(s"Updating the members ${federationMemberNicknames.mkString(" ")}")
-    federationMemberNicknames
-      .map(federationMemberNickname =>
-        FederationMemberUpdateInputVariables(
-          federationName = federationName,
-          nickname = federationMemberNickname
+    for
+      _ <- IO(PrintLn.cyan("Updating the controller."))
+      _ <- UpdateControllerCommandHandler(controllerInputVariables)
+      _ <- ShellExecutor.printProgressOk("Update controller")
+      _ <- IO(println())
+      _ <- federationMemberNicknames
+        .map(federationMemberNickname =>
+          FederationMemberUpdateInputVariables(
+            federationName = federationName,
+            nickname = federationMemberNickname
+          )
         )
-      )
-      .foreach(UpdateMemberCommandHandler(_))
-    PrintLn.green("Members updated")
+        .traverse(inputVariables => {
+          IO(PrintLn.cyan(s"Updating member ${inputVariables.nickname}")) >>
+            UpdateMemberCommandHandler(inputVariables) >>
+            ShellExecutor.printProgressOk(s"Update member ${inputVariables.nickname}") >>
+            IO(println())
+        })
+    yield ()
   }
 }
diff --git a/cli/src/main/scala/acab/devcon0/services/command/update/UpdateMemberCommandHandler.scala b/cli/src/main/scala/acab/devcon0/services/command/update/UpdateMemberCommandHandler.scala
index 9179d9a492d44188712c957eb8aef63ac1de6068..e4dedb4579b0308931e5415add6457721985dfda 100644
--- a/cli/src/main/scala/acab/devcon0/services/command/update/UpdateMemberCommandHandler.scala
+++ b/cli/src/main/scala/acab/devcon0/services/command/update/UpdateMemberCommandHandler.scala
@@ -1,19 +1,19 @@
-package acab.devcon0.services.command.installer
+package acab.devcon0.services.command.update
 
 import acab.devcon0.dtos.*
 import acab.devcon0.dtos.TrileCli.Component.*
 import acab.devcon0.dtos.TrileCli.EnvironmentVariables.*
 import acab.devcon0.dtos.TrileCli.File.*
 import acab.devcon0.dtos.TrileCli.{Component, Defaults, Environment, File}
-import acab.devcon0.dtosFederationMemberUpdateInputVariables.FederationMemberUpdateInputVariables
 import acab.devcon0.services.{EnvVarsFileLoader, PathSanitizer}
 import acab.devcon0.services.shell.{DockerComposeUp, EnvSust}
 import acab.devcon0.{files, services}
+import cats.effect.IO
 import ujson.Value.Value
 
 object UpdateMemberCommandHandler {
 
-  def apply(inputVariables: FederationMemberUpdateInputVariables): Unit = {
+  def apply(inputVariables: FederationMemberUpdateInputVariables): IO[Unit] = {
     val userHomeAbsolutePath: String = PathSanitizer.expandHome(Defaults.userHomeAbsolutePath)
     val configurationPath: String    = s"$userHomeAbsolutePath/.config/trile"
     val federationName: String       = inputVariables.federationName
@@ -22,11 +22,11 @@ object UpdateMemberCommandHandler {
     val environmentVariables: TrileFederationEnvVars = EnvVarsFileLoader(installationPath + "/.installationEnvVars")
     val dockerComposeAbsolutePath: String            = getDockerComposeAbsolutePath(installationPath)
 
-    updateConfigurationFiles(environmentVariables, installationPath)
-    DockerComposeUp(dockerComposeAbsolutePath)
+    updateConfigurationFiles(environmentVariables, installationPath) >>
+      DockerComposeUp(dockerComposeAbsolutePath)
   }
 
-  private def updateConfigurationFiles(envVars: TrileFederationEnvVars, installationPath: String): Unit = {
+  private def updateConfigurationFiles(envVars: TrileFederationEnvVars, installationPath: String): IO[Unit] = {
     val environment                  = Environment.valueOf(envVars(TRILE_ENVIRONMENT))
     val dockerComposeAbsolutePath    = getDockerComposeAbsolutePath(installationPath)
     val dockerComposeTemplateContent = files.Loader.get(DockerCompose, FederationMember, environment)
diff --git a/cli/src/main/scala/acab/devcon0/services/shell/Bash.scala b/cli/src/main/scala/acab/devcon0/services/shell/Bash.scala
index 50dabd6232fd90c9ec0d5a4f13eefbb39fd125b5..96624af619bf2456a0b8073ee222e465855486b2 100644
--- a/cli/src/main/scala/acab/devcon0/services/shell/Bash.scala
+++ b/cli/src/main/scala/acab/devcon0/services/shell/Bash.scala
@@ -1,15 +1,16 @@
 package acab.devcon0.services.shell
 
-import scala.scalanative.libc.stdlib
-import scala.scalanative.unsafe.Zone
+import cats.effect.IO
 
 object Bash {
-  def apply(commandInput: String): Unit = {
-    Zone { implicit z =>
-      val commandStr = s"bash -c \"$commandInput\""
-      println(s"> $commandStr")
-      val command = scala.scalanative.unsafe.toCString(commandStr)
-      stdlib.system(command)
-    }
+  private val cmdLabel: String = "Executing shell command"
+
+  def apply(commandInput: String): IO[Unit] = {
+    val cmd: String = getCmd(commandInput)
+    ShellExecutor.withoutProgress(cmd, s"$cmdLabel: $commandInput", commandInput.length)
+  }
+
+  private def getCmd(commandInput: String): String = {
+    s"bash -c \"$commandInput\""
   }
 }
diff --git a/cli/src/main/scala/acab/devcon0/services/shell/ChmodX.scala b/cli/src/main/scala/acab/devcon0/services/shell/ChmodX.scala
index 907203d726b598618c8de339c509feacf72ff8df..8dc1bd5de9fae77d456393bf26adcc2164cb6cdd 100644
--- a/cli/src/main/scala/acab/devcon0/services/shell/ChmodX.scala
+++ b/cli/src/main/scala/acab/devcon0/services/shell/ChmodX.scala
@@ -1,15 +1,18 @@
 package acab.devcon0.services.shell
 
-import scala.scalanative.libc.stdlib
-import scala.scalanative.unsafe.Zone
+import cats.effect.IO
 
 object ChmodX {
-  def apply(path: String): Unit = {
-    Zone { implicit z =>
-      val commandStr = s"chmod +x $path"
-      println(s"> $commandStr")
-      val command = scala.scalanative.unsafe.toCString(commandStr)
-      stdlib.system(command)
-    }
+
+  private val cmdLabel: String = "Ensuring permissions"
+
+  def apply(path: String): IO[Unit] = {
+    val cmd: String = getCmd(path)
+    val label       = s"$cmdLabel: ${path.split('/').lastOption.getOrElse(path)}"
+    ShellExecutor.withoutProgress(cmd, label, label.length)
+  }
+
+  private def getCmd(path: String): String = {
+    s"chmod +x $path"
   }
 }
diff --git a/cli/src/main/scala/acab/devcon0/services/shell/DockerComposeUp.scala b/cli/src/main/scala/acab/devcon0/services/shell/DockerComposeUp.scala
index b5f07ed9d285b08cbda1201b618d3aabce29888a..1beaa407625dd26115030bc95e548f784046ac04 100644
--- a/cli/src/main/scala/acab/devcon0/services/shell/DockerComposeUp.scala
+++ b/cli/src/main/scala/acab/devcon0/services/shell/DockerComposeUp.scala
@@ -1,15 +1,19 @@
 package acab.devcon0.services.shell
 
-import scala.scalanative.libc.stdlib
-import scala.scalanative.unsafe.{Zone, toCString}
+import cats.effect.IO
+
+import scala.scalanative.unsafe
 
 object DockerComposeUp {
-  def apply(dockerComposeAbsolutePath: String): Unit = {
-    Zone { implicit z =>
-      val pull: String                = s"docker compose -f $dockerComposeAbsolutePath pull"
-      val upForcedAndDetached: String = s"docker compose -f $dockerComposeAbsolutePath up --force-recreate -d"
-      stdlib.system(toCString(pull))
-      stdlib.system(toCString(upForcedAndDetached))
-    }
+
+  private val cmdLabel = "Booting containers"
+
+  def apply(dockerComposeAbsolutePath: String): IO[Unit] = {
+    val cmd: String = getCmd(dockerComposeAbsolutePath)
+    ShellExecutor.withProgress(cmd, cmdLabel)
+  }
+
+  private def getCmd(dockerComposeAbsolutePath: String): String = {
+    s"docker compose --progress=quiet -f $dockerComposeAbsolutePath up --pull always --quiet-pull --force-recreate --detach --wait"
   }
 }
diff --git a/cli/src/main/scala/acab/devcon0/services/shell/EnvSust.scala b/cli/src/main/scala/acab/devcon0/services/shell/EnvSust.scala
index dee5cd8e1f74f6cb53ecaee38afb5207293b9863..cc92b9e48e6b1aabd19ba47731bd0d540d48eb15 100644
--- a/cli/src/main/scala/acab/devcon0/services/shell/EnvSust.scala
+++ b/cli/src/main/scala/acab/devcon0/services/shell/EnvSust.scala
@@ -1,16 +1,30 @@
 package acab.devcon0.services.shell
 
+import cats.effect.IO
+import cats.implicits.catsSyntaxMonadError
+
 import java.nio.charset.StandardCharsets
 import java.nio.file.{Files, Paths}
-import scala.io.Source
 
 object EnvSust {
   def apply(
       envVarsMap: Map[String, String],
       templateContent: String,
       absoluteOutputPath: String
+  ): IO[Unit] = {
+    val cmdLabel = s"File generation: ${absoluteOutputPath.split('/').lastOption.getOrElse(absoluteOutputPath)}"
+    IO(applyInner(envVarsMap, templateContent, absoluteOutputPath, true))
+      .attemptTap[Unit](either => ShellExecutor.attemptTapPrintProgressFinal(cmdLabel, either))
+  }
+
+  private def applyInner(
+      envVarsMap: Map[String, String],
+      templateContent: String,
+      absoluteOutputPath: String,
+      quiet: Boolean
   ): Unit = {
-    println(s"> envsust < .. > $absoluteOutputPath ")
+    if !quiet then println(s"> envsust < .. > $absoluteOutputPath ")
+
     val replacedContent = envVarsMap.foldLeft(templateContent) { (content, envVar) =>
       content.replace(
         s"$${${envVar._1}}",
diff --git a/cli/src/main/scala/acab/devcon0/services/shell/IpfsGenerateSwarmKey.scala b/cli/src/main/scala/acab/devcon0/services/shell/IpfsGenerateSwarmKey.scala
index 7bce267110f434b1b8d89c37c3b5878a5b3a8701..de5ee916ba72c407aedf2f755ac121ca920c7bf6 100644
--- a/cli/src/main/scala/acab/devcon0/services/shell/IpfsGenerateSwarmKey.scala
+++ b/cli/src/main/scala/acab/devcon0/services/shell/IpfsGenerateSwarmKey.scala
@@ -1,15 +1,21 @@
 package acab.devcon0.services.shell
 
+import cats.effect.IO
+
 import scala.scalanative.libc.stdlib
 import scala.scalanative.unsafe.{Zone, toCString}
 
 object IpfsGenerateSwarmKey {
-  def apply(swarmKeyAbsolutePath: String): Unit = {
-    Zone { implicit z =>
-      val commandStr: String =
-        s"echo \"/key/swarm/psk/1.0.0/\n/base16/\n$$(hexdump -n 32 -e '16/1 \"%02x\"' /dev/urandom)\" > $swarmKeyAbsolutePath"
-      println(s"> $commandStr")
-      stdlib.system(toCString(commandStr))
-    }
+
+  private val cmdLabel: String = "Creating IPFS swarm key"
+
+  def apply(swarmKeyAbsolutePath: String): IO[Unit] = {
+    val cmd: String = getCmd(swarmKeyAbsolutePath)
+    val label       = s"$cmdLabel: ${swarmKeyAbsolutePath.split('/').lastOption.getOrElse(swarmKeyAbsolutePath)}"
+    ShellExecutor.withoutProgress(cmd, label, label.length)
+  }
+
+  private def getCmd(swarmKeyAbsolutePath: String): String = {
+    s"echo \"/key/swarm/psk/1.0.0/\n/base16/\n$$(hexdump -n 32 -e '16/1 \"%02x\"' /dev/urandom)\" > $swarmKeyAbsolutePath"
   }
 }
diff --git a/cli/src/main/scala/acab/devcon0/services/shell/MkdirP.scala b/cli/src/main/scala/acab/devcon0/services/shell/MkdirP.scala
index d299f4c3cb59c4628b7340fcbbbed58105150576..1ac1bdc8a0ee3db1e93ebfdc96371f243913ffcd 100644
--- a/cli/src/main/scala/acab/devcon0/services/shell/MkdirP.scala
+++ b/cli/src/main/scala/acab/devcon0/services/shell/MkdirP.scala
@@ -1,15 +1,18 @@
 package acab.devcon0.services.shell
 
-import scala.scalanative.libc.stdlib
-import scala.scalanative.unsafe.Zone
+import cats.effect.IO
 
 object MkdirP {
-  def apply(folderPath: String): Unit = {
-    Zone { implicit z =>
-      val commandStr = s"mkdir -p $folderPath"
-      println(s"> $commandStr")
-      val command = scala.scalanative.unsafe.toCString(commandStr)
-      stdlib.system(command)
-    }
+
+  private val cmdLabel: String = "Ensuring folder exists"
+
+  def apply(folderPath: String): IO[Unit] = {
+    val cmd: String = getCmd(folderPath)
+    val label       = s"$cmdLabel: ${folderPath.split('/').lastOption.getOrElse(folderPath)}"
+    ShellExecutor.withProgress(cmd, label)
+  }
+
+  private def getCmd(folderPath: String): String = {
+    s"mkdir -p $folderPath"
   }
 }
diff --git a/cli/src/main/scala/acab/devcon0/services/shell/ShellExecutor.scala b/cli/src/main/scala/acab/devcon0/services/shell/ShellExecutor.scala
new file mode 100644
index 0000000000000000000000000000000000000000..fb0701aa44cb78835300aa07258e9df13d0f1a31
--- /dev/null
+++ b/cli/src/main/scala/acab/devcon0/services/shell/ShellExecutor.scala
@@ -0,0 +1,89 @@
+package acab.devcon0.services.shell
+
+import cats.effect.IO
+
+import scala.scalanative.libc.stdlib
+import scala.scalanative.unsafe
+import scala.scalanative.unsafe.{CInt, Zone, toCString}
+
+object ShellExecutor {
+
+  def withoutProgress(cmd: String, label: String, wipingLineLength: Int): IO[Unit] = {
+    getCmdIO(cmd)
+      .flatMap(cInt => {
+        if cInt != 0 then replacePrintProgressError(label) >> IO.raiseError(NonZeroExitCode())
+        else replacePrintProgressOk(label, Some(wipingLineLength))
+      })
+  }
+
+  def withProgress(cmd: String, label: String): IO[Unit] = {
+    getCmdIO(appendProgress(cmd, label))
+      .flatMap(cInt => {
+        if cInt != 0 then replacePrintProgressError(label) >> IO.raiseError(NonZeroExitCode())
+        else replacePrintProgressOk(label)
+      })
+  }
+
+  def filterNonZeroExitCode(cInt: CInt): IO[Unit] = {
+    if cInt != 0 then IO.raiseError(NonZeroExitCode())
+    else IO.unit
+  }
+
+  def attemptTapPrintProgressFinal(label: String, either: Either[Throwable, ?]): IO[Unit] = {
+    either
+      .fold(_ => printProgressError(label), _ => printProgressOk(label))
+  }
+
+  def printProgressOk(label: String): IO[Unit] = {
+    getCmdIO(s"printf \"\\033[1;32m[OK]\\033[0;32m $label   \\033[0m\"")
+      .flatMap(_ => IO(println()))
+  }
+
+  def printProgressError(label: String): IO[Unit] = {
+    getCmdIO(s"printf \"\\033[1;31m[Error]\\033[0;31m $label   \\033[0m\"")
+      .flatMap(_ => IO(println()))
+  }
+
+  private def replacePrintProgressOk(label: String, wipingLineLength: Option[Int] = None): IO[Unit] = {
+    val whiteChars: String = " " * wipingLineLength.getOrElse(label.length)
+    getCmdIO(s"printf \"\\r\\033[1;32m[OK]\\033[0;32m $label $whiteChars\\033[0m\"")
+      .flatMap(_ => IO(println()))
+  }
+
+  private def replacePrintProgressError(label: String): IO[Unit] = {
+    getCmdIO(s"printf \"\\r\\033[1;31m[Error]\\033[0;31m $label   \\033[0m\"")
+      .flatMap(_ => IO(println()))
+  }
+
+  private def appendProgress(cmd: String, label: String): String = {
+    s"""$cmd &
+pid=$$! # Process Id of the previous running command
+
+Off='\\033[0m'
+BBlue='\\033[1;34m'
+
+i=0
+while kill -0 $$pid 2>/dev/null
+do
+  for s in / - \\\\ \\|; do
+    printf \"\\r \\033[1;34m [$$s] \\033[0m $label\"
+    sleep .1
+  done
+  i=$$((i+1))
+done
+wait $$pid
+exit $$?
+""".stripMargin
+  }
+
+  private def getCmdIO(cmd: String): IO[CInt] = {
+    IO {
+      Zone { implicit z =>
+        stdlib.system(toCString(cmd))
+      }
+    }
+  }
+
+}
+
+final case class NonZeroExitCode() extends Throwable
diff --git a/cli/src/main/scala/acab/devcon0/services/shell/SudoRm.scala b/cli/src/main/scala/acab/devcon0/services/shell/SudoRm.scala
index 6d3e040c631f8d0c19f70b4e8668f967749c91bf..767bfeb7b7677e01a65294354742bcf5e685368d 100644
--- a/cli/src/main/scala/acab/devcon0/services/shell/SudoRm.scala
+++ b/cli/src/main/scala/acab/devcon0/services/shell/SudoRm.scala
@@ -1,15 +1,23 @@
 package acab.devcon0.services.shell
 
-import scala.scalanative.libc.stdlib
-import scala.scalanative.unsafe.Zone
+import cats.effect.IO
 
 object SudoRm {
-  def apply(folderPath: String): Unit = {
-    Zone { implicit z =>
-      val commandStr = s"sudo rm -rf $folderPath"
-      println(s"> $commandStr")
-      val command = scala.scalanative.unsafe.toCString(commandStr)
-      stdlib.system(command)
-    }
+
+  private val cmdLabel: String = "Deleting folder"
+
+  def apply(folderPath: String): IO[Unit] = {
+    val message     = s"Up to delete $folderPath folder as sudo"
+    val cmd: String = getCmd(folderPath, message)
+    ShellExecutor.withoutProgress(cmd, s"$cmdLabel: $folderPath", message.length)
+  }
+
+  private def getCmd(folderPath: String, message: String): String = {
+    s"""
+if [ -d "$folderPath" ]; then
+    printf \"$message\\n\"
+    sudo rm -rf "$folderPath"
+fi
+    """
   }
 }