You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by sk...@apache.org on 2022/06/23 09:36:50 UTC

[ignite-3] branch main updated: IGNITE-17093 Map error codes for cli commands. #876

This is an automated email from the ASF dual-hosted git repository.

sk0x50 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new 6a64a74ec IGNITE-17093 Map error codes for cli commands. #876
6a64a74ec is described below

commit 6a64a74ecee7981e1bb1d81b01fe3db724b91200
Author: Vadim Pakhnushev <86...@users.noreply.github.com>
AuthorDate: Thu Jun 23 12:36:03 2022 +0300

    IGNITE-17093 Map error codes for cli commands. #876
    
    Signed-off-by: Slava Koptilin <sl...@gmail.com>
---
 modules/cli/pom.xml                                |   6 +
 .../configuration/ItShowConfigurationCallTest.java |  43 ----
 .../commands/CliCommandTestIntegrationBase.java    |  39 ++--
 .../cli/commands/connect/ItConnectCommandTest.java |   8 +-
 .../ignite/cli/commands/sql/ItSqlCommandTest.java  |   2 -
 .../ignite/cli/deprecated/ItConfigCommandTest.java |   8 +-
 .../src/main/java/org/apache/ignite/cli/Main.java  |   8 +-
 .../call/configuration/ClusterConfigShowCall.java  |  14 +-
 .../configuration/ClusterConfigShowCallInput.java  |   1 +
 .../configuration/ClusterConfigUpdateCall.java     |  22 +-
 .../ClusterConfigUpdateCallInput.java              |   4 +-
 .../cli/call/configuration/NodeConfigShowCall.java |  14 +-
 .../call/configuration/NodeConfigUpdateCall.java   |  22 +-
 .../ignite/cli/call/connect/ConnectCall.java       |  12 +-
 .../repl/executor => call/sql}/SqlQueryCall.java   |   2 +-
 .../apache/ignite/cli/call/status/StatusCall.java  |  35 ++-
 .../ignite/cli/call/status/StatusReplCall.java     |   7 +-
 .../apache/ignite/cli/commands/BaseCommand.java    |   6 +-
 .../ignite/cli/commands/TopLevelCliCommand.java    |   5 -
 .../cli/commands/TopLevelCliReplCommand.java       |   4 +-
 .../commands/cliconfig/CliConfigGetSubCommand.java |   7 +-
 .../commands/cliconfig/CliConfigSetSubCommand.java |   7 +-
 .../commands/cliconfig/CliConfigSubCommand.java    |   7 +-
 .../cluster/ClusterConfigShowReplSubCommand.java   |  29 ++-
 .../cluster/ClusterConfigShowSubCommand.java       |   7 +-
 .../cluster/ClusterConfigUpdateReplSubCommand.java |   4 +-
 .../cluster/ClusterConfigUpdateSubCommand.java     |   7 +-
 .../node/NodeConfigShowReplSubCommand.java         |  24 +--
 .../node/NodeConfigShowSubCommand.java             |   7 +-
 .../node/NodeConfigUpdateReplSubCommand.java       |   4 +-
 .../node/NodeConfigUpdateSubCommand.java           |  13 +-
 .../cli/commands/connect/ConnectCommand.java       |  15 +-
 .../cli/commands/connect/DisconnectCommand.java    |   7 +-
 .../apache/ignite/cli/commands/sql/SqlCommand.java |  85 ++------
 .../sql/{SqlCommand.java => SqlReplCommand.java}   |  33 +--
 .../ignite/cli/commands/status/StatusCommand.java  |  16 +-
 .../cli/commands/status/StatusReplCommand.java     |  11 +-
 .../cli/commands/topology/TopologyCommand.java     |  13 +-
 .../cli/commands/version/VersionCommand.java       |  11 +-
 .../cli/core/call/CallExecutionPipeline.java       |  21 +-
 .../ignite/cli/core/call/StatusCallInput.java      |   1 +
 .../cli/core/exception/ExceptionHandler.java       |   8 +-
 .../cli/core/exception/ExceptionHandlers.java      |  13 +-
 ...tionHandler.java => IgniteCliApiException.java} |  35 +--
 .../handler/CommandExecutionExceptionHandler.java  |  41 ----
 .../handler/ConnectCommandExceptionHandler.java    |  37 ----
 .../exception/handler/ConnectExceptionHandler.java |  37 ----
 .../handler/DefaultExceptionHandlers.java          |   5 +-
 .../handler/EndOfFileExceptionHandler.java         |   3 +-
 .../handler/IgniteCliApiExceptionHandler.java      |  65 ++++++
 .../handler/IgniteCliExceptionHandler.java         |   3 +-
 .../handler/IgniteClientExceptionHandler.java      |   3 +-
 .../handler/PicocliExecutionExceptionHandler.java  |   3 +-
 .../exception/handler/SqlExceptionHandler.java     |   3 +-
 .../exception/handler/TimeoutExceptionHandler.java |   3 +-
 .../handler/UnknownCommandExceptionHandler.java    |   4 +-
 .../handler/UserInterruptExceptionHandler.java     |   3 +-
 .../builtins/cluster/ClusterApiClient.java         |   3 +-
 .../spec/BootstrapIgniteCommandSpec.java           |   7 +-
 .../cli/deprecated/spec/ClusterCommandSpec.java    |   6 +-
 .../deprecated/spec/ClusterReplCommandSpec.java    |   2 +-
 .../cli/deprecated/spec/NodeCommandSpec.java       |  21 +-
 .../org/apache/ignite/cli/sql/SqlSchemaLoader.java |   4 +-
 .../ignite/cli/commands/CliCommandTestBase.java    |  86 +++++++-
 .../cli/commands/UrlOptionsNegativeTest.java       | 235 +++++++++++++++++++++
 .../cliconfig/CliConfigGetSubCommandTest.java      |  47 +++--
 .../cliconfig/CliConfigSetSubCommandTest.java      |  53 ++---
 .../cliconfig/CliConfigSubCommandTest.java         |  19 +-
 .../configuration/ShowConfigSubCommandTest.java    |  48 -----
 .../ignite/cli/commands/sql/SqlCommandTest.java    |  56 +++++
 70 files changed, 779 insertions(+), 665 deletions(-)

diff --git a/modules/cli/pom.xml b/modules/cli/pom.xml
index 07d258972..e95d9fef3 100644
--- a/modules/cli/pom.xml
+++ b/modules/cli/pom.xml
@@ -214,6 +214,12 @@
             <scope>test</scope>
         </dependency>
 
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-params</artifactId>
+            <scope>test</scope>
+        </dependency>
+
         <dependency>
             <groupId>org.assertj</groupId>
             <artifactId>assertj-core</artifactId>
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/call/configuration/ItShowConfigurationCallTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/call/configuration/ItShowConfigurationCallTest.java
index aa66ccd0b..786fd8dd5 100644
--- a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/call/configuration/ItShowConfigurationCallTest.java
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/call/configuration/ItShowConfigurationCallTest.java
@@ -19,14 +19,9 @@ package org.apache.ignite.cli.call.configuration;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
-import com.google.common.primitives.Chars;
 import jakarta.inject.Inject;
-import java.util.ArrayList;
-import java.util.List;
 import org.apache.ignite.cli.call.CallIntegrationTestBase;
-import org.apache.ignite.cli.core.call.CallExecutionPipeline;
 import org.apache.ignite.cli.core.call.DefaultCallOutput;
-import org.apache.ignite.cli.core.exception.handler.CommandExecutionExceptionHandler;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 
@@ -110,42 +105,4 @@ class ItShowConfigurationCallTest extends CallIntegrationTestBase {
         // And
         assertThat(output.body()).isEqualTo("5000");
     }
-
-    @Test
-    @DisplayName("Should display error when wrong port is given")
-    public void incorrectPortTest() {
-        var input = NodeConfigShowCallInput.builder()
-                .nodeUrl(NODE_URL + "incorrect")
-                .build();
-        List<Character> list = new ArrayList<>();
-
-        CallExecutionPipeline.builder(nodeConfigShowCall)
-                .inputProvider(() -> input)
-                .exceptionHandler(new CommandExecutionExceptionHandler())
-                .errOutput(output(list))
-                .build()
-                .runPipeline();
-
-        assertThat(new String(Chars.toArray(list)))
-                .contains("Invalid URL port: \"10300incorrect");
-    }
-
-    @Test
-    @DisplayName("Should display error when wrong url is given")
-    public void incorrectSchemeTest() {
-        var input = NodeConfigShowCallInput.builder()
-                .nodeUrl("incorrect" + NODE_URL)
-                .build();
-        List<Character> list = new ArrayList<>();
-
-        CallExecutionPipeline.builder(nodeConfigShowCall)
-                .inputProvider(() -> input)
-                .exceptionHandler(new CommandExecutionExceptionHandler())
-                .errOutput(output(list))
-                .build()
-                .runPipeline();
-
-        assertThat(new String(Chars.toArray(list)))
-                .contains("Expected URL scheme 'http' or 'https' but was 'incorrecthttp'");
-    }
 }
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/CliCommandTestIntegrationBase.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/CliCommandTestIntegrationBase.java
index b59ce489b..363787cfd 100644
--- a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/CliCommandTestIntegrationBase.java
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/CliCommandTestIntegrationBase.java
@@ -25,7 +25,6 @@ import jakarta.inject.Inject;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import org.apache.ignite.cli.IntegrationTestBase;
-import org.jetbrains.annotations.NotNull;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.TestInfo;
 import picocli.CommandLine;
@@ -38,26 +37,30 @@ public class CliCommandTestIntegrationBase extends IntegrationTestBase {
     protected static final String JDBC_URL = "jdbc:ignite:thin://127.0.0.1:10800";
 
     @Inject
-    protected ApplicationContext applicationContext;
+    private ApplicationContext context;
 
     private CommandLine cmd;
-
     private StringWriter sout;
-
     private StringWriter serr;
-
     private int exitCode = Integer.MIN_VALUE;
 
-    protected void setupCmd(MicronautFactory factory) {
-        cmd = new CommandLine(getCommandClass(), factory);
+    /**
+     * Invokes before the test will start.
+     *
+     * @param testInfo Test information object.
+     * @throws Exception If failed.
+     */
+    @BeforeEach
+    public void setUp(TestInfo testInfo) throws Exception {
+        super.setUp(testInfo);
+        cmd = new CommandLine(getCommandClass(), new MicronautFactory(context));
         sout = new StringWriter();
         serr = new StringWriter();
         cmd.setOut(new PrintWriter(sout));
         cmd.setErr(new PrintWriter(serr));
     }
 
-    @NotNull
-    protected Class getCommandClass() {
+    protected Class<?> getCommandClass() {
         return TopLevelCliCommand.class;
     }
 
@@ -66,10 +69,9 @@ public class CliCommandTestIntegrationBase extends IntegrationTestBase {
     }
 
     protected void assertExitCodeIs(int expectedExitCode) {
-        //TODO: https://issues.apache.org/jira/browse/IGNITE-17093
-        /*assertThat(exitCode)
+        assertThat(exitCode)
                 .as("Expected exit code to be: " + expectedExitCode + " but was " + exitCode)
-                .isEqualTo(expectedExitCode);*/
+                .isEqualTo(expectedExitCode);
     }
 
     protected void assertExitCodeIsZero() {
@@ -117,17 +119,4 @@ public class CliCommandTestIntegrationBase extends IntegrationTestBase {
                 .as("Expected command error output to be equal to: " + expectedErrOutput)
                 .isEqualTo(expectedErrOutput);
     }
-
-    /**
-     * Invokes before the test will start.
-     *
-     * @param testInfo Test information object.
-     * @throws Exception If failed.
-     */
-    @BeforeEach
-    public void setUp(TestInfo testInfo) throws Exception {
-        super.setUp(testInfo);
-        setupCmd(new MicronautFactory(applicationContext));
-    }
 }
-
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/connect/ItConnectCommandTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/connect/ItConnectCommandTest.java
index ef82683ee..2b10db076 100644
--- a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/connect/ItConnectCommandTest.java
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/connect/ItConnectCommandTest.java
@@ -24,7 +24,6 @@ import jakarta.inject.Inject;
 import org.apache.ignite.cli.commands.CliCommandTestIntegrationBase;
 import org.apache.ignite.cli.commands.TopLevelCliReplCommand;
 import org.apache.ignite.cli.core.repl.prompt.PromptProvider;
-import org.jetbrains.annotations.NotNull;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 import picocli.CommandLine.Help.Ansi;
@@ -34,7 +33,7 @@ class ItConnectCommandTest extends CliCommandTestIntegrationBase {
     PromptProvider promptProvider;
 
     @Override
-    protected @NotNull Class<?> getCommandClass() {
+    protected Class<?> getCommandClass() {
         return TopLevelCliReplCommand.class;
     }
 
@@ -50,7 +49,6 @@ class ItConnectCommandTest extends CliCommandTestIntegrationBase {
 
         // Then
         assertAll(
-                this::assertExitCodeIsZero,
                 this::assertErrOutputIsEmpty,
                 () -> assertOutputContains("Connected to http://localhost:10300")
         );
@@ -67,7 +65,6 @@ class ItConnectCommandTest extends CliCommandTestIntegrationBase {
 
         // Then
         assertAll(
-                this::assertExitCodeIsZero,
                 this::assertErrOutputIsEmpty,
                 () -> assertOutputContains("Connected to http://localhost:10301")
         );
@@ -81,7 +78,7 @@ class ItConnectCommandTest extends CliCommandTestIntegrationBase {
 
         // Then
         assertAll(
-                () -> assertErrOutputIs("Can not connect to http://localhost:11111" + System.lineSeparator())
+                () -> assertErrOutputIs("Could not connect to URL: http://localhost:11111" + System.lineSeparator())
         );
         // And prompt is
         String prompt = Ansi.OFF.string(promptProvider.getPrompt());
@@ -101,7 +98,6 @@ class ItConnectCommandTest extends CliCommandTestIntegrationBase {
         execute("disconnect");
         // Then
         assertAll(
-                this::assertExitCodeIsZero,
                 this::assertErrOutputIsEmpty,
                 () -> assertOutputContains("Disconnected from http://localhost:10300")
         );
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/sql/ItSqlCommandTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/sql/ItSqlCommandTest.java
index 31dd7b486..68933cbe3 100644
--- a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/sql/ItSqlCommandTest.java
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/sql/ItSqlCommandTest.java
@@ -63,7 +63,6 @@ class ItSqlCommandTest extends CliCommandTestIntegrationBase {
         assertAll(
                 () -> assertExitCodeIs(1),
                 this::assertOutputIsEmpty,
-                this::assertErrOutputIsNotEmpty,
                 // TODO: https://issues.apache.org/jira/browse/IGNITE-17090
                 () -> assertErrOutputIs(CLIENT_CONNECTION_FAILED_MESSAGE + System.lineSeparator())
         );
@@ -77,7 +76,6 @@ class ItSqlCommandTest extends CliCommandTestIntegrationBase {
         assertAll(
                 () -> assertExitCodeIs(1),
                 this::assertOutputIsEmpty,
-                this::assertErrOutputIsNotEmpty,
                 // TODO: https://issues.apache.org/jira/browse/IGNITE-17090
                 () -> assertErrOutputIs("SQL query parsing error: Sql query execution failed." + System.lineSeparator())
         );
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/deprecated/ItConfigCommandTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/deprecated/ItConfigCommandTest.java
index 52dfc8742..2bbd71bd3 100644
--- a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/deprecated/ItConfigCommandTest.java
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/deprecated/ItConfigCommandTest.java
@@ -123,10 +123,10 @@ public class ItConfigCommandTest extends AbstractCliIntegrationTest {
                 "network.foo=\"bar\""
         );
 
-        //assertEquals(1, exitCode); // TODO
+        assertEquals(1, exitCode);
         assertThat(
                 err.toString(UTF_8),
-                both(startsWith("Command node config update failed with reason: Got error while updating the node configuration."))
+                both(startsWith("An error occurred"))
                         .and(containsString("'network' configuration doesn't have the 'foo' sub-configuration"))
         );
 
@@ -141,10 +141,10 @@ public class ItConfigCommandTest extends AbstractCliIntegrationTest {
                 "network.shutdownQuietPeriod=asd"
         );
 
-        //assertEquals(1, exitCode); // TODO
+        assertEquals(1, exitCode);
         assertThat(
                 err.toString(UTF_8),
-                both(containsString("Command node config update failed with reason: Got error while updating the node configuration."))
+                both(containsString("An error occurred"))
                         .and(containsString("'long' is expected as a type for the 'network.shutdownQuietPeriod' configuration value"))
         );
     }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/Main.java b/modules/cli/src/main/java/org/apache/ignite/cli/Main.java
index ca443970d..eeb0f37c9 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/Main.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/Main.java
@@ -54,11 +54,12 @@ public class Main {
     public static void main(String[] args) {
         initJavaLoggerProps();
 
+        int exitCode = 0;
         try (MicronautFactory micronautFactory = new MicronautFactory()) {
             AnsiConsole.systemInstall();
             if (args.length != 0 || !isatty()) { // do not enter REPL if input or output is redirected
                 try {
-                    executeCommand(args, micronautFactory);
+                    exitCode = executeCommand(args, micronautFactory);
                 } catch (Exception e) {
                     System.err.println("Error occurred during command execution");
                 }
@@ -72,6 +73,7 @@ public class Main {
         } finally {
             AnsiConsole.systemUninstall();
         }
+        System.exit(exitCode);
     }
 
     private static boolean isatty() {
@@ -108,12 +110,12 @@ public class Main {
                 .build());
     }
 
-    private static void executeCommand(String[] args, MicronautFactory micronautFactory) throws Exception {
+    private static int executeCommand(String[] args, MicronautFactory micronautFactory) throws Exception {
         CommandLine cmd = new CommandLine(TopLevelCliCommand.class, micronautFactory);
         cmd.setExecutionExceptionHandler(new PicocliExecutionExceptionHandler());
         Config config = micronautFactory.create(Config.class);
         cmd.setDefaultValueProvider(new CommandLine.PropertiesDefaultProvider(config.getProperties()));
-        cmd.execute(args);
+        return cmd.execute(args);
     }
 
     private static final String[] BANNER = new String[]{
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/call/configuration/ClusterConfigShowCall.java b/modules/cli/src/main/java/org/apache/ignite/cli/call/configuration/ClusterConfigShowCall.java
index 28cc526c5..55413acee 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/call/configuration/ClusterConfigShowCall.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/call/configuration/ClusterConfigShowCall.java
@@ -20,7 +20,7 @@ package org.apache.ignite.cli.call.configuration;
 import jakarta.inject.Singleton;
 import org.apache.ignite.cli.core.call.Call;
 import org.apache.ignite.cli.core.call.DefaultCallOutput;
-import org.apache.ignite.cli.core.exception.CommandExecutionException;
+import org.apache.ignite.cli.core.exception.IgniteCliApiException;
 import org.apache.ignite.rest.client.api.ClusterConfigurationApi;
 import org.apache.ignite.rest.client.invoker.ApiClient;
 import org.apache.ignite.rest.client.invoker.ApiException;
@@ -34,15 +34,13 @@ public class ClusterConfigShowCall implements Call<ClusterConfigShowCallInput, S
 
     /** {@inheritDoc} */
     @Override
-    public DefaultCallOutput<String> execute(ClusterConfigShowCallInput clusterConfigShowCallInput) {
-        ClusterConfigurationApi client = createApiClient(clusterConfigShowCallInput);
+    public DefaultCallOutput<String> execute(ClusterConfigShowCallInput input) {
+        ClusterConfigurationApi client = createApiClient(input);
 
         try {
-            return DefaultCallOutput.success(readClusterConfig(client, clusterConfigShowCallInput));
-        } catch (ApiException e) {
-            return DefaultCallOutput.failure(e);
-        } catch (IllegalArgumentException e) {
-            return DefaultCallOutput.failure(new CommandExecutionException("cluster config show", e.getMessage()));
+            return DefaultCallOutput.success(readClusterConfig(client, input));
+        } catch (ApiException | IllegalArgumentException e) {
+            return DefaultCallOutput.failure(new IgniteCliApiException(e, input.getClusterUrl()));
         }
     }
 
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/call/configuration/ClusterConfigShowCallInput.java b/modules/cli/src/main/java/org/apache/ignite/cli/call/configuration/ClusterConfigShowCallInput.java
index 87ba85633..687d8d97d 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/call/configuration/ClusterConfigShowCallInput.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/call/configuration/ClusterConfigShowCallInput.java
@@ -68,6 +68,7 @@ public class ClusterConfigShowCallInput implements CallInput {
      */
     public static class ShowConfigurationCallInputBuilder {
         private String selector;
+
         private String clusterUrl;
 
         public ShowConfigurationCallInputBuilder selector(String selector) {
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/call/configuration/ClusterConfigUpdateCall.java b/modules/cli/src/main/java/org/apache/ignite/cli/call/configuration/ClusterConfigUpdateCall.java
index 6a19a6523..52770eb25 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/call/configuration/ClusterConfigUpdateCall.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/call/configuration/ClusterConfigUpdateCall.java
@@ -20,12 +20,11 @@ package org.apache.ignite.cli.call.configuration;
 import jakarta.inject.Singleton;
 import org.apache.ignite.cli.core.call.Call;
 import org.apache.ignite.cli.core.call.DefaultCallOutput;
-import org.apache.ignite.cli.core.exception.CommandExecutionException;
+import org.apache.ignite.cli.core.exception.IgniteCliApiException;
 import org.apache.ignite.rest.client.api.ClusterConfigurationApi;
 import org.apache.ignite.rest.client.invoker.ApiClient;
 import org.apache.ignite.rest.client.invoker.ApiException;
 import org.apache.ignite.rest.client.invoker.Configuration;
-import org.jetbrains.annotations.NotNull;
 
 /**
  * Updates cluster configuration.
@@ -34,21 +33,13 @@ import org.jetbrains.annotations.NotNull;
 public class ClusterConfigUpdateCall implements Call<ClusterConfigUpdateCallInput, String> {
     /** {@inheritDoc} */
     @Override
-    public DefaultCallOutput<String> execute(ClusterConfigUpdateCallInput clusterConfigUpdateCallInput) {
-        ClusterConfigurationApi client = createApiClient(clusterConfigUpdateCallInput);
+    public DefaultCallOutput<String> execute(ClusterConfigUpdateCallInput input) {
+        ClusterConfigurationApi client = createApiClient(input);
 
         try {
-            return updateClusterConfig(client, clusterConfigUpdateCallInput);
-        } catch (ApiException e) {
-            if (e.getCode() == 400) {
-                return DefaultCallOutput.failure(
-                        new CommandExecutionException(
-                                "cluster config update",
-                                "Got error while updating the cluster configuration. " + System.lineSeparator()
-                                        + "Code:  " + e.getCode() + ", response:  " + e.getResponseBody()
-                        ));
-            }
-            return DefaultCallOutput.failure(new CommandExecutionException("cluster config update", "Ignite api return " + e.getCode()));
+            return updateClusterConfig(client, input);
+        } catch (ApiException | IllegalArgumentException e) {
+            return DefaultCallOutput.failure(new IgniteCliApiException(e, input.getClusterUrl()));
         }
     }
 
@@ -58,7 +49,6 @@ public class ClusterConfigUpdateCall implements Call<ClusterConfigUpdateCallInpu
         return DefaultCallOutput.success("Cluster configuration was updated successfully.");
     }
 
-    @NotNull
     private ClusterConfigurationApi createApiClient(ClusterConfigUpdateCallInput input) {
         ApiClient client = Configuration.getDefaultApiClient();
         client.setBasePath(input.getClusterUrl());
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/call/configuration/ClusterConfigUpdateCallInput.java b/modules/cli/src/main/java/org/apache/ignite/cli/call/configuration/ClusterConfigUpdateCallInput.java
index d0250cd53..2ebbe7efa 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/call/configuration/ClusterConfigUpdateCallInput.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/call/configuration/ClusterConfigUpdateCallInput.java
@@ -20,7 +20,7 @@ package org.apache.ignite.cli.call.configuration;
 import org.apache.ignite.cli.core.call.CallInput;
 
 /**
- * Input for {@link NodeConfigUpdateCall}.
+ * Input for {@link ClusterConfigUpdateCall}.
  */
 public class ClusterConfigUpdateCallInput implements CallInput {
     /**
@@ -59,7 +59,7 @@ public class ClusterConfigUpdateCallInput implements CallInput {
     /**
      * Get cluster URL.
      *
-     * @return Cluster url.
+     * @return Cluster URL.
      */
     public String getClusterUrl() {
         return clusterUrl;
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/call/configuration/NodeConfigShowCall.java b/modules/cli/src/main/java/org/apache/ignite/cli/call/configuration/NodeConfigShowCall.java
index b3f7e8fa8..a7b2ec846 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/call/configuration/NodeConfigShowCall.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/call/configuration/NodeConfigShowCall.java
@@ -20,7 +20,7 @@ package org.apache.ignite.cli.call.configuration;
 import jakarta.inject.Singleton;
 import org.apache.ignite.cli.core.call.Call;
 import org.apache.ignite.cli.core.call.DefaultCallOutput;
-import org.apache.ignite.cli.core.exception.CommandExecutionException;
+import org.apache.ignite.cli.core.exception.IgniteCliApiException;
 import org.apache.ignite.rest.client.api.NodeConfigurationApi;
 import org.apache.ignite.rest.client.invoker.ApiClient;
 import org.apache.ignite.rest.client.invoker.ApiException;
@@ -34,15 +34,13 @@ public class NodeConfigShowCall implements Call<NodeConfigShowCallInput, String>
 
     /** {@inheritDoc} */
     @Override
-    public DefaultCallOutput<String> execute(NodeConfigShowCallInput readConfigurationInput) {
-        NodeConfigurationApi client = createApiClient(readConfigurationInput);
+    public DefaultCallOutput<String> execute(NodeConfigShowCallInput input) {
+        NodeConfigurationApi client = createApiClient(input);
 
         try {
-            return DefaultCallOutput.success(readNodeConfig(client, readConfigurationInput));
-        } catch (ApiException e) {
-            return DefaultCallOutput.failure(e);
-        } catch (IllegalArgumentException e) {
-            return DefaultCallOutput.failure(new CommandExecutionException("node config show", e.getMessage()));
+            return DefaultCallOutput.success(readNodeConfig(client, input));
+        } catch (ApiException | IllegalArgumentException e) {
+            return DefaultCallOutput.failure(new IgniteCliApiException(e, input.getNodeUrl()));
         }
     }
 
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/call/configuration/NodeConfigUpdateCall.java b/modules/cli/src/main/java/org/apache/ignite/cli/call/configuration/NodeConfigUpdateCall.java
index c073e5310..382441155 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/call/configuration/NodeConfigUpdateCall.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/call/configuration/NodeConfigUpdateCall.java
@@ -20,12 +20,11 @@ package org.apache.ignite.cli.call.configuration;
 import jakarta.inject.Singleton;
 import org.apache.ignite.cli.core.call.Call;
 import org.apache.ignite.cli.core.call.DefaultCallOutput;
-import org.apache.ignite.cli.core.exception.CommandExecutionException;
+import org.apache.ignite.cli.core.exception.IgniteCliApiException;
 import org.apache.ignite.rest.client.api.NodeConfigurationApi;
 import org.apache.ignite.rest.client.invoker.ApiClient;
 import org.apache.ignite.rest.client.invoker.ApiException;
 import org.apache.ignite.rest.client.invoker.Configuration;
-import org.jetbrains.annotations.NotNull;
 
 /**
  * Updates configuration for node.
@@ -34,21 +33,13 @@ import org.jetbrains.annotations.NotNull;
 public class NodeConfigUpdateCall implements Call<NodeConfigUpdateCallInput, String> {
     /** {@inheritDoc} */
     @Override
-    public DefaultCallOutput<String> execute(NodeConfigUpdateCallInput nodeConfigUpdateCallInput) {
-        NodeConfigurationApi client = createApiClient(nodeConfigUpdateCallInput);
+    public DefaultCallOutput<String> execute(NodeConfigUpdateCallInput input) {
+        NodeConfigurationApi client = createApiClient(input);
 
         try {
-            return updateNodeConfig(client, nodeConfigUpdateCallInput);
-        } catch (ApiException e) {
-            if (e.getCode() == 400) {
-                return DefaultCallOutput.failure(
-                        new CommandExecutionException(
-                                "node config update",
-                                "Got error while updating the node configuration. " + System.lineSeparator()
-                                        + "Code:  " + e.getCode() + ", response:  " + e.getResponseBody()
-                        ));
-            }
-            return DefaultCallOutput.failure(new CommandExecutionException("node config update", "Ignite api return " + e.getCode()));
+            return updateNodeConfig(client, input);
+        } catch (ApiException | IllegalArgumentException e) {
+            return DefaultCallOutput.failure(new IgniteCliApiException(e, input.getNodeUrl()));
         }
     }
 
@@ -58,7 +49,6 @@ public class NodeConfigUpdateCall implements Call<NodeConfigUpdateCallInput, Str
         return DefaultCallOutput.success("Node configuration was updated successfully.");
     }
 
-    @NotNull
     private NodeConfigurationApi createApiClient(NodeConfigUpdateCallInput input) {
         ApiClient client = Configuration.getDefaultApiClient();
         client.setBasePath(input.getNodeUrl());
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/call/connect/ConnectCall.java b/modules/cli/src/main/java/org/apache/ignite/cli/call/connect/ConnectCall.java
index 270fb5cc8..67c099f3f 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/call/connect/ConnectCall.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/call/connect/ConnectCall.java
@@ -19,20 +19,18 @@ package org.apache.ignite.cli.call.connect;
 
 import com.google.gson.Gson;
 import jakarta.inject.Singleton;
-import java.net.ConnectException;
 import java.net.MalformedURLException;
 import java.net.URL;
 import org.apache.ignite.cli.core.call.Call;
 import org.apache.ignite.cli.core.call.CallOutput;
 import org.apache.ignite.cli.core.call.DefaultCallOutput;
-import org.apache.ignite.cli.core.exception.ConnectCommandException;
+import org.apache.ignite.cli.core.exception.IgniteCliApiException;
 import org.apache.ignite.cli.core.repl.Session;
 import org.apache.ignite.cli.core.repl.config.RootConfig;
 import org.apache.ignite.rest.client.api.NodeConfigurationApi;
 import org.apache.ignite.rest.client.invoker.ApiClient;
 import org.apache.ignite.rest.client.invoker.ApiException;
 import org.apache.ignite.rest.client.invoker.Configuration;
-import org.jetbrains.annotations.NotNull;
 
 
 /**
@@ -54,12 +52,9 @@ public class ConnectCall implements Call<ConnectCallInput, String> {
         try {
             String configuration = api.getNodeConfiguration();
             setJdbcUrl(configuration, nodeUrl);
-        } catch (ApiException e) {
+        } catch (ApiException | IllegalArgumentException e) {
             session.setConnectedToNode(false);
-            if (e.getCause() instanceof ConnectException) {
-                return DefaultCallOutput.failure(new ConnectCommandException("Can not connect to " + input.getNodeUrl()));
-            }
-            return DefaultCallOutput.failure(e);
+            return DefaultCallOutput.failure(new IgniteCliApiException(e, nodeUrl));
         }
 
         session.setNodeUrl(nodeUrl);
@@ -67,7 +62,6 @@ public class ConnectCall implements Call<ConnectCallInput, String> {
         return DefaultCallOutput.success("Connected to " + nodeUrl);
     }
 
-    @NotNull
     private NodeConfigurationApi createApiClient(ConnectCallInput input) {
         ApiClient client = Configuration.getDefaultApiClient();
         client.setBasePath(input.getNodeUrl());
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/core/repl/executor/SqlQueryCall.java b/modules/cli/src/main/java/org/apache/ignite/cli/call/sql/SqlQueryCall.java
similarity index 97%
rename from modules/cli/src/main/java/org/apache/ignite/cli/core/repl/executor/SqlQueryCall.java
rename to modules/cli/src/main/java/org/apache/ignite/cli/call/sql/SqlQueryCall.java
index 7c19262f1..8c1ea8926 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/core/repl/executor/SqlQueryCall.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/call/sql/SqlQueryCall.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.cli.core.repl.executor;
+package org.apache.ignite.cli.call.sql;
 
 import java.sql.SQLException;
 import org.apache.ignite.cli.core.call.Call;
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/call/status/StatusCall.java b/modules/cli/src/main/java/org/apache/ignite/cli/call/status/StatusCall.java
index 294d249f9..55a860af4 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/call/status/StatusCall.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/call/status/StatusCall.java
@@ -18,12 +18,11 @@
 package org.apache.ignite.cli.call.status;
 
 import jakarta.inject.Singleton;
-import java.net.ConnectException;
 import org.apache.ignite.cli.core.call.Call;
 import org.apache.ignite.cli.core.call.CallOutput;
 import org.apache.ignite.cli.core.call.DefaultCallOutput;
 import org.apache.ignite.cli.core.call.StatusCallInput;
-import org.apache.ignite.cli.core.exception.CommandExecutionException;
+import org.apache.ignite.cli.core.exception.IgniteCliApiException;
 import org.apache.ignite.cli.deprecated.CliPathsConfigLoader;
 import org.apache.ignite.cli.deprecated.IgnitePaths;
 import org.apache.ignite.cli.deprecated.builtins.node.NodeManager;
@@ -37,7 +36,6 @@ import org.apache.ignite.rest.client.invoker.Configuration;
  * Call to get cluster status.
  */
 @Singleton
-//TODO: https://issues.apache.org/jira/browse/IGNITE-17093
 public class StatusCall implements Call<StatusCallInput, Status> {
 
     private final NodeManager nodeManager;
@@ -55,32 +53,27 @@ public class StatusCall implements Call<StatusCallInput, Status> {
     @Override
     public CallOutput<Status> execute(StatusCallInput input) {
         IgnitePaths paths = cliPathsCfgLdr.loadIgnitePathsOrThrowError();
-        String connected = null;
         try {
-            connected = createNodeApi(input).getNodeConfiguration();
-        } catch (ApiException e) {
-            if (e.getCause() instanceof ConnectException) {
-                return DefaultCallOutput.failure(new CommandExecutionException("status", "cannot connect to " + input.getClusterUrl()));
-            } else {
-                return DefaultCallOutput.failure(e);
-            }
+            String connected = createNodeApi(input).getNodeConfiguration();
+            return DefaultCallOutput.success(
+                    Status.builder()
+                            .connected(connected != null)
+                            .connectedNodeUrl(input.getClusterUrl())
+                            .initialized(connected != null && canReadClusterConfig(input))
+                            .nodeCount(nodeManager.getRunningNodes(paths.logDir, paths.cliPidsDir()).size())
+                            .build()
+            );
+        } catch (ApiException | IllegalArgumentException e) {
+            return DefaultCallOutput.failure(new IgniteCliApiException(e, input.getClusterUrl()));
         }
-        return DefaultCallOutput.success(
-                Status.builder()
-                        .connected(connected != null)
-                        .connectedNodeUrl(input.getClusterUrl())
-                        .initialized(connected != null && canReadClusterConfig(input))
-                        .nodeCount(nodeManager.getRunningNodes(paths.logDir, paths.cliPidsDir()).size())
-                        .build()
-        );
     }
 
     private boolean canReadClusterConfig(StatusCallInput input) {
-        var clusterApi = createClusterApi(input);
+        ClusterConfigurationApi clusterApi = createClusterApi(input);
         try {
             clusterApi.getClusterConfiguration();
             return true;
-        } catch (ApiException e) {
+        } catch (ApiException | IllegalArgumentException e) {
             return false;
         }
     }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/call/status/StatusReplCall.java b/modules/cli/src/main/java/org/apache/ignite/cli/call/status/StatusReplCall.java
index a7c8babcf..14d325246 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/call/status/StatusReplCall.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/call/status/StatusReplCall.java
@@ -30,13 +30,11 @@ import org.apache.ignite.rest.client.api.ClusterConfigurationApi;
 import org.apache.ignite.rest.client.invoker.ApiClient;
 import org.apache.ignite.rest.client.invoker.ApiException;
 import org.apache.ignite.rest.client.invoker.Configuration;
-import org.jetbrains.annotations.NotNull;
 
 /**
  * Call to get cluster status.
  */
 @Singleton
-//TODO: https://issues.apache.org/jira/browse/IGNITE-17093
 public class StatusReplCall implements Call<EmptyCallInput, Status> {
 
     private final NodeManager nodeManager;
@@ -68,16 +66,15 @@ public class StatusReplCall implements Call<EmptyCallInput, Status> {
     }
 
     private boolean canReadClusterConfig() {
-        var clusterApi = createApiClient();
+        ClusterConfigurationApi clusterApi = createApiClient();
         try {
             clusterApi.getClusterConfiguration();
             return true;
-        } catch (ApiException e) {
+        } catch (ApiException | IllegalArgumentException e) {
             return false;
         }
     }
 
-    @NotNull
     private ClusterConfigurationApi createApiClient() {
         ApiClient client = Configuration.getDefaultApiClient();
         client.setBasePath(session.getNodeUrl());
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/BaseCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/BaseCommand.java
index 46f8b4b26..8e05a2dc5 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/BaseCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/BaseCommand.java
@@ -24,15 +24,11 @@ import picocli.CommandLine.Spec;
 /**
  * Base class for commands.
  */
-public abstract class BaseCommand implements Runnable {
+public abstract class BaseCommand {
     /** Help option specification. */
     @Option(names = {"-h", "--help"}, usageHelp = true, description = "Show this help message and exit.")
     protected boolean usageHelpRequested;
 
     @Spec
     protected CommandSpec spec;
-
-    @Override
-    public void run() {
-    }
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/TopLevelCliCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/TopLevelCliCommand.java
index 040523bee..981ed9ced 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/TopLevelCliCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/TopLevelCliCommand.java
@@ -52,9 +52,4 @@ public class TopLevelCliCommand extends BaseCommand {
     @SuppressWarnings("PMD.UnusedPrivateField")
     @Option(names = {"--version"}, versionHelp = true, description = "Print version information and exit")
     private boolean versionRequested;
-
-    @Override
-    public void run() {
-
-    }
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/TopLevelCliReplCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/TopLevelCliReplCommand.java
index 94638253e..db4766553 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/TopLevelCliReplCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/TopLevelCliReplCommand.java
@@ -23,7 +23,7 @@ import org.apache.ignite.cli.commands.configuration.cluster.ClusterReplCommand;
 import org.apache.ignite.cli.commands.configuration.node.NodeReplCommand;
 import org.apache.ignite.cli.commands.connect.ConnectCommand;
 import org.apache.ignite.cli.commands.connect.DisconnectCommand;
-import org.apache.ignite.cli.commands.sql.SqlCommand;
+import org.apache.ignite.cli.commands.sql.SqlReplCommand;
 import org.apache.ignite.cli.commands.status.StatusReplCommand;
 import org.apache.ignite.cli.commands.version.VersionCommand;
 import org.apache.ignite.cli.deprecated.spec.BootstrapIgniteCommandSpec;
@@ -36,7 +36,7 @@ import picocli.shell.jline3.PicocliCommands;
 @CommandLine.Command(name = "",
         footer = {"", "Press Ctrl-D to exit."},
         subcommands = {
-                SqlCommand.class,
+                SqlReplCommand.class,
                 PicocliCommands.ClearScreen.class,
                 CommandLine.HelpCommand.class,
                 VersionCommand.class,
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/cliconfig/CliConfigGetSubCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/cliconfig/CliConfigGetSubCommand.java
index 7f33bfdd5..9ad7a23b2 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/cliconfig/CliConfigGetSubCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/cliconfig/CliConfigGetSubCommand.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.cli.commands.cliconfig;
 
 import jakarta.inject.Inject;
+import java.util.concurrent.Callable;
 import org.apache.ignite.cli.call.cliconfig.CliConfigGetCall;
 import org.apache.ignite.cli.commands.BaseCommand;
 import org.apache.ignite.cli.core.call.CallExecutionPipeline;
@@ -29,7 +30,7 @@ import picocli.CommandLine.Parameters;
  * Command to get CLI configuration parameters.
  */
 @Command(name = "get")
-public class CliConfigGetSubCommand extends BaseCommand {
+public class CliConfigGetSubCommand extends BaseCommand implements Callable<Integer> {
     @Parameters
     private String key;
 
@@ -37,8 +38,8 @@ public class CliConfigGetSubCommand extends BaseCommand {
     private CliConfigGetCall call;
 
     @Override
-    public void run() {
-        CallExecutionPipeline.builder(call)
+    public Integer call() {
+        return CallExecutionPipeline.builder(call)
                 .inputProvider(() -> new StringCallInput(key))
                 .output(spec.commandLine().getOut())
                 .errOutput(spec.commandLine().getErr())
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/cliconfig/CliConfigSetSubCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/cliconfig/CliConfigSetSubCommand.java
index 137a41fd2..f78b82d78 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/cliconfig/CliConfigSetSubCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/cliconfig/CliConfigSetSubCommand.java
@@ -19,6 +19,7 @@ package org.apache.ignite.cli.commands.cliconfig;
 
 import jakarta.inject.Inject;
 import java.util.Map;
+import java.util.concurrent.Callable;
 import org.apache.ignite.cli.call.cliconfig.CliConfigSetCall;
 import org.apache.ignite.cli.call.cliconfig.CliConfigSetCallInput;
 import org.apache.ignite.cli.commands.BaseCommand;
@@ -30,7 +31,7 @@ import picocli.CommandLine.Parameters;
  * Command to set CLI configuration parameters.
  */
 @Command(name = "set")
-public class CliConfigSetSubCommand extends BaseCommand {
+public class CliConfigSetSubCommand extends BaseCommand implements Callable<Integer> {
     @Parameters(arity = "1..*")
     private Map<String, String> parameters;
 
@@ -38,8 +39,8 @@ public class CliConfigSetSubCommand extends BaseCommand {
     private CliConfigSetCall call;
 
     @Override
-    public void run() {
-        CallExecutionPipeline.builder(call)
+    public Integer call() {
+        return CallExecutionPipeline.builder(call)
                 .inputProvider(() -> new CliConfigSetCallInput(parameters))
                 .output(spec.commandLine().getOut())
                 .errOutput(spec.commandLine().getErr())
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/cliconfig/CliConfigSubCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/cliconfig/CliConfigSubCommand.java
index 095518504..a622631db 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/cliconfig/CliConfigSubCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/cliconfig/CliConfigSubCommand.java
@@ -19,6 +19,7 @@ package org.apache.ignite.cli.commands.cliconfig;
 
 import jakarta.inject.Inject;
 import jakarta.inject.Singleton;
+import java.util.concurrent.Callable;
 import org.apache.ignite.cli.call.cliconfig.CliConfigCall;
 import org.apache.ignite.cli.commands.BaseCommand;
 import org.apache.ignite.cli.commands.decorators.ConfigDecorator;
@@ -34,14 +35,14 @@ import picocli.CommandLine.Command;
         CliConfigSetSubCommand.class
 })
 @Singleton
-public class CliConfigSubCommand extends BaseCommand {
+public class CliConfigSubCommand extends BaseCommand implements Callable<Integer> {
 
     @Inject
     private CliConfigCall call;
 
     @Override
-    public void run() {
-        CallExecutionPipeline.builder(call)
+    public Integer call() {
+        return CallExecutionPipeline.builder(call)
                 .inputProvider(EmptyCallInput::new)
                 .output(spec.commandLine().getOut())
                 .errOutput(spec.commandLine().getErr())
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/cluster/ClusterConfigShowReplSubCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/cluster/ClusterConfigShowReplSubCommand.java
index 65fd49b95..a0cd050f4 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/cluster/ClusterConfigShowReplSubCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/cluster/ClusterConfigShowReplSubCommand.java
@@ -23,8 +23,9 @@ import org.apache.ignite.cli.call.configuration.ClusterConfigShowCallInput;
 import org.apache.ignite.cli.commands.BaseCommand;
 import org.apache.ignite.cli.commands.decorators.JsonDecorator;
 import org.apache.ignite.cli.core.call.CallExecutionPipeline;
-import org.apache.ignite.cli.core.exception.ExceptionHandler;
 import org.apache.ignite.cli.core.exception.ExceptionWriter;
+import org.apache.ignite.cli.core.exception.IgniteCliApiException;
+import org.apache.ignite.cli.core.exception.handler.IgniteCliApiExceptionHandler;
 import org.apache.ignite.cli.core.repl.Session;
 import org.apache.ignite.rest.client.invoker.ApiException;
 import picocli.CommandLine.Command;
@@ -35,7 +36,7 @@ import picocli.CommandLine.Option;
  */
 @Command(name = "show",
         description = "Shows cluster configuration.")
-public class ClusterConfigShowReplSubCommand extends BaseCommand {
+public class ClusterConfigShowReplSubCommand extends BaseCommand implements Runnable {
 
     /**
      * Configuration selector option.
@@ -57,7 +58,6 @@ public class ClusterConfigShowReplSubCommand extends BaseCommand {
     @Inject
     private Session session;
 
-    /** {@inheritDoc} */
     @Override
     public void run() {
         var input = ClusterConfigShowCallInput.builder().selector(selector);
@@ -80,21 +80,18 @@ public class ClusterConfigShowReplSubCommand extends BaseCommand {
                 .runPipeline();
     }
 
-    private static class ShowConfigReplExceptionHandler implements ExceptionHandler<ApiException> {
+    private static class ShowConfigReplExceptionHandler extends IgniteCliApiExceptionHandler {
         @Override
-        public void handle(ExceptionWriter err, ApiException e) {
-            if (e.getCode() == 500) { //TODO: https://issues.apache.org/jira/browse/IGNITE-17091
-                err.write("Cannot show cluster config, probably you have not initialized the cluster. "
-                        + "Try to run 'cluster init' command.");
-                return;
+        public int handle(ExceptionWriter err, IgniteCliApiException e) {
+            if (e.getCause() instanceof ApiException) {
+                ApiException apiException = (ApiException) e.getCause();
+                if (apiException.getCode() == 500) { //TODO: https://issues.apache.org/jira/browse/IGNITE-17091
+                    err.write("Cannot show cluster config, probably you have not initialized the cluster. "
+                            + "Try to run 'cluster init' command.");
+                    return 1;
+                }
             }
-
-            err.write(e.getResponseBody());
-        }
-
-        @Override
-        public Class<ApiException> applicableException() {
-            return ApiException.class;
+            return super.handle(err, e);
         }
     }
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/cluster/ClusterConfigShowSubCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/cluster/ClusterConfigShowSubCommand.java
index 29548c47f..0a586682f 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/cluster/ClusterConfigShowSubCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/cluster/ClusterConfigShowSubCommand.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.cli.commands.configuration.cluster;
 
 import jakarta.inject.Inject;
+import java.util.concurrent.Callable;
 import org.apache.ignite.cli.call.configuration.ClusterConfigShowCall;
 import org.apache.ignite.cli.call.configuration.ClusterConfigShowCallInput;
 import org.apache.ignite.cli.commands.BaseCommand;
@@ -31,7 +32,7 @@ import picocli.CommandLine.Option;
  */
 @Command(name = "show",
         description = "Shows cluster configuration.")
-public class ClusterConfigShowSubCommand extends BaseCommand {
+public class ClusterConfigShowSubCommand extends BaseCommand implements Callable<Integer> {
 
     /**
      * Configuration selector option.
@@ -53,8 +54,8 @@ public class ClusterConfigShowSubCommand extends BaseCommand {
 
     /** {@inheritDoc} */
     @Override
-    public void run() {
-        CallExecutionPipeline.builder(call)
+    public Integer call() {
+        return CallExecutionPipeline.builder(call)
                 .inputProvider(this::buildCallInput)
                 .output(spec.commandLine().getOut())
                 .errOutput(spec.commandLine().getErr())
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/cluster/ClusterConfigUpdateReplSubCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/cluster/ClusterConfigUpdateReplSubCommand.java
index 91ada9d0e..d3f28c9ea 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/cluster/ClusterConfigUpdateReplSubCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/cluster/ClusterConfigUpdateReplSubCommand.java
@@ -34,7 +34,7 @@ import picocli.CommandLine.Parameters;
 @Command(name = "update",
         description = "Updates cluster configuration.")
 @Singleton
-public class ClusterConfigUpdateReplSubCommand extends BaseCommand {
+public class ClusterConfigUpdateReplSubCommand extends BaseCommand implements Runnable {
     /**
      * Cluster url option.
      */
@@ -69,7 +69,7 @@ public class ClusterConfigUpdateReplSubCommand extends BaseCommand {
             return;
         }
 
-        CallExecutionPipeline.builder(this.call)
+        CallExecutionPipeline.builder(call)
                 .inputProvider(input::build)
                 .output(spec.commandLine().getOut())
                 .errOutput(spec.commandLine().getErr())
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/cluster/ClusterConfigUpdateSubCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/cluster/ClusterConfigUpdateSubCommand.java
index 12aafaf0e..9504f78e0 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/cluster/ClusterConfigUpdateSubCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/cluster/ClusterConfigUpdateSubCommand.java
@@ -19,6 +19,7 @@ package org.apache.ignite.cli.commands.configuration.cluster;
 
 import jakarta.inject.Inject;
 import jakarta.inject.Singleton;
+import java.util.concurrent.Callable;
 import org.apache.ignite.cli.call.configuration.ClusterConfigUpdateCall;
 import org.apache.ignite.cli.call.configuration.ClusterConfigUpdateCallInput;
 import org.apache.ignite.cli.commands.BaseCommand;
@@ -33,7 +34,7 @@ import picocli.CommandLine.Parameters;
 @Command(name = "update",
         description = "Updates cluster configuration.")
 @Singleton
-public class ClusterConfigUpdateSubCommand extends BaseCommand {
+public class ClusterConfigUpdateSubCommand extends BaseCommand implements Callable<Integer> {
     @Inject
     ClusterConfigUpdateCall call;
 
@@ -54,8 +55,8 @@ public class ClusterConfigUpdateSubCommand extends BaseCommand {
 
     /** {@inheritDoc} */
     @Override
-    public void run() {
-        CallExecutionPipeline.builder(call)
+    public Integer call() {
+        return CallExecutionPipeline.builder(call)
                 .inputProvider(this::buildCallInput)
                 .output(spec.commandLine().getOut())
                 .errOutput(spec.commandLine().getErr())
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/node/NodeConfigShowReplSubCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/node/NodeConfigShowReplSubCommand.java
index 8cb43510f..a6894c876 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/node/NodeConfigShowReplSubCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/node/NodeConfigShowReplSubCommand.java
@@ -23,10 +23,7 @@ import org.apache.ignite.cli.call.configuration.NodeConfigShowCallInput;
 import org.apache.ignite.cli.commands.BaseCommand;
 import org.apache.ignite.cli.commands.decorators.JsonDecorator;
 import org.apache.ignite.cli.core.call.CallExecutionPipeline;
-import org.apache.ignite.cli.core.exception.ExceptionHandler;
-import org.apache.ignite.cli.core.exception.ExceptionWriter;
 import org.apache.ignite.cli.core.repl.Session;
-import org.apache.ignite.rest.client.invoker.ApiException;
 import picocli.CommandLine.Command;
 import picocli.CommandLine.Option;
 
@@ -35,7 +32,7 @@ import picocli.CommandLine.Option;
  */
 @Command(name = "show",
         description = "Shows node configuration.")
-public class NodeConfigShowReplSubCommand extends BaseCommand {
+public class NodeConfigShowReplSubCommand extends BaseCommand implements Runnable {
 
     /**
      * Configuration selector option.
@@ -75,26 +72,7 @@ public class NodeConfigShowReplSubCommand extends BaseCommand {
                 .output(spec.commandLine().getOut())
                 .errOutput(spec.commandLine().getErr())
                 .decorator(new JsonDecorator())
-                .exceptionHandler(new ShowConfigReplExceptionHandler())
                 .build()
                 .runPipeline();
     }
-
-    private static class ShowConfigReplExceptionHandler implements ExceptionHandler<ApiException> {
-        @Override
-        public void handle(ExceptionWriter err, ApiException e) {
-            if (e.getCode() == 500) { //TODO: https://issues.apache.org/jira/browse/IGNITE-17091
-                err.write("Cannot show node config, probably you have not initialized the cluster. "
-                        + "Try to run 'cluster init' command.");
-                return;
-            }
-
-            err.write(e.getResponseBody());
-        }
-
-        @Override
-        public Class<ApiException> applicableException() {
-            return ApiException.class;
-        }
-    }
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/node/NodeConfigShowSubCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/node/NodeConfigShowSubCommand.java
index 56a8de470..a1a89135c 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/node/NodeConfigShowSubCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/node/NodeConfigShowSubCommand.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.cli.commands.configuration.node;
 
 import jakarta.inject.Inject;
+import java.util.concurrent.Callable;
 import org.apache.ignite.cli.call.configuration.NodeConfigShowCall;
 import org.apache.ignite.cli.call.configuration.NodeConfigShowCallInput;
 import org.apache.ignite.cli.commands.BaseCommand;
@@ -31,7 +32,7 @@ import picocli.CommandLine.Option;
  */
 @Command(name = "show",
         description = "Shows node configuration.")
-public class NodeConfigShowSubCommand extends BaseCommand {
+public class NodeConfigShowSubCommand extends BaseCommand implements Callable<Integer> {
 
     /**
      * Configuration selector option.
@@ -53,8 +54,8 @@ public class NodeConfigShowSubCommand extends BaseCommand {
 
     /** {@inheritDoc} */
     @Override
-    public void run() {
-        CallExecutionPipeline.builder(call)
+    public Integer call() {
+        return CallExecutionPipeline.builder(call)
                 .inputProvider(this::buildCallInput)
                 .output(spec.commandLine().getOut())
                 .errOutput(spec.commandLine().getErr())
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/node/NodeConfigUpdateReplSubCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/node/NodeConfigUpdateReplSubCommand.java
index 6d3116084..0a8e74671 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/node/NodeConfigUpdateReplSubCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/node/NodeConfigUpdateReplSubCommand.java
@@ -34,7 +34,7 @@ import picocli.CommandLine.Parameters;
 @Command(name = "update",
         description = "Updates node configuration.")
 @Singleton
-public class NodeConfigUpdateReplSubCommand extends BaseCommand {
+public class NodeConfigUpdateReplSubCommand extends BaseCommand implements Runnable {
     /**
      * Node url option.
      */
@@ -69,7 +69,7 @@ public class NodeConfigUpdateReplSubCommand extends BaseCommand {
             return;
         }
 
-        CallExecutionPipeline.builder(this.call)
+        CallExecutionPipeline.builder(call)
                 .inputProvider(input::build)
                 .output(spec.commandLine().getOut())
                 .errOutput(spec.commandLine().getErr())
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/node/NodeConfigUpdateSubCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/node/NodeConfigUpdateSubCommand.java
index 08a3125e5..30896902e 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/node/NodeConfigUpdateSubCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/configuration/node/NodeConfigUpdateSubCommand.java
@@ -19,6 +19,7 @@ package org.apache.ignite.cli.commands.configuration.node;
 
 import jakarta.inject.Inject;
 import jakarta.inject.Singleton;
+import java.util.concurrent.Callable;
 import org.apache.ignite.cli.call.configuration.NodeConfigUpdateCall;
 import org.apache.ignite.cli.call.configuration.NodeConfigUpdateCallInput;
 import org.apache.ignite.cli.commands.BaseCommand;
@@ -33,9 +34,7 @@ import picocli.CommandLine.Parameters;
 @Command(name = "update",
         description = "Updates node configuration.")
 @Singleton
-public class NodeConfigUpdateSubCommand extends BaseCommand {
-    @Inject
-    NodeConfigUpdateCall call;
+public class NodeConfigUpdateSubCommand extends BaseCommand implements Callable<Integer> {
     /**
      * Node url option.
      */
@@ -44,16 +43,20 @@ public class NodeConfigUpdateSubCommand extends BaseCommand {
             descriptionKey = "ignite.cluster-url", defaultValue = "http://localhost:10300"
     )
     private String nodeUrl;
+
     /**
      * Configuration that will be updated.
      */
     @Parameters(index = "0")
     private String config;
 
+    @Inject
+    private NodeConfigUpdateCall call;
+
     /** {@inheritDoc} */
     @Override
-    public void run() {
-        CallExecutionPipeline.builder(call)
+    public Integer call() {
+        return CallExecutionPipeline.builder(call)
                 .inputProvider(this::buildCallInput)
                 .output(spec.commandLine().getOut())
                 .errOutput(spec.commandLine().getErr())
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/connect/ConnectCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/connect/ConnectCommand.java
index 32a6e534a..d73a19a49 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/connect/ConnectCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/connect/ConnectCommand.java
@@ -24,16 +24,14 @@ import org.apache.ignite.cli.call.connect.ConnectCallInput;
 import org.apache.ignite.cli.commands.BaseCommand;
 import org.apache.ignite.cli.core.call.CallExecutionPipeline;
 import picocli.CommandLine.Command;
-import picocli.CommandLine.Model.CommandSpec;
 import picocli.CommandLine.Parameters;
-import picocli.CommandLine.Spec;
 
 /**
  * Connects to the Ignite 3 node.
  */
 @Command(name = "connect", description = "Connect to Ignite 3 node.")
 @Singleton
-public class ConnectCommand extends BaseCommand {
+public class ConnectCommand extends BaseCommand implements Runnable {
 
     /**
      * Cluster url option.
@@ -44,9 +42,6 @@ public class ConnectCommand extends BaseCommand {
     )
     private String nodeUrl;
 
-    @Spec
-    private CommandSpec spec;
-
     @Inject
     private ConnectCall connectCall;
 
@@ -54,10 +49,16 @@ public class ConnectCommand extends BaseCommand {
     @Override
     public void run() {
         CallExecutionPipeline.builder(connectCall)
-                .inputProvider(() -> ConnectCallInput.builder().nodeUrl(nodeUrl).build())
+                .inputProvider(this::buildCallInput)
                 .output(spec.commandLine().getOut())
                 .errOutput(spec.commandLine().getErr())
                 .build()
                 .runPipeline();
     }
+
+    private ConnectCallInput buildCallInput() {
+        return ConnectCallInput.builder()
+                .nodeUrl(nodeUrl)
+                .build();
+    }
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/connect/DisconnectCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/connect/DisconnectCommand.java
index 588989507..a6da5d1a4 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/connect/DisconnectCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/connect/DisconnectCommand.java
@@ -24,18 +24,13 @@ import org.apache.ignite.cli.commands.BaseCommand;
 import org.apache.ignite.cli.core.call.CallExecutionPipeline;
 import org.apache.ignite.cli.core.call.EmptyCallInput;
 import picocli.CommandLine.Command;
-import picocli.CommandLine.Model.CommandSpec;
-import picocli.CommandLine.Spec;
 
 /**
  * Connects to the Ignite 3 node.
  */
 @Command(name = "disconnect", description = "Disconnect from Ignite 3 node.")
 @Singleton
-public class DisconnectCommand extends BaseCommand {
-    @Spec
-    private CommandSpec spec;
-
+public class DisconnectCommand extends BaseCommand implements Runnable {
     @Inject
     private DisconnectCall disconnectCall;
 
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/sql/SqlCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/sql/SqlCommand.java
index 986f4e2a8..8b68f8323 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/sql/SqlCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/sql/SqlCommand.java
@@ -17,29 +17,22 @@
 
 package org.apache.ignite.cli.commands.sql;
 
-import jakarta.inject.Inject;
 import java.io.File;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.sql.SQLException;
+import java.util.concurrent.Callable;
+import org.apache.ignite.cli.call.sql.SqlQueryCall;
 import org.apache.ignite.cli.commands.BaseCommand;
 import org.apache.ignite.cli.commands.decorators.SqlQueryResultDecorator;
-import org.apache.ignite.cli.core.CallExecutionPipelineProvider;
 import org.apache.ignite.cli.core.call.CallExecutionPipeline;
 import org.apache.ignite.cli.core.call.StringCallInput;
-import org.apache.ignite.cli.core.exception.CommandExecutionException;
-import org.apache.ignite.cli.core.exception.ExceptionHandlers;
 import org.apache.ignite.cli.core.exception.ExceptionWriter;
-import org.apache.ignite.cli.core.exception.handler.DefaultExceptionHandlers;
 import org.apache.ignite.cli.core.exception.handler.SqlExceptionHandler;
-import org.apache.ignite.cli.core.repl.Repl;
-import org.apache.ignite.cli.core.repl.executor.RegistryCommandExecutor;
-import org.apache.ignite.cli.core.repl.executor.ReplExecutorProvider;
-import org.apache.ignite.cli.core.repl.executor.SqlQueryCall;
+import org.apache.ignite.cli.deprecated.IgniteCliException;
 import org.apache.ignite.cli.sql.SqlManager;
-import org.apache.ignite.cli.sql.SqlSchemaProvider;
-import org.jetbrains.annotations.NotNull;
+import picocli.CommandLine.ArgGroup;
 import picocli.CommandLine.Command;
 import picocli.CommandLine.Option;
 
@@ -47,27 +40,28 @@ import picocli.CommandLine.Option;
  * Command for sql execution.
  */
 @Command(name = "sql", description = "Executes SQL query.")
-public class SqlCommand extends BaseCommand {
-    private static final String INTERNAL_COMMAND_PREFIX = "!";
+public class SqlCommand extends BaseCommand implements Callable<Integer> {
 
     @Option(names = {"-u", "--jdbc-url"}, required = true,
             descriptionKey = "ignite.jdbc-url", description = "JDBC url to ignite cluster")
     private String jdbc;
 
-    @Option(names = {"-e", "--execute", "--exec"}) //todo: can be passed as parameter, not option (see IEP-88)
-    private String command;
+    @ArgGroup(multiplicity = "1")
+    private ExecOptions execOptions;
 
-    @Option(names = {"-f", "--script-file"})
-    private File file;
+    private static class ExecOptions {
+        @Option(names = {"-e", "--execute", "--exec"}) //todo: can be passed as parameter, not option (see IGNITE-17209)
+        private String command;
 
-    @Inject
-    private ReplExecutorProvider replExecutorProvider;
+        @Option(names = {"-f", "--script-file"})
+        private File file;
+    }
 
     private static String extract(File file) {
         try {
             return String.join("\n", Files.readAllLines(file.toPath(), StandardCharsets.UTF_8));
         } catch (IOException e) {
-            throw new CommandExecutionException("sql", "File with command not found.");
+            throw new IgniteCliException("File with command not found.");
         }
     }
 
@@ -75,51 +69,18 @@ public class SqlCommand extends BaseCommand {
      * {@inheritDoc}
      */
     @Override
-    public void run() {
+    public Integer call() {
         try (SqlManager sqlManager = new SqlManager(jdbc)) {
-            if (command == null && file == null) {
-                replExecutorProvider.get().execute(Repl.builder()
-                        .withPromptProvider(() -> "sql-cli> ")
-                        .withCompleter(new SqlCompleter(new SqlSchemaProvider(sqlManager::getMetadata)))
-                        .withCommandClass(SqlReplTopLevelCliCommand.class)
-                        .withCallExecutionPipelineProvider(provider(sqlManager))
-                        .withHistoryFileName("sqlhistory")
-                        .build());
-            } else {
-                String executeCommand = file != null ? extract(file) : command;
-                createSqlExecPipeline(sqlManager, executeCommand).runPipeline();
-            }
+            String executeCommand = execOptions.file != null ? extract(execOptions.file) : execOptions.command;
+            return CallExecutionPipeline.builder(new SqlQueryCall(sqlManager))
+                    .inputProvider(() -> new StringCallInput(executeCommand))
+                    .output(spec.commandLine().getOut())
+                    .errOutput(spec.commandLine().getErr())
+                    .decorator(new SqlQueryResultDecorator())
+                    .build().runPipeline();
         } catch (SQLException e) {
-            new SqlExceptionHandler().handle(ExceptionWriter.fromPrintWriter(spec.commandLine().getErr()), e);
+            return new SqlExceptionHandler().handle(ExceptionWriter.fromPrintWriter(spec.commandLine().getErr()), e);
         }
     }
 
-    @NotNull
-    private CallExecutionPipelineProvider provider(SqlManager sqlManager) {
-        return (call, exceptionHandlers, line) -> line.startsWith(INTERNAL_COMMAND_PREFIX)
-                ? createInternalCommandPipeline(call, exceptionHandlers, line)
-                : createSqlExecPipeline(sqlManager, line);
-    }
-
-    private CallExecutionPipeline<?, ?> createSqlExecPipeline(SqlManager sqlManager, String line) {
-        return CallExecutionPipeline.builder(new SqlQueryCall(sqlManager))
-                .inputProvider(() -> new StringCallInput(line))
-                .output(spec.commandLine().getOut())
-                .errOutput(spec.commandLine().getErr())
-                .exceptionHandlers(new DefaultExceptionHandlers())
-                .decorator(new SqlQueryResultDecorator())
-                .build();
-    }
-
-    private CallExecutionPipeline<?, ?> createInternalCommandPipeline(RegistryCommandExecutor call,
-            ExceptionHandlers exceptionHandlers,
-            String line) {
-        return CallExecutionPipeline.builder(call)
-                .inputProvider(() -> new StringCallInput(line.substring(INTERNAL_COMMAND_PREFIX.length())))
-                .output(spec.commandLine().getOut())
-                .errOutput(spec.commandLine().getErr())
-                .exceptionHandlers(new DefaultExceptionHandlers())
-                .exceptionHandlers(exceptionHandlers)
-                .build();
-    }
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/sql/SqlCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/sql/SqlReplCommand.java
similarity index 83%
copy from modules/cli/src/main/java/org/apache/ignite/cli/commands/sql/SqlCommand.java
copy to modules/cli/src/main/java/org/apache/ignite/cli/commands/sql/SqlReplCommand.java
index 986f4e2a8..14bd83dba 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/sql/SqlCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/sql/SqlReplCommand.java
@@ -23,42 +23,46 @@ import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.sql.SQLException;
+import org.apache.ignite.cli.call.sql.SqlQueryCall;
 import org.apache.ignite.cli.commands.BaseCommand;
 import org.apache.ignite.cli.commands.decorators.SqlQueryResultDecorator;
 import org.apache.ignite.cli.core.CallExecutionPipelineProvider;
 import org.apache.ignite.cli.core.call.CallExecutionPipeline;
 import org.apache.ignite.cli.core.call.StringCallInput;
-import org.apache.ignite.cli.core.exception.CommandExecutionException;
 import org.apache.ignite.cli.core.exception.ExceptionHandlers;
 import org.apache.ignite.cli.core.exception.ExceptionWriter;
-import org.apache.ignite.cli.core.exception.handler.DefaultExceptionHandlers;
 import org.apache.ignite.cli.core.exception.handler.SqlExceptionHandler;
 import org.apache.ignite.cli.core.repl.Repl;
 import org.apache.ignite.cli.core.repl.executor.RegistryCommandExecutor;
 import org.apache.ignite.cli.core.repl.executor.ReplExecutorProvider;
-import org.apache.ignite.cli.core.repl.executor.SqlQueryCall;
+import org.apache.ignite.cli.deprecated.IgniteCliException;
 import org.apache.ignite.cli.sql.SqlManager;
 import org.apache.ignite.cli.sql.SqlSchemaProvider;
-import org.jetbrains.annotations.NotNull;
+import picocli.CommandLine.ArgGroup;
 import picocli.CommandLine.Command;
 import picocli.CommandLine.Option;
 
 /**
- * Command for sql execution.
+ * Command for sql execution in REPL mode.
  */
 @Command(name = "sql", description = "Executes SQL query.")
-public class SqlCommand extends BaseCommand {
+public class SqlReplCommand extends BaseCommand implements Runnable {
     private static final String INTERNAL_COMMAND_PREFIX = "!";
 
     @Option(names = {"-u", "--jdbc-url"}, required = true,
             descriptionKey = "ignite.jdbc-url", description = "JDBC url to ignite cluster")
     private String jdbc;
 
-    @Option(names = {"-e", "--execute", "--exec"}) //todo: can be passed as parameter, not option (see IEP-88)
-    private String command;
+    @ArgGroup
+    private ExecOptions execOptions;
 
-    @Option(names = {"-f", "--script-file"})
-    private File file;
+    private static class ExecOptions {
+        @Option(names = {"-e", "--execute", "--exec"}) //todo: can be passed as parameter, not option (see IEP-88)
+        private String command;
+
+        @Option(names = {"-f", "--script-file"})
+        private File file;
+    }
 
     @Inject
     private ReplExecutorProvider replExecutorProvider;
@@ -67,7 +71,7 @@ public class SqlCommand extends BaseCommand {
         try {
             return String.join("\n", Files.readAllLines(file.toPath(), StandardCharsets.UTF_8));
         } catch (IOException e) {
-            throw new CommandExecutionException("sql", "File with command not found.");
+            throw new IgniteCliException("File with command not found.");
         }
     }
 
@@ -77,7 +81,7 @@ public class SqlCommand extends BaseCommand {
     @Override
     public void run() {
         try (SqlManager sqlManager = new SqlManager(jdbc)) {
-            if (command == null && file == null) {
+            if (execOptions == null) {
                 replExecutorProvider.get().execute(Repl.builder()
                         .withPromptProvider(() -> "sql-cli> ")
                         .withCompleter(new SqlCompleter(new SqlSchemaProvider(sqlManager::getMetadata)))
@@ -86,7 +90,7 @@ public class SqlCommand extends BaseCommand {
                         .withHistoryFileName("sqlhistory")
                         .build());
             } else {
-                String executeCommand = file != null ? extract(file) : command;
+                String executeCommand = execOptions.file != null ? extract(execOptions.file) : execOptions.command;
                 createSqlExecPipeline(sqlManager, executeCommand).runPipeline();
             }
         } catch (SQLException e) {
@@ -94,7 +98,6 @@ public class SqlCommand extends BaseCommand {
         }
     }
 
-    @NotNull
     private CallExecutionPipelineProvider provider(SqlManager sqlManager) {
         return (call, exceptionHandlers, line) -> line.startsWith(INTERNAL_COMMAND_PREFIX)
                 ? createInternalCommandPipeline(call, exceptionHandlers, line)
@@ -106,7 +109,6 @@ public class SqlCommand extends BaseCommand {
                 .inputProvider(() -> new StringCallInput(line))
                 .output(spec.commandLine().getOut())
                 .errOutput(spec.commandLine().getErr())
-                .exceptionHandlers(new DefaultExceptionHandlers())
                 .decorator(new SqlQueryResultDecorator())
                 .build();
     }
@@ -118,7 +120,6 @@ public class SqlCommand extends BaseCommand {
                 .inputProvider(() -> new StringCallInput(line.substring(INTERNAL_COMMAND_PREFIX.length())))
                 .output(spec.commandLine().getOut())
                 .errOutput(spec.commandLine().getErr())
-                .exceptionHandlers(new DefaultExceptionHandlers())
                 .exceptionHandlers(exceptionHandlers)
                 .build();
     }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/status/StatusCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/status/StatusCommand.java
index c2be4f3aa..f380046b7 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/status/StatusCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/status/StatusCommand.java
@@ -19,15 +19,14 @@ package org.apache.ignite.cli.commands.status;
 
 import jakarta.inject.Inject;
 import jakarta.inject.Singleton;
+import java.util.concurrent.Callable;
 import org.apache.ignite.cli.call.status.StatusCall;
 import org.apache.ignite.cli.commands.BaseCommand;
 import org.apache.ignite.cli.commands.decorators.StatusDecorator;
 import org.apache.ignite.cli.core.call.CallExecutionPipeline;
 import org.apache.ignite.cli.core.call.StatusCallInput;
 import picocli.CommandLine.Command;
-import picocli.CommandLine.Model.CommandSpec;
 import picocli.CommandLine.Option;
-import picocli.CommandLine.Spec;
 
 /**
  * Command that prints status of ignite cluster.
@@ -36,7 +35,7 @@ import picocli.CommandLine.Spec;
         aliases = "cluster show", //TODO: https://issues.apache.org/jira/browse/IGNITE-17102
         description = "Prints status of the cluster.")
 @Singleton
-public class StatusCommand extends BaseCommand {
+public class StatusCommand extends BaseCommand implements Callable<Integer> {
 
     /**
      * Cluster url option.
@@ -48,19 +47,16 @@ public class StatusCommand extends BaseCommand {
     )
     private String clusterUrl;
 
-    @Spec
-    private CommandSpec commandSpec;
-
     @Inject
     private StatusCall statusCall;
 
     /** {@inheritDoc} */
     @Override
-    public void run() {
-        CallExecutionPipeline.builder(statusCall)
+    public Integer call() {
+        return CallExecutionPipeline.builder(statusCall)
                 .inputProvider(() -> new StatusCallInput(clusterUrl))
-                .output(commandSpec.commandLine().getOut())
-                .errOutput(commandSpec.commandLine().getErr())
+                .output(spec.commandLine().getOut())
+                .errOutput(spec.commandLine().getErr())
                 .decorator(new StatusDecorator())
                 .build()
                 .runPipeline();
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/status/StatusReplCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/status/StatusReplCommand.java
index 4ebb924ca..a4847df7a 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/status/StatusReplCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/status/StatusReplCommand.java
@@ -25,16 +25,14 @@ import org.apache.ignite.cli.commands.decorators.StatusReplDecorator;
 import org.apache.ignite.cli.core.call.CallExecutionPipeline;
 import org.apache.ignite.cli.core.call.EmptyCallInput;
 import picocli.CommandLine.Command;
-import picocli.CommandLine.Model.CommandSpec;
 import picocli.CommandLine.Option;
-import picocli.CommandLine.Spec;
 
 /**
  * Command that prints status of ignite cluster.
  */
 @Command(name = "status", description = "Prints status of the cluster.")
 @Singleton
-public class StatusReplCommand extends BaseCommand {
+public class StatusReplCommand extends BaseCommand implements Runnable {
 
     /**
      * Cluster url option.
@@ -45,9 +43,6 @@ public class StatusReplCommand extends BaseCommand {
     )
     private String clusterUrl;
 
-    @Spec
-    private CommandSpec commandSpec;
-
     @Inject
     private StatusReplCall statusReplCall;
 
@@ -56,8 +51,8 @@ public class StatusReplCommand extends BaseCommand {
     public void run() {
         CallExecutionPipeline.builder(statusReplCall)
                 .inputProvider(EmptyCallInput::new)
-                .output(commandSpec.commandLine().getOut())
-                .errOutput(commandSpec.commandLine().getErr())
+                .output(spec.commandLine().getOut())
+                .errOutput(spec.commandLine().getErr())
                 .decorator(new StatusReplDecorator())
                 .build()
                 .runPipeline();
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/topology/TopologyCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/topology/TopologyCommand.java
index 872238fb6..e01a888a4 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/topology/TopologyCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/topology/TopologyCommand.java
@@ -18,18 +18,17 @@
 package org.apache.ignite.cli.commands.topology;
 
 import jakarta.inject.Singleton;
+import java.util.concurrent.Callable;
 import org.apache.ignite.cli.commands.BaseCommand;
 import picocli.CommandLine.Command;
-import picocli.CommandLine.Model.CommandSpec;
 import picocli.CommandLine.Option;
-import picocli.CommandLine.Spec;
 
 /**
  * Command that prints ignite cluster topology.
  */
 @Command(name = "topology", description = "Prints topology information.")
 @Singleton
-public class TopologyCommand extends BaseCommand {
+public class TopologyCommand extends BaseCommand implements Callable<Integer> {
 
     /**
      * Cluster url option.
@@ -41,13 +40,11 @@ public class TopologyCommand extends BaseCommand {
     )
     private String clusterUrl;
 
-    @Spec
-    private CommandSpec commandSpec;
-
     /** {@inheritDoc} */
     @Override
-    public void run() {
+    public Integer call() {
         //TODO: https://issues.apache.org/jira/browse/IGNITE-17092
-        commandSpec.commandLine().getOut().println("Topology command is not implemented yet.");
+        spec.commandLine().getOut().println("Topology command is not implemented yet.");
+        return 0;
     }
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/version/VersionCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/version/VersionCommand.java
index 18f5946e5..046d319e8 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/version/VersionCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/version/VersionCommand.java
@@ -20,26 +20,21 @@ package org.apache.ignite.cli.commands.version;
 import jakarta.inject.Inject;
 import jakarta.inject.Singleton;
 import org.apache.ignite.cli.VersionProvider;
+import org.apache.ignite.cli.commands.BaseCommand;
 import picocli.CommandLine.Command;
-import picocli.CommandLine.Model.CommandSpec;
-import picocli.CommandLine.Spec;
 
 /**
  * Command that prints CLI version.
  */
 @Command(name = "version", description = "Prints CLI version.")
 @Singleton
-public class VersionCommand implements Runnable {
-
-    @Spec
-    private CommandSpec commandSpec;
+public class VersionCommand extends BaseCommand implements Runnable {
 
     @Inject
     private VersionProvider versionProvider;
 
-    /** {@inheritDoc} */
     @Override
     public void run() {
-        commandSpec.commandLine().getOut().println(versionProvider.getVersion()[0]);
+        spec.commandLine().getOut().println(versionProvider.getVersion()[0]);
     }
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/core/call/CallExecutionPipeline.java b/modules/cli/src/main/java/org/apache/ignite/cli/core/call/CallExecutionPipeline.java
index 578ea02ae..0b75a2de8 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/core/call/CallExecutionPipeline.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/core/call/CallExecutionPipeline.java
@@ -90,24 +90,25 @@ public class CallExecutionPipeline<I extends CallInput, T> {
         return new CallExecutionPipelineBuilder<>(call);
     }
 
-    /** {@inheritDoc} */
-    public void runPipeline() {
+    /**
+     * Runs the pipeline.
+     *
+     * @return exit code.
+     */
+    public int runPipeline() {
         I callInput = inputProvider.get();
 
         CallOutput<T> callOutput = call.execute(callInput);
 
         if (callOutput.hasError()) {
-            exceptionHandlers.handleException(ExceptionWriter.fromPrintWriter(errOutput), callOutput.errorCause());
-            return;
+            return exceptionHandlers.handleException(ExceptionWriter.fromPrintWriter(errOutput), callOutput.errorCause());
         }
 
-        if (callOutput.isEmpty()) {
-            return;
+        if (!callOutput.isEmpty()) {
+            TerminalOutput decoratedOutput = decorator.decorate(callOutput.body());
+            output.println(decoratedOutput.toTerminalString());
         }
-
-        TerminalOutput decoratedOutput = decorator.decorate(callOutput.body());
-
-        output.println(decoratedOutput.toTerminalString());
+        return 0;
     }
 
     /** Builder for {@link CallExecutionPipeline}. */
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/core/call/StatusCallInput.java b/modules/cli/src/main/java/org/apache/ignite/cli/core/call/StatusCallInput.java
index 74aab7f89..8d2dad95e 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/core/call/StatusCallInput.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/core/call/StatusCallInput.java
@@ -30,4 +30,5 @@ public class StatusCallInput implements CallInput {
     public String getClusterUrl() {
         return clusterUrl;
     }
+
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/ExceptionHandler.java b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/ExceptionHandler.java
index e16798b69..99ea03858 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/ExceptionHandler.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/ExceptionHandler.java
@@ -30,9 +30,10 @@ public interface ExceptionHandler<T extends Throwable> {
 
     ExceptionHandler<Throwable> DEFAULT = new ExceptionHandler<>() {
         @Override
-        public void handle(ExceptionWriter err, Throwable e) {
+        public int handle(ExceptionWriter err, Throwable e) {
             logger.error("Unhandled exception ", e);
             err.write("Internal error!");
+            return 1;
         }
 
         @Override
@@ -42,12 +43,13 @@ public interface ExceptionHandler<T extends Throwable> {
     };
 
     /**
-     * Handler method.
+     * Handles an exception.
      *
      * @param err writer instance for any error messages.
      * @param e exception instance.
+     * @return exit code.
      */
-    void handle(ExceptionWriter err, T e);
+    int handle(ExceptionWriter err, T e);
 
     /**
      * Exception class getter.
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/ExceptionHandlers.java b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/ExceptionHandlers.java
index 09437e679..d2a6c69c5 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/ExceptionHandlers.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/ExceptionHandlers.java
@@ -56,23 +56,24 @@ public class ExceptionHandlers {
     }
 
     /**
-     * Handle method.
+     * Handles an exception.
      *
      * @param errOutput error output.
      * @param e exception instance.
      * @param <T> exception type.
+     * @return exit code.
      */
-    public <T extends Throwable> void handleException(ExceptionWriter errOutput, T e) {
-        processException(errOutput, e instanceof WrappedException ? e.getCause() : e);
+    public <T extends Throwable> int handleException(ExceptionWriter errOutput, T e) {
+        return processException(errOutput, e instanceof WrappedException ? e.getCause() : e);
     }
 
     @SuppressWarnings("unchecked")
-    private <T extends Throwable> void processException(ExceptionWriter errOutput, T e) {
+    private <T extends Throwable> int processException(ExceptionWriter errOutput, T e) {
         ExceptionHandler<T> exceptionHandler = (ExceptionHandler<T>) map.get(e.getClass());
         if (exceptionHandler != null) {
-            exceptionHandler.handle(errOutput, e);
+            return exceptionHandler.handle(errOutput, e);
         } else {
-            defaultHandler.handle(errOutput, e);
+            return defaultHandler.handle(errOutput, e);
         }
     }
 
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/ApiExceptionHandler.java b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/IgniteCliApiException.java
similarity index 57%
rename from modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/ApiExceptionHandler.java
rename to modules/cli/src/main/java/org/apache/ignite/cli/core/exception/IgniteCliApiException.java
index f581f579a..e247be9d6 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/ApiExceptionHandler.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/IgniteCliApiException.java
@@ -15,23 +15,32 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.cli.core.exception.handler;
-
-import org.apache.ignite.cli.core.exception.ExceptionHandler;
-import org.apache.ignite.cli.core.exception.ExceptionWriter;
-import org.apache.ignite.rest.client.invoker.ApiException;
+package org.apache.ignite.cli.core.exception;
 
 /**
- * Exception handler for {@link ApiException}.
+ * Top level runtime exception for throwing the error message from REST API to user.
  */
-public class ApiExceptionHandler implements ExceptionHandler<ApiException> {
-    @Override
-    public void handle(ExceptionWriter err, ApiException e) {
-        err.write("Api error: " + e.getCause());
+public class IgniteCliApiException extends RuntimeException {
+
+    private final String url;
+
+    /**
+     * Creates a new instance of {@code IgniteCliApiException}.
+     *
+     * @param cause the cause.
+     * @param url endpoint URL.
+     */
+    public IgniteCliApiException(Throwable cause, String url) {
+        super(cause);
+        this.url = url;
     }
 
-    @Override
-    public Class<ApiException> applicableException() {
-        return ApiException.class;
+    /**
+     * Gets the endpoint URL.
+     *
+     * @return endpoint URL.
+     */
+    public String getUrl() {
+        return url;
     }
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/CommandExecutionExceptionHandler.java b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/CommandExecutionExceptionHandler.java
deleted file mode 100644
index 36240644f..000000000
--- a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/CommandExecutionExceptionHandler.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ignite.cli.core.exception.handler;
-
-import org.apache.ignite.cli.core.exception.CommandExecutionException;
-import org.apache.ignite.cli.core.exception.ExceptionHandler;
-import org.apache.ignite.cli.core.exception.ExceptionWriter;
-import org.apache.ignite.lang.IgniteLogger;
-
-/**
- * Exception handler for {@link CommandExecutionException}.
- */
-public class CommandExecutionExceptionHandler implements ExceptionHandler<CommandExecutionException> {
-    private static final IgniteLogger log = IgniteLogger.forClass(CommandExecutionExceptionHandler.class);
-
-    @Override
-    public void handle(ExceptionWriter err, CommandExecutionException e) {
-        log.error("Command {} failed with reason {}", e.getCommandId(), e.getReason(), e);
-        err.write(String.format("Command %s failed with reason: %s", e.getCommandId(), e.getReason()));
-    }
-
-    @Override
-    public Class<CommandExecutionException> applicableException() {
-        return CommandExecutionException.class;
-    }
-}
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/ConnectCommandExceptionHandler.java b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/ConnectCommandExceptionHandler.java
deleted file mode 100644
index 2519fda17..000000000
--- a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/ConnectCommandExceptionHandler.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ignite.cli.core.exception.handler;
-
-import org.apache.ignite.cli.core.exception.ConnectCommandException;
-import org.apache.ignite.cli.core.exception.ExceptionHandler;
-import org.apache.ignite.cli.core.exception.ExceptionWriter;
-
-/**
- * Exception handler for {@link ConnectCommandException}.
- */
-public class ConnectCommandExceptionHandler implements ExceptionHandler<ConnectCommandException> {
-    @Override
-    public void handle(ExceptionWriter err, ConnectCommandException e) {
-        err.write(e.getReason());
-    }
-
-    @Override
-    public Class<ConnectCommandException> applicableException() {
-        return ConnectCommandException.class;
-    }
-}
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/ConnectExceptionHandler.java b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/ConnectExceptionHandler.java
deleted file mode 100644
index 516e2e6a9..000000000
--- a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/ConnectExceptionHandler.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ignite.cli.core.exception.handler;
-
-import java.net.ConnectException;
-import org.apache.ignite.cli.core.exception.ExceptionHandler;
-import org.apache.ignite.cli.core.exception.ExceptionWriter;
-
-/**
- * Exception handler for {@link ConnectException}.
- */
-public class ConnectExceptionHandler implements ExceptionHandler<ConnectException> {
-    @Override
-    public void handle(ExceptionWriter err, ConnectException e) {
-        err.write("Connection failed " + e.getMessage());
-    }
-
-    @Override
-    public Class<ConnectException> applicableException() {
-        return ConnectException.class;
-    }
-}
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/DefaultExceptionHandlers.java b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/DefaultExceptionHandlers.java
index 175fb7e71..ca00a9615 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/DefaultExceptionHandlers.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/DefaultExceptionHandlers.java
@@ -29,13 +29,10 @@ public class DefaultExceptionHandlers extends ExceptionHandlers {
      */
     public DefaultExceptionHandlers() {
         addExceptionHandler(new SqlExceptionHandler());
-        addExceptionHandler(new ConnectCommandExceptionHandler());
-        addExceptionHandler(new CommandExecutionExceptionHandler());
         addExceptionHandler(new TimeoutExceptionHandler());
         addExceptionHandler(new IgniteClientExceptionHandler());
         addExceptionHandler(new IgniteCliExceptionHandler());
-        addExceptionHandler(new ConnectExceptionHandler());
-        addExceptionHandler(new ApiExceptionHandler());
+        addExceptionHandler(new IgniteCliApiExceptionHandler());
         addExceptionHandler(new UnknownCommandExceptionHandler());
     }
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/EndOfFileExceptionHandler.java b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/EndOfFileExceptionHandler.java
index 64d1f85ff..cf0698a40 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/EndOfFileExceptionHandler.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/EndOfFileExceptionHandler.java
@@ -44,8 +44,9 @@ public class EndOfFileExceptionHandler implements ExceptionHandler<EndOfFileExce
     }
 
     @Override
-    public void handle(ExceptionWriter err, EndOfFileException e) {
+    public int handle(ExceptionWriter err, EndOfFileException e) {
         endAction.accept(true);
+        return 1;
     }
 
     @Override
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/IgniteCliApiExceptionHandler.java b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/IgniteCliApiExceptionHandler.java
new file mode 100644
index 000000000..56f2b1341
--- /dev/null
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/IgniteCliApiExceptionHandler.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.cli.core.exception.handler;
+
+import java.net.ConnectException;
+import java.net.UnknownHostException;
+import org.apache.ignite.cli.core.exception.ExceptionHandler;
+import org.apache.ignite.cli.core.exception.ExceptionWriter;
+import org.apache.ignite.cli.core.exception.IgniteCliApiException;
+import org.apache.ignite.lang.IgniteLogger;
+import org.apache.ignite.rest.client.invoker.ApiException;
+
+/**
+ * Exception handler for {@link IgniteCliApiException}.
+ */
+public class IgniteCliApiExceptionHandler implements ExceptionHandler<IgniteCliApiException> {
+    private static final IgniteLogger log = IgniteLogger.forClass(IgniteCliApiExceptionHandler.class);
+
+    @Override
+    public int handle(ExceptionWriter err, IgniteCliApiException e) {
+        String message;
+
+        if (e.getCause() instanceof ApiException) {
+            ApiException cause = (ApiException) e.getCause();
+            Throwable apiCause = cause.getCause();
+            if (apiCause instanceof UnknownHostException) {
+                message = "Could not determine IP address when connecting to URL: " + e.getUrl();
+            } else if (apiCause instanceof ConnectException) {
+                message = "Could not connect to URL: " + e.getUrl();
+            } else if (apiCause != null) {
+                message = apiCause.getMessage();
+            } else {
+                message = "An error occurred, error code: " + cause.getCode() + ", response: " + cause.getResponseBody();
+            }
+        } else {
+            message = e.getCause() != e ? e.getCause().getMessage() : e.getMessage();
+        }
+
+        log.error(message, e);
+
+        err.write(message);
+
+        return 1;
+    }
+
+    @Override
+    public Class<IgniteCliApiException> applicableException() {
+        return IgniteCliApiException.class;
+    }
+}
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/IgniteCliExceptionHandler.java b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/IgniteCliExceptionHandler.java
index 03cf5a72b..0d9862080 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/IgniteCliExceptionHandler.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/IgniteCliExceptionHandler.java
@@ -26,8 +26,9 @@ import org.apache.ignite.cli.deprecated.IgniteCliException;
  */
 public class IgniteCliExceptionHandler implements ExceptionHandler<IgniteCliException> {
     @Override
-    public void handle(ExceptionWriter err, IgniteCliException e) {
+    public int handle(ExceptionWriter err, IgniteCliException e) {
         err.write(e.getMessage());
+        return 1;
     }
 
     @Override
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/IgniteClientExceptionHandler.java b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/IgniteClientExceptionHandler.java
index 765d3e280..b013427ab 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/IgniteClientExceptionHandler.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/IgniteClientExceptionHandler.java
@@ -29,9 +29,10 @@ public class IgniteClientExceptionHandler implements ExceptionHandler<IgniteClie
     private static final IgniteLogger log = IgniteLogger.forClass(IgniteClientExceptionHandler.class);
 
     @Override
-    public void handle(ExceptionWriter err, IgniteClientException e) {
+    public int handle(ExceptionWriter err, IgniteClientException e) {
         log.error("Ignite client exception", e);
         err.write("Ignite client exception with code: " + e.errorCode());
+        return 1;
     }
 
     @Override
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/PicocliExecutionExceptionHandler.java b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/PicocliExecutionExceptionHandler.java
index 939db3c14..9c96a8b0d 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/PicocliExecutionExceptionHandler.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/PicocliExecutionExceptionHandler.java
@@ -30,7 +30,6 @@ public class PicocliExecutionExceptionHandler implements IExecutionExceptionHand
 
     @Override
     public int handleExecutionException(Exception ex, CommandLine commandLine, ParseResult parseResult) {
-        exceptionHandlers.handleException(System.err::println, ex);
-        return 1;
+        return exceptionHandlers.handleException(System.err::println, ex);
     }
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/SqlExceptionHandler.java b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/SqlExceptionHandler.java
index 3f60ad941..af1f103b7 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/SqlExceptionHandler.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/SqlExceptionHandler.java
@@ -38,7 +38,7 @@ public class SqlExceptionHandler implements ExceptionHandler<SQLException> {
     public static final String CONNECTION_BROKE_MESSAGE = "Connection error.";
 
     @Override
-    public void handle(ExceptionWriter err, SQLException e) {
+    public int handle(ExceptionWriter err, SQLException e) {
         switch (e.getSQLState()) {
             case SqlStateCode.CONNECTION_FAILURE:
             case SqlStateCode.CONNECTION_CLOSED:
@@ -58,6 +58,7 @@ public class SqlExceptionHandler implements ExceptionHandler<SQLException> {
                 log.error("Unrecognized error ", e);
                 err.write("Unrecognized error while process SQL query.");
         }
+        return 1;
     }
 
     @Override
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/TimeoutExceptionHandler.java b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/TimeoutExceptionHandler.java
index 86a6881ba..582f10d2b 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/TimeoutExceptionHandler.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/TimeoutExceptionHandler.java
@@ -29,9 +29,10 @@ public class TimeoutExceptionHandler implements ExceptionHandler<TimeoutExceptio
     private static final IgniteLogger log = IgniteLogger.forClass(TimeoutExceptionHandler.class);
 
     @Override
-    public void handle(ExceptionWriter err, TimeoutException e) {
+    public int handle(ExceptionWriter err, TimeoutException e) {
         log.error("Timeout exception ", e);
         err.write("Command failed with timeout.");
+        return 1;
     }
 
     @Override
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/UnknownCommandExceptionHandler.java b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/UnknownCommandExceptionHandler.java
index 989d05055..5750ecdcd 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/UnknownCommandExceptionHandler.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/UnknownCommandExceptionHandler.java
@@ -28,8 +28,10 @@ import org.jline.console.impl.SystemRegistryImpl.UnknownCommandException;
 public class UnknownCommandExceptionHandler implements ExceptionHandler<UnknownCommandException> {
 
     @Override
-    public void handle(ExceptionWriter err, UnknownCommandException e) {
+    public int handle(ExceptionWriter err, UnknownCommandException e) {
         err.write(e.getMessage());
+        // This exception is only thrown in the REPL mode so the return value is irrelevant, but we use 2 to keep it consistent
+        return 2;
     }
 
     @Override
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/UserInterruptExceptionHandler.java b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/UserInterruptExceptionHandler.java
index 46da1ee2e..4dae0d079 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/UserInterruptExceptionHandler.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/core/exception/handler/UserInterruptExceptionHandler.java
@@ -26,8 +26,9 @@ import org.jline.reader.UserInterruptException;
  */
 public class UserInterruptExceptionHandler implements ExceptionHandler<UserInterruptException> {
     @Override
-    public void handle(ExceptionWriter err, UserInterruptException e) {
+    public int handle(ExceptionWriter err, UserInterruptException e) {
         //NOOP
+        return 1;
     }
 
     @Override
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/builtins/cluster/ClusterApiClient.java b/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/builtins/cluster/ClusterApiClient.java
index 90b3b057d..2a89e8707 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/builtins/cluster/ClusterApiClient.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/builtins/cluster/ClusterApiClient.java
@@ -32,6 +32,7 @@ import java.net.http.HttpRequest.BodyPublishers;
 import java.net.http.HttpResponse;
 import java.net.http.HttpResponse.BodyHandlers;
 import java.util.List;
+import org.apache.ignite.cli.core.exception.IgniteCliApiException;
 import org.apache.ignite.cli.deprecated.IgniteCliException;
 
 /**
@@ -81,7 +82,7 @@ public class ClusterApiClient {
         try {
             httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString());
         } catch (IOException | InterruptedException e) {
-            throw new IgniteCliException("Connection issues while trying to send http request", e);
+            throw new IgniteCliApiException(e, nodeEndpoint);
         }
 
         if (httpResponse.statusCode() == HttpURLConnection.HTTP_OK) {
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/spec/BootstrapIgniteCommandSpec.java b/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/spec/BootstrapIgniteCommandSpec.java
index 84ad3fbb5..af00c5a31 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/spec/BootstrapIgniteCommandSpec.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/spec/BootstrapIgniteCommandSpec.java
@@ -19,6 +19,7 @@ package org.apache.ignite.cli.deprecated.spec;
 
 import jakarta.inject.Inject;
 import java.net.URL;
+import java.util.concurrent.Callable;
 import org.apache.ignite.cli.commands.BaseCommand;
 import org.apache.ignite.cli.common.IgniteCommand;
 import org.apache.ignite.cli.deprecated.builtins.init.InitIgniteCommand;
@@ -30,7 +31,7 @@ import picocli.CommandLine;
  * @see IgniteCommand
  */
 @CommandLine.Command(name = "bootstrap", description = "Installs Ignite core modules locally.")
-public class BootstrapIgniteCommandSpec extends BaseCommand implements IgniteCommand {
+public class BootstrapIgniteCommandSpec extends BaseCommand implements IgniteCommand, Callable<Integer> {
     /** Init command implementation. */
     @Inject
     private InitIgniteCommand cmd;
@@ -44,7 +45,9 @@ public class BootstrapIgniteCommandSpec extends BaseCommand implements IgniteCom
 
     /** {@inheritDoc} */
     @Override
-    public void run() {
+    public Integer call() {
         cmd.init(urls, spec.commandLine().getOut(), spec.commandLine().getColorScheme());
+
+        return 0;
     }
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/spec/ClusterCommandSpec.java b/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/spec/ClusterCommandSpec.java
index cd15643cb..b4389edca 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/spec/ClusterCommandSpec.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/spec/ClusterCommandSpec.java
@@ -20,6 +20,7 @@ package org.apache.ignite.cli.deprecated.spec;
 import jakarta.inject.Inject;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.Callable;
 import org.apache.ignite.cli.commands.BaseCommand;
 import org.apache.ignite.cli.deprecated.builtins.cluster.ClusterApiClient;
 import picocli.CommandLine;
@@ -40,7 +41,7 @@ public class ClusterCommandSpec {
      * Initializes an Ignite cluster.
      */
     @CommandLine.Command(name = "init", description = "Initializes an Ignite cluster.")
-    public static class InitClusterCommandSpec extends BaseCommand {
+    public static class InitClusterCommandSpec extends BaseCommand implements Callable<Integer> {
 
         @Inject
         private ClusterApiClient clusterApiClient;
@@ -80,7 +81,7 @@ public class ClusterCommandSpec {
 
         /** {@inheritDoc} */
         @Override
-        public void run() {
+        public Integer call() {
             clusterApiClient.init(
                     nodeEndpoint,
                     metaStorageNodes,
@@ -88,6 +89,7 @@ public class ClusterCommandSpec {
                     clusterName,
                     spec.commandLine().getOut()
             );
+            return 0;
         }
     }
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/spec/ClusterReplCommandSpec.java b/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/spec/ClusterReplCommandSpec.java
index d5039f6ad..107f2d2f7 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/spec/ClusterReplCommandSpec.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/spec/ClusterReplCommandSpec.java
@@ -41,7 +41,7 @@ public class ClusterReplCommandSpec {
      * Initializes an Ignite cluster.
      */
     @CommandLine.Command(name = "init", description = "Initializes an Ignite cluster.")
-    public static class InitClusterCommandSpec extends BaseCommand {
+    public static class InitClusterCommandSpec extends BaseCommand implements Runnable {
 
         @Inject
         private ClusterApiClient clusterApiClient;
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/spec/NodeCommandSpec.java b/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/spec/NodeCommandSpec.java
index 0114de2eb..e4d9bcb4d 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/spec/NodeCommandSpec.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/deprecated/spec/NodeCommandSpec.java
@@ -22,6 +22,7 @@ import java.io.IOException;
 import java.io.PrintWriter;
 import java.nio.file.Path;
 import java.util.List;
+import java.util.concurrent.Callable;
 import org.apache.ignite.cli.commands.BaseCommand;
 import org.apache.ignite.cli.deprecated.CliPathsConfigLoader;
 import org.apache.ignite.cli.deprecated.IgniteCliException;
@@ -50,7 +51,7 @@ public class NodeCommandSpec {
      * Starts Ignite node command.
      */
     @CommandLine.Command(name = "start", description = "Starts an Ignite node locally.")
-    public static class StartNodeCommandSpec extends BaseCommand {
+    public static class StartNodeCommandSpec extends BaseCommand implements Callable<Integer> {
 
         /** Loader for Ignite distributive paths. */
         @Inject
@@ -70,7 +71,7 @@ public class NodeCommandSpec {
 
         /** {@inheritDoc} */
         @Override
-        public void run() {
+        public Integer call() {
             IgnitePaths ignitePaths = cliPathsCfgLdr.loadIgnitePathsOrThrowError();
 
             PrintWriter out = spec.commandLine().getOut();
@@ -97,6 +98,7 @@ public class NodeCommandSpec {
             tbl.addRow("@|bold Log File|@", node.logFile);
 
             out.println(tbl);
+            return 0;
         }
     }
 
@@ -104,7 +106,7 @@ public class NodeCommandSpec {
      * Command for stopping Ignite node on the current machine.
      */
     @CommandLine.Command(name = "stop", description = "Stops a locally running Ignite node.")
-    public static class StopNodeCommandSpec extends BaseCommand {
+    public static class StopNodeCommandSpec extends BaseCommand implements Callable<Integer> {
         /** Node manager. */
         @Inject
         private NodeManager nodeMgr;
@@ -123,7 +125,7 @@ public class NodeCommandSpec {
 
         /** {@inheritDoc} */
         @Override
-        public void run() {
+        public Integer call() {
             IgnitePaths ignitePaths = cliPathsCfgLdr.loadIgnitePathsOrThrowError();
 
             PrintWriter out = spec.commandLine().getOut();
@@ -138,6 +140,7 @@ public class NodeCommandSpec {
                     out.println(cs.text("@|bold,red Failed|@"));
                 }
             });
+            return 0;
         }
     }
 
@@ -145,7 +148,7 @@ public class NodeCommandSpec {
      * Command for listing the running nodes.
      */
     @CommandLine.Command(name = "list", description = "Shows the list of currently running local Ignite nodes.")
-    public static class ListNodesCommandSpec extends BaseCommand {
+    public static class ListNodesCommandSpec extends BaseCommand implements Callable<Integer> {
         /** Node manager. */
         @Inject
         private NodeManager nodeMgr;
@@ -156,7 +159,7 @@ public class NodeCommandSpec {
 
         /** {@inheritDoc} */
         @Override
-        public void run() {
+        public Integer call() {
             IgnitePaths paths = cliPathsCfgLdr.loadIgnitePathsOrThrowError();
 
             List<NodeManager.RunningNode> nodes = nodeMgr.getRunningNodes(paths.logDir, paths.cliPidsDir());
@@ -183,6 +186,7 @@ public class NodeCommandSpec {
 
                 out.println(tbl);
             }
+            return 0;
         }
     }
 
@@ -190,14 +194,14 @@ public class NodeCommandSpec {
      * Command for reading the current classpath of Ignite nodes.
      */
     @CommandLine.Command(name = "classpath", description = "Shows the current classpath used by the Ignite nodes.")
-    public static class NodesClasspathCommandSpec extends BaseCommand {
+    public static class NodesClasspathCommandSpec extends BaseCommand implements Callable<Integer> {
         /** Node manager. */
         @Inject
         private NodeManager nodeMgr;
 
         /** {@inheritDoc} */
         @Override
-        public void run() {
+        public Integer call() {
             try {
                 List<String> items = nodeMgr.classpathItems();
 
@@ -211,6 +215,7 @@ public class NodeCommandSpec {
             } catch (IOException e) {
                 throw new IgniteCliException("Can't get current classpath", e);
             }
+            return 0;
         }
     }
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/sql/SqlSchemaLoader.java b/modules/cli/src/main/java/org/apache/ignite/cli/sql/SqlSchemaLoader.java
index 2b03119bf..7a155e7c7 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/sql/SqlSchemaLoader.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/sql/SqlSchemaLoader.java
@@ -50,7 +50,7 @@ public class SqlSchemaLoader {
                 tables.put(tableName, loadColumns(tableSchema, tableName));
             }
         } catch (SQLException e) {
-            //TODO: https://issues.apache.org/jira/browse/IGNITE-17093
+            //TODO: https://issues.apache.org/jira/browse/IGNITE-17090
         }
         return new SqlSchema(schema);
     }
@@ -64,7 +64,7 @@ public class SqlSchemaLoader {
                 columns.add(rs.getString("COLUMN_NAME"));
             }
         } catch (SQLException e) {
-            //TODO: https://issues.apache.org/jira/browse/IGNITE-17093
+            //TODO: https://issues.apache.org/jira/browse/IGNITE-17090
         }
         return columns;
     }
diff --git a/modules/cli/src/test/java/org/apache/ignite/cli/commands/CliCommandTestBase.java b/modules/cli/src/test/java/org/apache/ignite/cli/commands/CliCommandTestBase.java
index 4e5209c84..dfe2106a3 100644
--- a/modules/cli/src/test/java/org/apache/ignite/cli/commands/CliCommandTestBase.java
+++ b/modules/cli/src/test/java/org/apache/ignite/cli/commands/CliCommandTestBase.java
@@ -17,39 +17,103 @@
 
 package org.apache.ignite.cli.commands;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 import io.micronaut.configuration.picocli.MicronautFactory;
 import io.micronaut.context.ApplicationContext;
 import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
 import jakarta.inject.Inject;
 import java.io.PrintWriter;
 import java.io.StringWriter;
+import org.junit.jupiter.api.BeforeEach;
 import picocli.CommandLine;
 
 /**
  * Base class for testing CLI commands.
  */
 @MicronautTest
-public class CliCommandTestBase {
+public abstract class CliCommandTestBase {
     @Inject
     private ApplicationContext context;
 
-    private CommandLine commandLine;
+    private CommandLine cmd;
 
-    protected StringWriter err;
+    private StringWriter sout;
 
-    protected StringWriter out;
+    private StringWriter serr;
 
     private int exitCode = Integer.MIN_VALUE;
 
-    protected void setUp(Class<?> commandClass) {
-        err = new StringWriter();
-        out = new StringWriter();
-        commandLine = new CommandLine(commandClass, new MicronautFactory(context));
-        commandLine.setErr(new PrintWriter(err));
-        commandLine.setOut(new PrintWriter(out));
+    @BeforeEach
+    public void setUp() {
+        cmd = new CommandLine(getCommandClass(), new MicronautFactory(context));
+        sout = new StringWriter();
+        serr = new StringWriter();
+        cmd.setOut(new PrintWriter(sout));
+        cmd.setErr(new PrintWriter(serr));
     }
 
+    protected abstract Class<?> getCommandClass();
+
     protected void execute(String... args) {
-        exitCode = commandLine.execute(args);
+        exitCode = cmd.execute(args);
+    }
+
+    protected void assertExitCodeIs(int expectedExitCode) {
+        assertThat(exitCode)
+                .as("Expected exit code to be: " + expectedExitCode + " but was " + exitCode)
+                .isEqualTo(expectedExitCode);
+    }
+
+    protected void assertExitCodeIsZero() {
+        assertExitCodeIs(0);
+    }
+
+    protected void assertOutputIsNotEmpty() {
+        assertThat(sout.toString())
+                .as("Expected command output not to be empty")
+                .isNotEmpty();
+    }
+
+    protected void assertOutputIs(String expectedOutput) {
+        assertThat(sout.toString())
+                .as("Expected command output to be: " + expectedOutput + " but was " + sout.toString())
+                .isEqualTo(expectedOutput);
+    }
+
+    protected void assertOutputContains(String expectedOutput) {
+        assertThat(sout.toString())
+                .as("Expected command output to contain: " + expectedOutput + " but was " + sout.toString())
+                .contains(expectedOutput);
+    }
+
+    protected void assertOutputIsEmpty() {
+        assertThat(sout.toString())
+                .as("Expected command output to be empty")
+                .isEmpty();
+    }
+
+    protected void assertErrOutputIsNotEmpty() {
+        assertThat(serr.toString())
+                .as("Expected command error output not to be empty")
+                .isNotEmpty();
+    }
+
+    protected void assertErrOutputIsEmpty() {
+        assertThat(serr.toString())
+                .as("Expected command error output to be empty")
+                .isEmpty();
+    }
+
+    protected void assertErrOutputIs(String expectedErrOutput) {
+        assertThat(serr.toString())
+                .as("Expected command error output to be equal to: " + expectedErrOutput)
+                .isEqualTo(expectedErrOutput);
+    }
+
+    protected void assertErrOutputContains(String expectedErrOutput) {
+        assertThat(serr.toString())
+                .as("Expected command error output to contain: " + expectedErrOutput + " but was " + serr.toString())
+                .contains(expectedErrOutput);
     }
 }
diff --git a/modules/cli/src/test/java/org/apache/ignite/cli/commands/UrlOptionsNegativeTest.java b/modules/cli/src/test/java/org/apache/ignite/cli/commands/UrlOptionsNegativeTest.java
new file mode 100644
index 000000000..aa9098b24
--- /dev/null
+++ b/modules/cli/src/test/java/org/apache/ignite/cli/commands/UrlOptionsNegativeTest.java
@@ -0,0 +1,235 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.cli.commands;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+import io.micronaut.configuration.picocli.MicronautFactory;
+import io.micronaut.context.ApplicationContext;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.ignite.cli.commands.configuration.cluster.ClusterConfigShowReplSubCommand;
+import org.apache.ignite.cli.commands.configuration.cluster.ClusterConfigShowSubCommand;
+import org.apache.ignite.cli.commands.configuration.cluster.ClusterConfigUpdateReplSubCommand;
+import org.apache.ignite.cli.commands.configuration.cluster.ClusterConfigUpdateSubCommand;
+import org.apache.ignite.cli.commands.configuration.node.NodeConfigShowReplSubCommand;
+import org.apache.ignite.cli.commands.configuration.node.NodeConfigShowSubCommand;
+import org.apache.ignite.cli.commands.configuration.node.NodeConfigUpdateReplSubCommand;
+import org.apache.ignite.cli.commands.configuration.node.NodeConfigUpdateSubCommand;
+import org.apache.ignite.cli.commands.connect.ConnectCommand;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import picocli.CommandLine;
+
+/**
+ * Tests error handling with various invalid URLs in CLI commands that use REST.
+ */
+@MicronautTest
+public class UrlOptionsNegativeTest {
+    private static final String NODE_URL = "http://localhost:10300";
+
+    @Inject
+    private ApplicationContext context;
+
+    private CommandLine cmd;
+
+    private StringWriter sout;
+
+    private StringWriter serr;
+
+    private int exitCode = Integer.MIN_VALUE;
+
+    private void setUp(Class<?> cmdClass) {
+        cmd = new CommandLine(cmdClass, new MicronautFactory(context));
+        sout = new StringWriter();
+        serr = new StringWriter();
+        cmd.setOut(new PrintWriter(sout));
+        cmd.setErr(new PrintWriter(serr));
+    }
+
+    private void execute(Class<?> cmdClass, String urlOptionName, String urlOptionValue, List<String> additionalOptions) {
+        setUp(cmdClass);
+        List<String> options = new ArrayList<>();
+        options.add(urlOptionName + urlOptionValue);
+        options.addAll(additionalOptions);
+        exitCode = cmd.execute(options.toArray(new String[0]));
+    }
+
+    static List<Arguments> cmdClassAndOptionsProvider() {
+        return List.of(
+                Arguments.arguments(NodeConfigShowSubCommand.class, "--node-url=", List.of()),
+                Arguments.arguments(NodeConfigUpdateSubCommand.class, "--node-url=", List.of("{key: value}")),
+                Arguments.arguments(ClusterConfigShowSubCommand.class, "--cluster-url=", List.of()),
+                Arguments.arguments(ClusterConfigUpdateSubCommand.class, "--cluster-url=", List.of("{key: value}"))
+        // TODO https://issues.apache.org/jira/browse/IGNITE-17091
+        //                Arguments.arguments(StatusCommand.class, "--cluster-url=", List.of()),
+        // TODO https://issues.apache.org/jira/browse/IGNITE-17102
+        //                Arguments.arguments(ClusterShowCommand.class, "--cluster-url=", List.of()),
+        // TODO https://issues.apache.org/jira/browse/IGNITE-17092
+        //                Arguments.arguments(TopologyCommand.class, "--cluster-url", List.of()),
+        // TODO https://issues.apache.org/jira/browse/IGNITE-17162
+        //                Arguments.arguments(ClusterCommandSpec.InitClusterCommandSpec.class, "---cluster-url=",
+        //                        List.of("--cluster-name=cluster", "--meta-storage-node=test"))
+        );
+    }
+
+    static List<Arguments> cmdReplClassAndOptionsProvider() {
+        return List.of(
+                Arguments.arguments(NodeConfigShowReplSubCommand.class, "--node-url=", List.of()),
+                Arguments.arguments(NodeConfigUpdateReplSubCommand.class, "--node-url=", List.of("{key: value}")),
+                Arguments.arguments(ClusterConfigShowReplSubCommand.class, "--cluster-url=", List.of()),
+                Arguments.arguments(ClusterConfigUpdateReplSubCommand.class, "--cluster-url=", List.of("{key: value}")),
+                Arguments.arguments(ConnectCommand.class, "", List.of())
+        // TODO https://issues.apache.org/jira/browse/IGNITE-17091
+        //                Arguments.arguments(StatusReplCommand.class, "--cluster-url=", List.of()),
+        // TODO https://issues.apache.org/jira/browse/IGNITE-17102
+        //                Arguments.arguments(ClusterShowReplCommand.class, "--cluster-url=", List.of()),
+        // TODO https://issues.apache.org/jira/browse/IGNITE-17092
+        //                Arguments.arguments(TopologyReplCommand.class, "--cluster-url", List.of()),
+        // TODO https://issues.apache.org/jira/browse/IGNITE-17162
+        //                Arguments.arguments(ClusterReplCommandSpec.InitClusterCommandSpec.class, "---cluster-url=",
+        //                        List.of("--cluster-name=cluster", "--meta-storage-node=test"))
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("cmdClassAndOptionsProvider")
+    @DisplayName("Should display error when wrong port is given")
+    void incorrectPort(Class<?> cmdClass, String urlOptionName, List<String> additionalOptions) {
+        execute(cmdClass, urlOptionName, NODE_URL + "incorrect", additionalOptions);
+
+        assertAll(
+                this::assertExitCodeIsFailure,
+                this::assertOutputIsEmpty,
+                () -> assertErrOutputIs("Invalid URL port: \"10300incorrect\"" + System.lineSeparator())
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("cmdClassAndOptionsProvider")
+    @DisplayName("Should display error when wrong url is given")
+    void invalidUrlScheme(Class<?> cmdClass, String urlOptionName, List<String> additionalOptions) {
+        execute(cmdClass, urlOptionName, "incorrect" + NODE_URL, additionalOptions);
+
+        assertAll(
+                this::assertExitCodeIsFailure,
+                this::assertOutputIsEmpty,
+                () -> assertErrOutputIs("Expected URL scheme 'http' or 'https' but was 'incorrecthttp'" + System.lineSeparator())
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("cmdClassAndOptionsProvider")
+    @DisplayName("Should display error when unknown host is given")
+    void invalidUrl(Class<?> cmdClass, String urlOptionName, List<String> additionalOptions) {
+        execute(cmdClass, urlOptionName, "http://no-such-host.com", additionalOptions);
+
+        assertAll(
+                this::assertExitCodeIsFailure,
+                this::assertOutputIsEmpty,
+                () -> assertErrOutputIs("Could not determine IP address when connecting to URL: http://no-such-host.com" + System.lineSeparator())
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("cmdClassAndOptionsProvider")
+    @DisplayName("Should display error when failed to connect to host")
+    void connectError(Class<?> cmdClass, String urlOptionName, List<String> additionalOptions) {
+        execute(cmdClass, urlOptionName, NODE_URL, additionalOptions);
+
+        assertAll(
+                this::assertExitCodeIsFailure,
+                this::assertOutputIsEmpty,
+                () -> assertErrOutputIs("Could not connect to URL: " + NODE_URL + System.lineSeparator())
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("cmdReplClassAndOptionsProvider")
+    @DisplayName("Should display error when wrong port is given")
+    void incorrectPortRepl(Class<?> cmdClass, String urlOptionName, List<String> additionalOptions) {
+        execute(cmdClass, urlOptionName, NODE_URL + "incorrect", additionalOptions);
+
+        assertAll(
+                this::assertOutputIsEmpty,
+                () -> assertErrOutputIs("Invalid URL port: \"10300incorrect\"" + System.lineSeparator())
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("cmdReplClassAndOptionsProvider")
+    @DisplayName("Should display error when wrong url is given")
+    void invalidUrlSchemeRepl(Class<?> cmdClass, String urlOptionName, List<String> additionalOptions) {
+        execute(cmdClass, urlOptionName, "incorrect" + NODE_URL, additionalOptions);
+
+        assertAll(
+                this::assertOutputIsEmpty,
+                () -> assertErrOutputIs("Expected URL scheme 'http' or 'https' but was 'incorrecthttp'" + System.lineSeparator())
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("cmdReplClassAndOptionsProvider")
+    @DisplayName("Should display error when unknown host is given")
+    void invalidUrlRepl(Class<?> cmdClass, String urlOptionName, List<String> additionalOptions) {
+        execute(cmdClass, urlOptionName, "http://no-such-host.com", additionalOptions);
+
+        assertAll(
+                this::assertOutputIsEmpty,
+                () -> assertErrOutputIs("Could not determine IP address when connecting to URL: http://no-such-host.com" + System.lineSeparator())
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("cmdReplClassAndOptionsProvider")
+    @DisplayName("Should display error when failed to connect to host")
+    void connectErrorRepl(Class<?> cmdClass, String urlOptionName, List<String> additionalOptions) {
+        execute(cmdClass, urlOptionName, NODE_URL, additionalOptions);
+
+        assertAll(
+                this::assertOutputIsEmpty,
+                () -> assertErrOutputIs("Could not connect to URL: " + NODE_URL + System.lineSeparator())
+        );
+    }
+
+    private void assertExitCodeIsFailure() {
+        assertThat(exitCode)
+                .as("Check exit code")
+                .isEqualTo(1);
+    }
+
+    private void assertOutputIsEmpty() {
+        assertThat(sout.toString())
+                .as("Check command output")
+                .isEmpty();
+    }
+
+    private void assertErrOutputIs(String expectedErrOutput) {
+        assertThat(serr.toString())
+                .as("Check command error output")
+                .isEqualTo(expectedErrOutput);
+    }
+
+}
diff --git a/modules/cli/src/test/java/org/apache/ignite/cli/commands/cliconfig/CliConfigGetSubCommandTest.java b/modules/cli/src/test/java/org/apache/ignite/cli/commands/cliconfig/CliConfigGetSubCommandTest.java
index 94f7a02a7..26c4c196b 100644
--- a/modules/cli/src/test/java/org/apache/ignite/cli/commands/cliconfig/CliConfigGetSubCommandTest.java
+++ b/modules/cli/src/test/java/org/apache/ignite/cli/commands/cliconfig/CliConfigGetSubCommandTest.java
@@ -17,18 +17,17 @@
 
 package org.apache.ignite.cli.commands.cliconfig;
 
-import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
 
 import org.apache.ignite.cli.commands.CliCommandTestBase;
-import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 
 class CliConfigGetSubCommandTest extends CliCommandTestBase {
 
-    @BeforeEach
-    void setUp() {
-        setUp(CliConfigGetSubCommand.class);
+    @Override
+    protected Class<?> getCommandClass() {
+        return CliConfigGetSubCommand.class;
     }
 
     @Test
@@ -37,10 +36,11 @@ class CliConfigGetSubCommandTest extends CliCommandTestBase {
         // When executed without arguments
         execute();
 
-        // Then
-        assertThat(err.toString()).contains("Missing required parameter: '<key>'");
-        // And
-        assertThat(out.toString()).isEmpty();
+        assertAll(
+                () -> assertExitCodeIs(2),
+                this::assertOutputIsEmpty,
+                () -> assertErrOutputContains("Missing required parameter: '<key>'")
+        );
     }
 
     @Test
@@ -49,22 +49,24 @@ class CliConfigGetSubCommandTest extends CliCommandTestBase {
         // When executed with single key
         execute("ignite.cluster-url");
 
-        // Then
-        assertThat(out.toString()).isEqualTo("test_cluster_url" + System.lineSeparator());
-        // And
-        assertThat(err.toString()).isEmpty();
+        assertAll(
+                this::assertExitCodeIsZero,
+                () -> assertOutputIs("test_cluster_url" + System.lineSeparator()),
+                this::assertErrOutputIsEmpty
+        );
     }
 
     @Test
-    @DisplayName("Displays error for nonexistent key")
+    @DisplayName("Displays empty string for nonexistent key")
     void nonexistentKey() {
         // When executed with nonexistent key
         execute("nonexistentKey");
 
-        // Then
-        assertThat(err.toString()).isEmpty();
-        // And
-        assertThat(out.toString().trim()).isEmpty();
+        assertAll(
+                this::assertExitCodeIsZero,
+                () -> assertOutputIs(System.lineSeparator()),
+                this::assertErrOutputIsEmpty
+        );
     }
 
     @Test
@@ -73,9 +75,10 @@ class CliConfigGetSubCommandTest extends CliCommandTestBase {
         // When executed with multiple keys
         execute("ignite.cluster-url", "ignite.jdbc-url");
 
-        // Then
-        assertThat(err.toString()).contains("Unmatched argument at index 1");
-        // And
-        assertThat(out.toString()).isEmpty();
+        assertAll(
+                () -> assertExitCodeIs(2),
+                this::assertOutputIsEmpty,
+                () -> assertErrOutputContains("Unmatched argument at index 1")
+        );
     }
 }
diff --git a/modules/cli/src/test/java/org/apache/ignite/cli/commands/cliconfig/CliConfigSetSubCommandTest.java b/modules/cli/src/test/java/org/apache/ignite/cli/commands/cliconfig/CliConfigSetSubCommandTest.java
index 0ca5a1b9a..2235ffff8 100644
--- a/modules/cli/src/test/java/org/apache/ignite/cli/commands/cliconfig/CliConfigSetSubCommandTest.java
+++ b/modules/cli/src/test/java/org/apache/ignite/cli/commands/cliconfig/CliConfigSetSubCommandTest.java
@@ -18,11 +18,11 @@
 package org.apache.ignite.cli.commands.cliconfig;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
 
 import jakarta.inject.Inject;
 import org.apache.ignite.cli.commands.CliCommandTestBase;
 import org.apache.ignite.cli.config.Config;
-import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 
@@ -31,9 +31,9 @@ class CliConfigSetSubCommandTest extends CliCommandTestBase {
     @Inject
     Config config;
 
-    @BeforeEach
-    void setUp() {
-        setUp(CliConfigSetSubCommand.class);
+    @Override
+    protected Class<?> getCommandClass() {
+        return CliConfigSetSubCommand.class;
     }
 
     @Test
@@ -42,10 +42,11 @@ class CliConfigSetSubCommandTest extends CliCommandTestBase {
         // When executed without arguments
         execute();
 
-        // Then
-        assertThat(err.toString()).contains("Missing required parameter");
-        // And
-        assertThat(out.toString()).isEmpty();
+        assertAll(
+                () -> assertExitCodeIs(2),
+                this::assertOutputIsEmpty,
+                () -> assertErrOutputContains("Missing required parameter")
+        );
     }
 
     @Test
@@ -54,10 +55,11 @@ class CliConfigSetSubCommandTest extends CliCommandTestBase {
         // When executed with key
         execute("ignite.cluster-url");
 
-        // Then
-        assertThat(err.toString()).contains("should be in KEY=VALUE format but was ignite.cluster-url");
-        // And
-        assertThat(out.toString()).isEmpty();
+        assertAll(
+                () -> assertExitCodeIs(2),
+                this::assertOutputIsEmpty,
+                () -> assertErrOutputContains("should be in KEY=VALUE format but was ignite.cluster-url")
+        );
     }
 
     @Test
@@ -66,12 +68,12 @@ class CliConfigSetSubCommandTest extends CliCommandTestBase {
         // When executed with key
         execute("ignite.cluster-url=test");
 
-        // Then
-        assertThat(out.toString()).isEmpty();
-        // And
-        assertThat(err.toString()).isEmpty();
-        // And
-        assertThat(config.getProperty("ignite.cluster-url")).isEqualTo("test");
+        assertAll(
+                this::assertExitCodeIsZero,
+                this::assertOutputIsEmpty,
+                this::assertErrOutputIsEmpty,
+                () -> assertThat(config.getProperty("ignite.cluster-url")).isEqualTo("test")
+        );
     }
 
     @Test
@@ -80,13 +82,12 @@ class CliConfigSetSubCommandTest extends CliCommandTestBase {
         // When executed with multiple keys
         execute("ignite.cluster-url=test", "ignite.jdbc-url=test2");
 
-        // Then
-        assertThat(out.toString()).isEmpty();
-        // And
-        assertThat(err.toString()).isEmpty();
-        // And
-        assertThat(config.getProperty("ignite.cluster-url")).isEqualTo("test");
-        // And
-        assertThat(config.getProperty("ignite.jdbc-url")).isEqualTo("test2");
+        assertAll(
+                this::assertExitCodeIsZero,
+                this::assertOutputIsEmpty,
+                this::assertErrOutputIsEmpty,
+                () -> assertThat(config.getProperty("ignite.cluster-url")).isEqualTo("test"),
+                () -> assertThat(config.getProperty("ignite.jdbc-url")).isEqualTo("test2")
+        );
     }
 }
diff --git a/modules/cli/src/test/java/org/apache/ignite/cli/commands/cliconfig/CliConfigSubCommandTest.java b/modules/cli/src/test/java/org/apache/ignite/cli/commands/cliconfig/CliConfigSubCommandTest.java
index a1fab8055..635badfc1 100644
--- a/modules/cli/src/test/java/org/apache/ignite/cli/commands/cliconfig/CliConfigSubCommandTest.java
+++ b/modules/cli/src/test/java/org/apache/ignite/cli/commands/cliconfig/CliConfigSubCommandTest.java
@@ -17,31 +17,30 @@
 
 package org.apache.ignite.cli.commands.cliconfig;
 
-import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
 
 import org.apache.ignite.cli.commands.CliCommandTestBase;
-import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 
 class CliConfigSubCommandTest extends CliCommandTestBase {
 
-    @BeforeEach
-    void setUp() {
-        setUp(CliConfigSubCommand.class);
+    @Override
+    protected Class<?> getCommandClass() {
+        return CliConfigSubCommand.class;
     }
 
     @Test
     @DisplayName("Displays all keys")
     void noKey() {
-        // When executed without arguments
         execute();
 
-        // Then
         String expectedResult = "ignite.cluster-url=test_cluster_url" + System.lineSeparator()
                 + "ignite.jdbc-url=test_jdbc_url" + System.lineSeparator();
-        assertThat(out.toString()).isEqualTo(expectedResult);
-        // And
-        assertThat(err.toString()).isEmpty();
+        assertAll(
+                this::assertExitCodeIsZero,
+                () -> assertOutputIs(expectedResult),
+                this::assertErrOutputIsEmpty
+        );
     }
 }
diff --git a/modules/cli/src/test/java/org/apache/ignite/cli/commands/configuration/ShowConfigSubCommandTest.java b/modules/cli/src/test/java/org/apache/ignite/cli/commands/configuration/ShowConfigSubCommandTest.java
deleted file mode 100644
index fbaf927b0..000000000
--- a/modules/cli/src/test/java/org/apache/ignite/cli/commands/configuration/ShowConfigSubCommandTest.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ignite.cli.commands.configuration;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-import org.apache.ignite.cli.commands.CliCommandTestBase;
-import org.apache.ignite.cli.commands.configuration.node.NodeConfigShowSubCommand;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Disabled;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
-
-class ShowConfigSubCommandTest extends CliCommandTestBase {
-
-    @BeforeEach
-    void setUp() {
-        setUp(NodeConfigShowSubCommand.class);
-    }
-
-    @Disabled("Cluster-url has a default value")
-    @Test
-    @DisplayName("Cluster-url is mandatory option")
-    void mandatoryOptions() {
-        // When execute without --cluster-url
-        execute();
-
-        // Then
-        assertThat(err.toString()).contains("Missing required option: '--cluster-url=<clusterUrl>'");
-        // And
-        assertThat(out.toString()).isEmpty();
-    }
-}
diff --git a/modules/cli/src/test/java/org/apache/ignite/cli/commands/sql/SqlCommandTest.java b/modules/cli/src/test/java/org/apache/ignite/cli/commands/sql/SqlCommandTest.java
new file mode 100644
index 000000000..5093a5fb2
--- /dev/null
+++ b/modules/cli/src/test/java/org/apache/ignite/cli/commands/sql/SqlCommandTest.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.cli.commands.sql;
+
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+import org.apache.ignite.cli.commands.CliCommandTestBase;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class SqlCommandTest extends CliCommandTestBase {
+
+    @Override
+    protected Class<?> getCommandClass() {
+        return SqlCommand.class;
+    }
+
+    @Test
+    @DisplayName("Should throw error if executed without --execute or --script-file options")
+    void withoutOptions() {
+        execute("--jdbc-url=");
+
+        assertAll(
+                () -> assertExitCodeIs(2),
+                this::assertOutputIsEmpty,
+                () -> assertErrOutputContains("Missing required argument (specify one of these): (-e=<command> | -f=<file>)")
+        );
+    }
+
+    @Test
+    @DisplayName("Should throw error if both --execute or --script-file options are present")
+    void mutuallyExclusiveOptions() {
+        execute("--jdbc-url=", "--execute=", "--script-file=");
+
+        assertAll(
+                () -> assertExitCodeIs(2),
+                this::assertOutputIsEmpty,
+                () -> assertErrOutputContains("--execute=<command>, --script-file=<file> are mutually exclusive (specify only one)")
+        );
+    }
+}