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/08/11 15:53:43 UTC

[ignite-3] branch main updated: IGNITE-17110 Auto-connect on the REPL start. Fixes #984

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 ddb656486 IGNITE-17110 Auto-connect on the REPL start. Fixes #984
ddb656486 is described below

commit ddb656486460dcdadbc637cac87075fb69fbd0fd
Author: Vadim Pakhnushev <86...@users.noreply.github.com>
AuthorDate: Thu Aug 11 18:53:24 2022 +0300

    IGNITE-17110 Auto-connect on the REPL start. Fixes #984
    
    Signed-off-by: Slava Koptilin <sl...@gmail.com>
---
 .../org/apache/ignite/cli/IntegrationTestBase.java |   2 +-
 ...liCommandTestNotInitializedIntegrationBase.java |  10 +-
 .../commands/questions/ItConnectToClusterTest.java | 141 +++++++++++++++++++++
 .../src/main/java/org/apache/ignite/cli/Main.java  |   4 +
 .../profile/CliConfigCreateProfileCall.java        |   2 +-
 .../ignite/cli/call/connect/ConnectCall.java       |   8 +-
 .../questions/ConnectToClusterQuestion.java        |  45 ++++++-
 ...Profile.java => CachedStateConfigProvider.java} |  24 ++--
 .../cli/config/{Profile.java => Config.java}       |  38 +++++-
 .../apache/ignite/cli/config/ConfigConstants.java  |   1 +
 .../ignite/cli/config/ConfigManagerProvider.java   |   5 +
 .../java/org/apache/ignite/cli/config/Profile.java |  62 ++++++++-
 .../org/apache/ignite/cli/config/StateConfig.java  |  61 +++++++++
 ...nagerProvider.java => StateConfigProvider.java} |  11 +-
 .../ignite/cli/config/StateFolderProvider.java     |   5 +-
 .../config/ini/{IniProfile.java => IniConfig.java} |  24 ++--
 .../ignite/cli/config/ini/IniConfigManager.java    |  21 +--
 .../org/apache/ignite/cli/config/ini/IniFile.java  |  33 ++++-
 .../apache/ignite/cli/config/ini/IniParser.java    |   8 +-
 .../apache/ignite/cli/config/ini/IniProfile.java   |  40 +-----
 .../apache/ignite/cli/config/ini/IniSection.java   |   5 -
 .../apache/ignite/cli/core/flow/builder/Flows.java |   2 +-
 .../java/org/apache/ignite/cli/core/repl/Repl.java |  11 +-
 .../apache/ignite/cli/core/repl/ReplBuilder.java   |  10 +-
 .../cli/core/repl/executor/ReplExecutor.java       |   5 +-
 .../cliconfig/TestConfigManagerHelper.java         |  32 +++--
 .../ignite/cli/config/TestStateConfigHelper.java}  |  27 ++--
 .../cli/config/TestStateConfigProvider.java}       |  25 ++--
 .../src/test/resources/cluster_url_non_default.ini |   5 +
 .../src/test/resources/last_connected_default.ini  |   1 +
 30 files changed, 515 insertions(+), 153 deletions(-)

diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/IntegrationTestBase.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/IntegrationTestBase.java
index 13f036e3b..2c243b063 100644
--- a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/IntegrationTestBase.java
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/IntegrationTestBase.java
@@ -67,7 +67,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
  */
 @ExtendWith(WorkDirectoryExtension.class)
 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
-@MicronautTest
+@MicronautTest(rebuildContext = true)
 public class IntegrationTestBase extends BaseIgniteAbstractTest {
     public static final int DEFAULT_NODES_COUNT = 3;
 
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/CliCommandTestNotInitializedIntegrationBase.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/CliCommandTestNotInitializedIntegrationBase.java
index 4012df058..f82648d8c 100644
--- a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/CliCommandTestNotInitializedIntegrationBase.java
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/CliCommandTestNotInitializedIntegrationBase.java
@@ -30,6 +30,7 @@ import org.apache.ignite.cli.commands.cliconfig.TestConfigManagerHelper;
 import org.apache.ignite.cli.commands.cliconfig.TestConfigManagerProvider;
 import org.apache.ignite.cli.config.ConfigDefaultValueProvider;
 import org.apache.ignite.cli.config.ini.IniConfigManager;
+import org.apache.ignite.cli.core.repl.context.CommandLineContextProvider;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
@@ -43,12 +44,16 @@ import picocli.CommandLine;
 public class CliCommandTestNotInitializedIntegrationBase extends IntegrationTestBase {
     /** Correct ignite jdbc url. */
     protected static final String JDBC_URL = "jdbc:ignite:thin://127.0.0.1:10800";
+
     @Inject
-    ConfigDefaultValueProvider configDefaultValueProvider;
+    private ConfigDefaultValueProvider configDefaultValueProvider;
+
     @Inject
-    TestConfigManagerProvider configManagerProvider;
+    protected TestConfigManagerProvider configManagerProvider;
+
     @Inject
     private ApplicationContext context;
+
     private CommandLine cmd;
 
     private StringWriter sout;
@@ -73,6 +78,7 @@ public class CliCommandTestNotInitializedIntegrationBase extends IntegrationTest
         serr = new StringWriter();
         cmd.setOut(new PrintWriter(sout));
         cmd.setErr(new PrintWriter(serr));
+        CommandLineContextProvider.setCmd(cmd);
     }
 
     @BeforeAll
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/questions/ItConnectToClusterTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/questions/ItConnectToClusterTest.java
new file mode 100644
index 000000000..4b5451a52
--- /dev/null
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/questions/ItConnectToClusterTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.questions;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+import jakarta.inject.Inject;
+import java.io.FileDescriptor;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.apache.ignite.cli.commands.CliCommandTestInitializedIntegrationBase;
+import org.apache.ignite.cli.commands.cliconfig.TestConfigManagerHelper;
+import org.apache.ignite.cli.config.ConfigConstants;
+import org.apache.ignite.cli.config.TestStateConfigHelper;
+import org.apache.ignite.cli.config.TestStateConfigProvider;
+import org.apache.ignite.cli.config.ini.IniConfigManager;
+import org.apache.ignite.cli.core.flow.question.JlineQuestionWriterReader;
+import org.apache.ignite.cli.core.flow.question.QuestionAskerFactory;
+import org.apache.ignite.cli.core.repl.prompt.PromptProvider;
+import org.jline.reader.impl.LineReaderImpl;
+import org.jline.terminal.Terminal;
+import org.jline.terminal.impl.DumbTerminal;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import picocli.CommandLine.Help.Ansi;
+
+class ItConnectToClusterTest extends CliCommandTestInitializedIntegrationBase {
+    @Inject
+    private PromptProvider promptProvider;
+
+    @Inject
+    private TestStateConfigProvider stateConfigProvider;
+
+    @Inject
+    private ConnectToClusterQuestion question;
+
+    private Terminal terminal;
+    private Path input;
+
+    @BeforeEach
+    public void setUp(TestInfo testInfo) throws Exception {
+        super.setUp(testInfo);
+
+        input = Files.createTempFile("input", "");
+        input.toFile().deleteOnExit();
+        terminal = new DumbTerminal(Files.newInputStream(input), new FileOutputStream(FileDescriptor.out));
+        LineReaderImpl reader = new LineReaderImpl(terminal);
+        QuestionAskerFactory.setReadWriter(new JlineQuestionWriterReader(reader));
+    }
+
+    @AfterEach
+    public void cleanUp() throws IOException {
+        terminal.input().close();
+        terminal.close();
+    }
+
+    @Test
+    @DisplayName("Should connect to last connected cluster url")
+    void connectOnStart() throws IOException {
+        // Given prompt before connect
+        String promptBefore = Ansi.OFF.string(promptProvider.getPrompt());
+        assertThat(promptBefore).isEqualTo("[disconnected]> ");
+
+        // And last connected URL is equal to the default URL
+        stateConfigProvider.config = TestStateConfigHelper.createLastConnectedDefault();
+
+        // And answer to the first question is "y"
+        bindAnswers("y");
+
+        // When asked the question
+        question.askQuestionOnReplStart();
+
+        // Then
+        assertAll(
+                this::assertErrOutputIsEmpty,
+                () -> assertOutputContains("Connected to http://localhost:10300")
+        );
+        // And prompt is changed to connect
+        String promptAfter = Ansi.OFF.string(promptProvider.getPrompt());
+        assertThat(promptAfter).isEqualTo("[" + nodeName() + "]> ");
+    }
+
+    @Test
+    @DisplayName("Should connect to last connected cluster url and ask for save")
+    void connectOnStartAndSave() throws IOException {
+        // Given prompt before connect
+        String promptBefore = Ansi.OFF.string(promptProvider.getPrompt());
+        assertThat(promptBefore).isEqualTo("[disconnected]> ");
+
+        // And last connected URL is not equal to the default URL
+        configManagerProvider.configManager = new IniConfigManager(TestConfigManagerHelper.createClusterUrlNonDefault());
+        stateConfigProvider.config = TestStateConfigHelper.createLastConnectedDefault();
+
+        // And answer to both questions is "y"
+        bindAnswers("y", "y");
+
+        // When asked the questions
+        question.askQuestionOnReplStart();
+
+        // Then
+        assertAll(
+                this::assertErrOutputIsEmpty,
+                () -> assertOutputContains("Connected to http://localhost:10300"),
+                () -> assertOutputContains("Config saved")
+        );
+        // And prompt is changed to connect
+        String promptAfter = Ansi.OFF.string(promptProvider.getPrompt());
+        assertThat(promptAfter).isEqualTo("[" + nodeName() + "]> ");
+        assertThat(configManagerProvider.get().getCurrentProperty(ConfigConstants.CLUSTER_URL))
+                .isEqualTo("http://localhost:10300");
+    }
+
+    private String nodeName() {
+        return CLUSTER_NODES.get(0).name();
+    }
+
+    private void bindAnswers(String... answers) throws IOException {
+        Files.writeString(input, String.join("\n", answers) + "\n");
+    }
+}
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 dac94907d..57a17cb0d 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
@@ -28,6 +28,7 @@ import java.util.HashMap;
 import java.util.stream.Collectors;
 import org.apache.ignite.cli.commands.TopLevelCliCommand;
 import org.apache.ignite.cli.commands.TopLevelCliReplCommand;
+import org.apache.ignite.cli.commands.questions.ConnectToClusterQuestion;
 import org.apache.ignite.cli.config.ConfigDefaultValueProvider;
 import org.apache.ignite.cli.core.call.CallExecutionPipeline;
 import org.apache.ignite.cli.core.call.StringCallInput;
@@ -92,6 +93,8 @@ public class Main {
         VersionProvider versionProvider = micronautFactory.create(VersionProvider.class);
         System.out.println(banner(versionProvider));
 
+        ConnectToClusterQuestion question = micronautFactory.create(ConnectToClusterQuestion.class);
+
         replExecutorProvider.get().execute(Repl.builder()
                 .withPromptProvider(micronautFactory.create(PromptProvider.class))
                 .withAliases(aliases)
@@ -105,6 +108,7 @@ public class Main {
                                 .exceptionHandlers(new DefaultExceptionHandlers())
                                 .exceptionHandlers(exceptionHandlers)
                                 .build())
+                .withOnStart(question::askQuestionOnReplStart)
                 .withHistoryFileName("history")
                 .withTailTipWidgets()
                 .build());
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/call/cliconfig/profile/CliConfigCreateProfileCall.java b/modules/cli/src/main/java/org/apache/ignite/cli/call/cliconfig/profile/CliConfigCreateProfileCall.java
index 685fc4f5c..e76d83635 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/call/cliconfig/profile/CliConfigCreateProfileCall.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/call/cliconfig/profile/CliConfigCreateProfileCall.java
@@ -47,7 +47,7 @@ public class CliConfigCreateProfileCall implements Call<CliConfigCreateProfileCa
         Profile newProfile = configManager.createProfile(profileName);
 
         if (copyFrom != null) {
-            newProfile.setProperties(copyFrom);
+            newProfile.setProperties(copyFrom.getAll());
         }
 
         if (input.isActivate()) {
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 675dd24e6..bf72de775 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
@@ -21,6 +21,8 @@ import com.google.gson.Gson;
 import jakarta.inject.Singleton;
 import java.net.MalformedURLException;
 import java.net.URL;
+import org.apache.ignite.cli.config.ConfigConstants;
+import org.apache.ignite.cli.config.StateConfigProvider;
 import org.apache.ignite.cli.core.call.Call;
 import org.apache.ignite.cli.core.call.CallOutput;
 import org.apache.ignite.cli.core.call.DefaultCallOutput;
@@ -41,8 +43,11 @@ public class ConnectCall implements Call<ConnectCallInput, String> {
 
     private final Session session;
 
-    public ConnectCall(Session session) {
+    private final StateConfigProvider stateConfigProvider;
+
+    public ConnectCall(Session session, StateConfigProvider stateConfigProvider) {
         this.session = session;
+        this.stateConfigProvider = stateConfigProvider;
     }
 
     @Override
@@ -50,6 +55,7 @@ public class ConnectCall implements Call<ConnectCallInput, String> {
         try {
             String nodeUrl = input.getNodeUrl();
             session.setNodeUrl(nodeUrl);
+            stateConfigProvider.get().setProperty(ConfigConstants.LAST_CONNECTED_URL, nodeUrl);
             session.setNodeName(fetchNodeName(input));
             String configuration = fetchNodeConfiguration(input);
             session.setJdbcUrl(constructJdbcUrl(configuration, nodeUrl));
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/questions/ConnectToClusterQuestion.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/questions/ConnectToClusterQuestion.java
index 81dc6ac54..5c3a6496e 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/questions/ConnectToClusterQuestion.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/questions/ConnectToClusterQuestion.java
@@ -24,6 +24,7 @@ import org.apache.ignite.cli.call.connect.ConnectCall;
 import org.apache.ignite.cli.call.connect.ConnectCallInput;
 import org.apache.ignite.cli.config.ConfigConstants;
 import org.apache.ignite.cli.config.ConfigManagerProvider;
+import org.apache.ignite.cli.config.StateConfigProvider;
 import org.apache.ignite.cli.core.flow.Flowable;
 import org.apache.ignite.cli.core.flow.builder.FlowBuilder;
 import org.apache.ignite.cli.core.flow.builder.Flows;
@@ -41,7 +42,10 @@ public class ConnectToClusterQuestion {
     private ConnectCall connectCall;
 
     @Inject
-    private ConfigManagerProvider provider;
+    private ConfigManagerProvider configManagerProvider;
+
+    @Inject
+    private StateConfigProvider stateConfigProvider;
 
     @Inject
     private Session session;
@@ -54,13 +58,13 @@ public class ConnectToClusterQuestion {
      * @return {@link FlowBuilder} instance with question in case when cluster url.
      */
     public FlowBuilder<Void, String> askQuestionIfNotConnected(String clusterUrl) {
-        String clusterProperty = provider.get().getCurrentProperty(ConfigConstants.CLUSTER_URL);
+        String defaultUrl = configManagerProvider.get().getCurrentProperty(ConfigConstants.CLUSTER_URL);
         String question = "You are not connected to node. Do you want to connect to the default node "
-                + clusterProperty + " ? [Y/n] ";
+                + defaultUrl + " ? [Y/n] ";
 
         return Flows.from(clusterUrlOrSessionNode(clusterUrl))
                 .ifThen(Objects::isNull, Flows.<String, ConnectCallInput>acceptQuestion(question,
-                                () -> new ConnectCallInput(clusterProperty))
+                                () -> new ConnectCallInput(defaultUrl))
                         .then(Flows.fromCall(connectCall))
                         .toOutput(CommandLineContextProvider.getContext())
                         .build())
@@ -70,4 +74,37 @@ public class ConnectToClusterQuestion {
     private String clusterUrlOrSessionNode(String clusterUrl) {
         return clusterUrl != null ? clusterUrl : session.nodeUrl();
     }
+
+    /**
+     * Ask for connect to the cluster and suggest to save the last connected URL as default.
+     */
+    public void askQuestionOnReplStart() {
+        String defaultUrl = configManagerProvider.get().getCurrentProperty(ConfigConstants.CLUSTER_URL);
+        String lastConnectedUrl = stateConfigProvider.get().getProperty(ConfigConstants.LAST_CONNECTED_URL);
+        String question;
+        String clusterUrl;
+        if (lastConnectedUrl != null) {
+            question = "Do you want to connect to the last connected node " + lastConnectedUrl + " ? [Y/n]";
+            clusterUrl = lastConnectedUrl;
+        } else {
+            question = "Do you want to connect to the default node " + defaultUrl + " ? [Y/n]";
+            clusterUrl = defaultUrl;
+        }
+
+        Flows.acceptQuestion(question, () -> new ConnectCallInput(clusterUrl))
+                .then(Flows.fromCall(connectCall))
+                .toOutput(CommandLineContextProvider.getContext())
+                .ifThen(s -> !Objects.equals(lastConnectedUrl, defaultUrl) && session.isConnectedToNode(),
+                        defaultUrlQuestion(lastConnectedUrl).toOutput(CommandLineContextProvider.getContext()).build())
+                .build().start(Flowable.empty());
+    }
+
+    private FlowBuilder<String, String> defaultUrlQuestion(String lastConnectedUrl) {
+        return Flows.acceptQuestion("Would you like to use " + lastConnectedUrl + " as the default URL? [Y/n]",
+                () -> {
+                    configManagerProvider.get().setProperty(ConfigConstants.CLUSTER_URL, lastConnectedUrl);
+                    return "Config saved";
+                }
+        );
+    }
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/config/Profile.java b/modules/cli/src/main/java/org/apache/ignite/cli/config/CachedStateConfigProvider.java
similarity index 65%
copy from modules/cli/src/main/java/org/apache/ignite/cli/config/Profile.java
copy to modules/cli/src/main/java/org/apache/ignite/cli/config/CachedStateConfigProvider.java
index 31d47a5fb..4caaf0375 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/config/Profile.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/config/CachedStateConfigProvider.java
@@ -17,23 +17,19 @@
 
 package org.apache.ignite.cli.config;
 
-import java.util.Map;
+import jakarta.inject.Singleton;
 
 /**
- * Ignite CLI Profile.
+ * Implementation of {@link StateConfigProvider} based on the ini file in the state folder.
  */
-public interface Profile {
-    String getName();
+@Singleton
+public class CachedStateConfigProvider implements StateConfigProvider {
+    private static final String CONFIG_FILE_NAME = "config.ini";
 
-    Map<String, String> getAll();
+    private final Config config = StateConfig.getStateConfig(StateFolderProvider.getStateFile(CONFIG_FILE_NAME));
 
-    String getProperty(String key);
-
-    String getProperty(String key, String defaultValue);
-
-    void setProperty(String key, String value);
-
-    void setProperties(Map<String, String> values);
-
-    void setProperties(Profile copyFrom);
+    @Override
+    public Config get() {
+        return config;
+    }
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/config/Profile.java b/modules/cli/src/main/java/org/apache/ignite/cli/config/Config.java
similarity index 59%
copy from modules/cli/src/main/java/org/apache/ignite/cli/config/Profile.java
copy to modules/cli/src/main/java/org/apache/ignite/cli/config/Config.java
index 31d47a5fb..cc9e078a3 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/config/Profile.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/config/Config.java
@@ -20,20 +20,46 @@ package org.apache.ignite.cli.config;
 import java.util.Map;
 
 /**
- * Ignite CLI Profile.
+ * Ignite CLI config.
  */
-public interface Profile {
-    String getName();
-
+public interface Config {
+    /**
+     * Gets all properties.
+     *
+     * @return all properties
+     */
     Map<String, String> getAll();
 
+    /**
+     * Gets a property.
+     *
+     * @param key property to get
+     * @return property value or {@code null} if config doesn't contain this property
+     */
     String getProperty(String key);
 
+    /**
+     * Gets a property.
+     *
+     * @param key property to get
+     * @param defaultValue default value of the property
+     *
+     * @return property value or {@code defaultValue} if config doesn't contain this property
+     */
     String getProperty(String key, String defaultValue);
 
+    /**
+     * Sets a property.
+     *
+     * @param key property to set
+     * @param value value to set
+     */
     void setProperty(String key, String value);
 
+    /**
+     * Sets properties to this profile.
+     *
+     * @param values map of properties to set
+     */
     void setProperties(Map<String, String> values);
-
-    void setProperties(Profile copyFrom);
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/config/ConfigConstants.java b/modules/cli/src/main/java/org/apache/ignite/cli/config/ConfigConstants.java
index d8ce17083..cf8912157 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/config/ConfigConstants.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/config/ConfigConstants.java
@@ -31,6 +31,7 @@ public final class ConfigConstants {
     public static final String CURRENT_PROFILE = "current_profile";
     public static final String CLUSTER_URL = "ignite.cluster-endpoint-url";
     public static final String JDBC_URL = "ignite.jdbc-url";
+    public static final String LAST_CONNECTED_URL = "ignite.last-connected-url";
 
     private ConfigConstants() {
 
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/config/ConfigManagerProvider.java b/modules/cli/src/main/java/org/apache/ignite/cli/config/ConfigManagerProvider.java
index 921e65138..c7fe47923 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/config/ConfigManagerProvider.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/config/ConfigManagerProvider.java
@@ -21,5 +21,10 @@ package org.apache.ignite.cli.config;
  * Provider for {@link ConfigManager}.
  */
 public interface ConfigManagerProvider {
+    /**
+     * Gets the CLI config manager.
+     *
+     * @return config manager
+     */
     ConfigManager get();
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/config/Profile.java b/modules/cli/src/main/java/org/apache/ignite/cli/config/Profile.java
index 31d47a5fb..24036e9dc 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/config/Profile.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/config/Profile.java
@@ -23,17 +23,67 @@ import java.util.Map;
  * Ignite CLI Profile.
  */
 public interface Profile {
+    /**
+     * Gets name of the profile.
+     *
+     * @return profile name
+     */
     String getName();
 
-    Map<String, String> getAll();
+    /**
+     * Gets a {@link Config} stored in this profile.
+     *
+     * @return config
+     */
+    Config getConfig();
 
-    String getProperty(String key);
+    /**
+     * Convenience method to get all properties from this profile.
+     *
+     * @return map of all properties
+     */
+    default Map<String, String> getAll() {
+        return getConfig().getAll();
+    }
 
-    String getProperty(String key, String defaultValue);
+    /**
+     * Convenience method to get a property from this profile.
+     *
+     * @param key property to get
+     * @return property value or {@code null} if config doesn't contain this property
+     */
+    default String getProperty(String key) {
+        return getConfig().getProperty(key);
+    }
 
-    void setProperty(String key, String value);
+    /**
+     * Convenience method to get a property from this profile.
+     *
+     * @param key property to get
+     * @param defaultValue default value of the property
+     *
+     * @return property value or {@code defaultValue} if config doesn't contain this property
+     */
+    default String getProperty(String key, String defaultValue) {
+        return getConfig().getProperty(key, defaultValue);
+    }
 
-    void setProperties(Map<String, String> values);
+    /**
+     * Convenience method to set a property to this profile.
+     *
+     * @param key property to set
+     * @param value value to set
+     */
+    default void setProperty(String key, String value) {
+        getConfig().setProperty(key, value);
+    }
 
-    void setProperties(Profile copyFrom);
+    /**
+     * Convenience method to set properties to this profile.
+     *
+     * @param values map of properties to set
+     */
+    default void setProperties(Map<String, String> values) {
+        getConfig().setProperties(values);
+    }
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/config/StateConfig.java b/modules/cli/src/main/java/org/apache/ignite/cli/config/StateConfig.java
new file mode 100644
index 000000000..16dfba036
--- /dev/null
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/config/StateConfig.java
@@ -0,0 +1,61 @@
+/*
+ * 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.config;
+
+import java.io.File;
+import java.io.IOException;
+import org.apache.ignite.cli.config.ini.IniConfig;
+import org.apache.ignite.cli.config.ini.IniFile;
+
+/**
+ * Config file which stores information between application restarts, but not part of the config.
+ */
+public class StateConfig {
+
+    /**
+     * Returns an instance of {@link Config} holding the properties.
+     *
+     * @param file INI file.
+     * @return new instance of {@link Config}
+     */
+    public static Config getStateConfig(File file) {
+        IniFile iniFile = loadStateConfig(file);
+        return new IniConfig(iniFile.getTopLevelSection(), iniFile::store);
+    }
+
+    private static IniFile loadStateConfig(File file) {
+        try {
+            return new IniFile(file);
+        } catch (IOException e) {
+            return createDefaultConfig(file);
+        }
+    }
+
+    private static IniFile createDefaultConfig(File file) {
+        try {
+            file.getParentFile().mkdirs();
+            file.delete();
+            file.createNewFile();
+            IniFile ini = new IniFile(file);
+            ini.store();
+            return ini;
+        } catch (IOException e) {
+            throw new ConfigInitializationException(file.getAbsolutePath(), e);
+        }
+    }
+}
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/config/ConfigManagerProvider.java b/modules/cli/src/main/java/org/apache/ignite/cli/config/StateConfigProvider.java
similarity index 80%
copy from modules/cli/src/main/java/org/apache/ignite/cli/config/ConfigManagerProvider.java
copy to modules/cli/src/main/java/org/apache/ignite/cli/config/StateConfigProvider.java
index 921e65138..db0a07b74 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/config/ConfigManagerProvider.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/config/StateConfigProvider.java
@@ -18,8 +18,13 @@
 package org.apache.ignite.cli.config;
 
 /**
- * Provider for {@link ConfigManager}.
+ * Provider for the application state config.
  */
-public interface ConfigManagerProvider {
-    ConfigManager get();
+public interface StateConfigProvider {
+    /**
+     * Gets the application state config.
+     *
+     * @return application state config
+     */
+    Config get();
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/config/StateFolderProvider.java b/modules/cli/src/main/java/org/apache/ignite/cli/config/StateFolderProvider.java
index 021cd93b5..24d408a4d 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/config/StateFolderProvider.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/config/StateFolderProvider.java
@@ -28,7 +28,6 @@ public final class StateFolderProvider {
     private static final String PARENT_FOLDER_NAME = "ignitecli";
 
     private StateFolderProvider() {
-
     }
 
     /**
@@ -36,8 +35,8 @@ public final class StateFolderProvider {
      *
      * @return Folder for state storage.
      */
-    public static File getStateFolder() {
-        return getStateRoot().resolve(PARENT_FOLDER_NAME).toFile();
+    public static File getStateFile(String name) {
+        return getStateRoot().resolve(PARENT_FOLDER_NAME).resolve(name).toFile();
     }
 
     private static Path getStateRoot() {
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/config/ini/IniProfile.java b/modules/cli/src/main/java/org/apache/ignite/cli/config/ini/IniConfig.java
similarity index 79%
copy from modules/cli/src/main/java/org/apache/ignite/cli/config/ini/IniProfile.java
copy to modules/cli/src/main/java/org/apache/ignite/cli/config/ini/IniConfig.java
index dcda97307..8d8e2ed21 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/config/ini/IniProfile.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/config/ini/IniConfig.java
@@ -18,55 +18,49 @@
 package org.apache.ignite.cli.config.ini;
 
 import java.util.Map;
-import org.apache.ignite.cli.config.Profile;
+import org.apache.ignite.cli.config.Config;
 
 /**
- * Implementation of {@link Profile} based on {@link IniSection}.
+ * Implementation of {@link Config} based on {@link IniSection}.
  */
-public class IniProfile implements Profile {
+public class IniConfig implements Config {
     private final IniSection section;
     private final Runnable saveAction;
 
-    public IniProfile(IniSection section, Runnable saveAction) {
+    public IniConfig(IniSection section, Runnable saveAction) {
         this.section = section;
         this.saveAction = saveAction;
     }
 
-    @Override
-    public String getName() {
-        return section.getName();
-    }
-
+    /** {@inheritDoc} */
     @Override
     public Map<String, String> getAll() {
         return section.getAll();
     }
 
+    /** {@inheritDoc} */
     @Override
     public String getProperty(String key) {
         return section.getProperty(key);
     }
 
+    /** {@inheritDoc} */
     @Override
     public String getProperty(String key, String defaultValue) {
         return section.getProperty(key, defaultValue);
     }
 
+    /** {@inheritDoc} */
     @Override
     public void setProperty(String key, String value) {
         section.setProperty(key, value);
         saveAction.run();
     }
 
+    /** {@inheritDoc} */
     @Override
     public void setProperties(Map<String, String> values) {
         section.setProperties(values);
         saveAction.run();
     }
-
-    @Override
-    public void setProperties(Profile copyFrom) {
-        section.setProperties(copyFrom);
-        saveAction.run();
-    }
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/config/ini/IniConfigManager.java b/modules/cli/src/main/java/org/apache/ignite/cli/config/ini/IniConfigManager.java
index bba9f1739..685f9bc53 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/config/ini/IniConfigManager.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/config/ini/IniConfigManager.java
@@ -24,7 +24,6 @@ import static org.apache.ignite.cli.config.ConfigConstants.JDBC_URL;
 import java.io.File;
 import java.io.IOException;
 import java.util.NoSuchElementException;
-import org.apache.ignite.cli.config.ConfigConstants;
 import org.apache.ignite.cli.config.ConfigInitializationException;
 import org.apache.ignite.cli.config.ConfigManager;
 import org.apache.ignite.cli.config.Profile;
@@ -56,7 +55,7 @@ public class IniConfigManager implements ConfigManager {
             findCurrentProfile(configFile);
         } catch (IOException | NoSuchElementException e) {
             log.warn("User config is corrupted or doesn't exist.", e);
-            configFile = createDefaultConfig();
+            configFile = createDefaultConfig(file);
         }
         this.configFile = configFile;
         this.currentProfileName = findCurrentProfile(configFile).getProperty(CURRENT_PROFILE);
@@ -65,7 +64,10 @@ public class IniConfigManager implements ConfigManager {
     private static IniSection findCurrentProfile(IniFile configFile) {
         IniSection internalSection = configFile.getSection(INTERNAL_SECTION_NAME);
         if (internalSection == null) {
-            IniSection section = configFile.getSections().stream().findFirst().orElseThrow();
+            IniSection section = configFile.getSections().stream()
+                    .filter(s -> !s.getName().equals(IniParser.NO_SECTION)) // Don't use top-level section
+                    .findFirst()
+                    .orElseThrow();
             internalSection = configFile.createSection(INTERNAL_SECTION_NAME);
             internalSection.setProperty(CURRENT_PROFILE, section.getName());
         }
@@ -102,13 +104,12 @@ public class IniConfigManager implements ConfigManager {
         configFile.store();
     }
 
-    private static IniFile createDefaultConfig() {
-        File configFile = ConfigConstants.getConfigFile();
+    private static IniFile createDefaultConfig(File file) {
         try {
-            configFile.getParentFile().mkdirs();
-            configFile.delete();
-            configFile.createNewFile();
-            IniFile ini = new IniFile(configFile);
+            file.getParentFile().mkdirs();
+            file.delete();
+            file.createNewFile();
+            IniFile ini = new IniFile(file);
             IniSection internal = ini.createSection(INTERNAL_SECTION_NAME);
             internal.setProperty("current_profile", "default");
             IniSection defaultSection = ini.createSection("default");
@@ -117,7 +118,7 @@ public class IniConfigManager implements ConfigManager {
             ini.store();
             return ini;
         } catch (IOException e) {
-            throw new ConfigInitializationException(configFile.getAbsolutePath(), e);
+            throw new ConfigInitializationException(file.getAbsolutePath(), e);
         }
     }
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/config/ini/IniFile.java b/modules/cli/src/main/java/org/apache/ignite/cli/config/ini/IniFile.java
index 6dce2340b..6948aa10c 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/config/ini/IniFile.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/config/ini/IniFile.java
@@ -51,6 +51,15 @@ public class IniFile {
         return content.get(name);
     }
 
+    /**
+     * Returns properties stored outside any section.
+     *
+     * @return top-level section
+     */
+    public IniSection getTopLevelSection() {
+        return getSection(IniParser.NO_SECTION);
+    }
+
     public Collection<IniSection> getSections() {
         return content.values();
     }
@@ -68,19 +77,31 @@ public class IniFile {
 
     private void store(OutputStream outputStream) throws IOException {
         BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
+
+        // Write top-level properties first
+        IniSection topLevelSection = getTopLevelSection();
+        if (topLevelSection != null) {
+            writeSection(bufferedWriter, topLevelSection);
+        }
         for (IniSection section : getSections()) {
-            bufferedWriter.write("[" + section.getName() + "]");
-            bufferedWriter.newLine();
-            for (Map.Entry<String, String> sectionEntry : section.getAll().entrySet()) {
-                bufferedWriter.write(sectionEntry.getKey() + " = ");
-                bufferedWriter.write(sectionEntry.getValue());
+            if (section != topLevelSection) {
+                bufferedWriter.write("[" + section.getName() + "]");
                 bufferedWriter.newLine();
+                writeSection(bufferedWriter, section);
             }
-            bufferedWriter.newLine();
         }
         bufferedWriter.flush();
     }
 
+    private void writeSection(BufferedWriter bufferedWriter, IniSection section) throws IOException {
+        for (Map.Entry<String, String> sectionEntry : section.getAll().entrySet()) {
+            bufferedWriter.write(sectionEntry.getKey() + " = ");
+            bufferedWriter.write(sectionEntry.getValue());
+            bufferedWriter.newLine();
+        }
+        bufferedWriter.newLine();
+    }
+
     /**
      * Create and return new {@link IniSection} with provided name.
      *
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/config/ini/IniParser.java b/modules/cli/src/main/java/org/apache/ignite/cli/config/ini/IniParser.java
index c0eb2bb92..fd475040b 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/config/ini/IniParser.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/config/ini/IniParser.java
@@ -33,6 +33,11 @@ import java.util.regex.Pattern;
  * INI file parser.
  */
 public class IniParser {
+    /**
+     * Section name for properties outside any section.
+     */
+    public static final String NO_SECTION = "NO_SECTION";
+
     private static final Pattern SECTION_PATTERN  = Pattern.compile("\\s*\\[([^]]*)\\]\\s*");
     private static final Pattern KEY_VALUE_PATTER = Pattern.compile("\\s*([^=]*)=(.*)");
     private static final Pattern COMMENT_LINE = Pattern.compile("^[;|#].*");
@@ -70,7 +75,8 @@ public class IniParser {
 
     private Map<String, IniSection> parseIniFile(BufferedReader bufferedReader) throws IOException {
         Map<String, IniSection> map = new LinkedHashMap<>();
-        IniSection currentSection = new IniSection("NO_SECTION");
+        IniSection currentSection = new IniSection(NO_SECTION);
+        map.put(NO_SECTION, currentSection);
         String line;
         while ((line = bufferedReader.readLine()) != null) {
 
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/config/ini/IniProfile.java b/modules/cli/src/main/java/org/apache/ignite/cli/config/ini/IniProfile.java
index dcda97307..127f820d8 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/config/ini/IniProfile.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/config/ini/IniProfile.java
@@ -17,7 +17,7 @@
 
 package org.apache.ignite.cli.config.ini;
 
-import java.util.Map;
+import org.apache.ignite.cli.config.Config;
 import org.apache.ignite.cli.config.Profile;
 
 /**
@@ -25,48 +25,22 @@ import org.apache.ignite.cli.config.Profile;
  */
 public class IniProfile implements Profile {
     private final IniSection section;
-    private final Runnable saveAction;
+    private final IniConfig config;
 
     public IniProfile(IniSection section, Runnable saveAction) {
         this.section = section;
-        this.saveAction = saveAction;
+        this.config = new IniConfig(section, saveAction);
     }
 
+    /** {@inheritDoc} */
     @Override
     public String getName() {
         return section.getName();
     }
 
+    /** {@inheritDoc} */
     @Override
-    public Map<String, String> getAll() {
-        return section.getAll();
-    }
-
-    @Override
-    public String getProperty(String key) {
-        return section.getProperty(key);
-    }
-
-    @Override
-    public String getProperty(String key, String defaultValue) {
-        return section.getProperty(key, defaultValue);
-    }
-
-    @Override
-    public void setProperty(String key, String value) {
-        section.setProperty(key, value);
-        saveAction.run();
-    }
-
-    @Override
-    public void setProperties(Map<String, String> values) {
-        section.setProperties(values);
-        saveAction.run();
-    }
-
-    @Override
-    public void setProperties(Profile copyFrom) {
-        section.setProperties(copyFrom);
-        saveAction.run();
+    public Config getConfig() {
+        return config;
     }
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/config/ini/IniSection.java b/modules/cli/src/main/java/org/apache/ignite/cli/config/ini/IniSection.java
index eb6d95b70..c55c428de 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/config/ini/IniSection.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/config/ini/IniSection.java
@@ -20,7 +20,6 @@ package org.apache.ignite.cli.config.ini;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Map;
-import org.apache.ignite.cli.config.Profile;
 
 /**
  * INI section representation.
@@ -56,8 +55,4 @@ public class IniSection {
     public void setProperties(Map<String, String> values) {
         props.putAll(values);
     }
-
-    public void setProperties(Profile copyFrom) {
-        props.putAll(copyFrom.getAll());
-    }
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/core/flow/builder/Flows.java b/modules/cli/src/main/java/org/apache/ignite/cli/core/flow/builder/Flows.java
index 69c59caeb..af6283601 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/core/flow/builder/Flows.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/core/flow/builder/Flows.java
@@ -110,7 +110,7 @@ public final class Flows {
     }
 
     /**
-     * Create new {@link FlowBuilder} which started from question.
+     * Create new {@link FlowBuilder} which starts from question.
      *
      * @param question question text.
      * @param answers all possible answers.
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/core/repl/Repl.java b/modules/cli/src/main/java/org/apache/ignite/cli/core/repl/Repl.java
index 78e0f7fd5..97fc65886 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/core/repl/Repl.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/core/repl/Repl.java
@@ -51,6 +51,8 @@ public class Repl {
 
     private final boolean tailTipWidgetsEnabled;
 
+    private final Runnable onStart;
+
     /**
      * Constructor.
      *
@@ -63,6 +65,7 @@ public class Repl {
      * @param completer completer instance.
      * @param historyFileName file name for storing commands history.
      * @param tailTipWidgetsEnabled whether tailtip widgets are enabled.
+     * @param onStart callback that will run when REPL is started.
      */
     public Repl(PromptProvider promptProvider,
             Class<?> commandClass,
@@ -72,7 +75,8 @@ public class Repl {
             CallExecutionPipelineProvider provider,
             Completer completer,
             String historyFileName,
-            boolean tailTipWidgetsEnabled
+            boolean tailTipWidgetsEnabled,
+            Runnable onStart
     ) {
         this.promptProvider = promptProvider;
         this.commandClass = commandClass;
@@ -83,6 +87,7 @@ public class Repl {
         this.completer = completer;
         this.historyFileName = historyFileName;
         this.tailTipWidgetsEnabled = tailTipWidgetsEnabled;
+        this.onStart = onStart;
     }
 
     /**
@@ -147,4 +152,8 @@ public class Repl {
     public boolean isTailTipWidgetsEnabled() {
         return tailTipWidgetsEnabled;
     }
+
+    public void onStart() {
+        onStart.run();
+    }
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/core/repl/ReplBuilder.java b/modules/cli/src/main/java/org/apache/ignite/cli/core/repl/ReplBuilder.java
index 838a8f076..55855c50f 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/core/repl/ReplBuilder.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/core/repl/ReplBuilder.java
@@ -49,6 +49,8 @@ public class ReplBuilder {
 
     private boolean tailTipWidgetsEnabled;
 
+    private Runnable onStart = () -> {};
+
     /**
      * Build methods.
      *
@@ -64,7 +66,8 @@ public class ReplBuilder {
                 provider,
                 completer,
                 historyFileName,
-                tailTipWidgetsEnabled
+                tailTipWidgetsEnabled,
+                onStart
         );
     }
 
@@ -127,6 +130,11 @@ public class ReplBuilder {
         return this;
     }
 
+    public ReplBuilder withOnStart(Runnable onStart) {
+        this.onStart = onStart;
+        return this;
+    }
+
     public ReplBuilder withHistoryFileName(String historyFileName) {
         this.historyFileName = historyFileName;
         return this;
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/core/repl/executor/ReplExecutor.java b/modules/cli/src/main/java/org/apache/ignite/cli/core/repl/executor/ReplExecutor.java
index d62d63620..b97b9032a 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/core/repl/executor/ReplExecutor.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/core/repl/executor/ReplExecutor.java
@@ -17,7 +17,6 @@
 
 package org.apache.ignite.cli.core.repl.executor;
 
-import java.io.File;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.List;
@@ -96,7 +95,7 @@ public class ReplExecutor {
                     ? new AggregateCompleter(registry.completer(), repl.getCompleter())
                     : registry.completer());
             if (repl.getHistoryFileName() != null) {
-                reader.variable(LineReader.HISTORY_FILE, new File(StateFolderProvider.getStateFolder(), repl.getHistoryFileName()));
+                reader.variable(LineReader.HISTORY_FILE, StateFolderProvider.getStateFile(repl.getHistoryFileName()));
             }
 
             RegistryCommandExecutor executor = new RegistryCommandExecutor(registry, parser);
@@ -110,6 +109,8 @@ public class ReplExecutor {
 
             QuestionAskerFactory.setReadWriter(new JlineQuestionWriterReader(reader));
 
+            repl.onStart();
+
             while (!interrupted.get()) {
                 try {
                     executor.cleanUp();
diff --git a/modules/cli/src/test/java/org/apache/ignite/cli/commands/cliconfig/TestConfigManagerHelper.java b/modules/cli/src/test/java/org/apache/ignite/cli/commands/cliconfig/TestConfigManagerHelper.java
index d4e6ab748..797787f7c 100644
--- a/modules/cli/src/test/java/org/apache/ignite/cli/commands/cliconfig/TestConfigManagerHelper.java
+++ b/modules/cli/src/test/java/org/apache/ignite/cli/commands/cliconfig/TestConfigManagerHelper.java
@@ -30,34 +30,46 @@ import org.apache.ignite.cli.config.ConfigManager;
  * Test factory for {@link ConfigManager}.
  */
 public class TestConfigManagerHelper {
-    public static final String EMPTY = "empty.ini";
-    public static final String TWO_SECTION_WITH_INTERNAL_PART = "two_section_with_internal.ini";
-    public static final String TWO_SECTION_WITHOUT_INTERNAL_PART = "two_section_without_internal.ini";
-    public static final String INTEGRATION_TESTS = "integration_tests.ini";
+    private static final String EMPTY = "empty.ini";
+    private static final String TWO_SECTION_WITH_INTERNAL_PART = "two_section_with_internal.ini";
+    private static final String TWO_SECTION_WITHOUT_INTERNAL_PART = "two_section_without_internal.ini";
+    private static final String INTEGRATION_TESTS = "integration_tests.ini";
+
+    private static final String CLUSTER_URL_NON_DEFAULT = "cluster_url_non_default.ini";
 
     public static File createEmptyConfig() {
-        return createIniFile(EMPTY);
+        return copyResourceToTempFile(EMPTY);
     }
 
     public static File createSectionWithInternalPart() {
-        return createIniFile(TWO_SECTION_WITH_INTERNAL_PART);
+        return copyResourceToTempFile(TWO_SECTION_WITH_INTERNAL_PART);
     }
 
     public static File createSectionWithoutInternalPart() {
-        return createIniFile(TWO_SECTION_WITHOUT_INTERNAL_PART);
+        return copyResourceToTempFile(TWO_SECTION_WITHOUT_INTERNAL_PART);
     }
 
     public static File createIntegrationTests() {
-        return createIniFile(INTEGRATION_TESTS);
+        return copyResourceToTempFile(INTEGRATION_TESTS);
+    }
+
+    public static File createClusterUrlNonDefault() {
+        return copyResourceToTempFile(CLUSTER_URL_NON_DEFAULT);
     }
 
-    private static File createIniFile(String iniResource) {
+    /**
+     * Helper method to copy file from the classpath to the temporary file which will be deleted on exit.
+     *
+     * @param resource The resource name
+     * @return A temporary file containing the resource's contents
+     */
+    public static File copyResourceToTempFile(String resource) {
         try {
             File tempFile = File.createTempFile("cli", null);
 
             try (FileOutputStream fileOutputStream = new FileOutputStream(tempFile)) {
                 FileChannel dest = fileOutputStream.getChannel();
-                InputStream resourceAsStream = TestConfigManagerHelper.class.getClassLoader().getResourceAsStream(iniResource);
+                InputStream resourceAsStream = TestConfigManagerHelper.class.getClassLoader().getResourceAsStream(resource);
                 ReadableByteChannel src = Channels.newChannel(resourceAsStream);
                 dest.transferFrom(src, 0, Integer.MAX_VALUE);
                 tempFile.deleteOnExit();
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/config/Profile.java b/modules/cli/src/test/java/org/apache/ignite/cli/config/TestStateConfigHelper.java
similarity index 56%
copy from modules/cli/src/main/java/org/apache/ignite/cli/config/Profile.java
copy to modules/cli/src/test/java/org/apache/ignite/cli/config/TestStateConfigHelper.java
index 31d47a5fb..62ce9cd50 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/config/Profile.java
+++ b/modules/cli/src/test/java/org/apache/ignite/cli/config/TestStateConfigHelper.java
@@ -17,23 +17,24 @@
 
 package org.apache.ignite.cli.config;
 
-import java.util.Map;
+import org.apache.ignite.cli.commands.cliconfig.TestConfigManagerHelper;
 
 /**
- * Ignite CLI Profile.
+ * Test factory for application state config.
  */
-public interface Profile {
-    String getName();
+public class TestStateConfigHelper {
+    public static final String EMPTY = "empty.ini";
+    public static final String LAST_CONNECTED_DEFAULT = "last_connected_default.ini";
 
-    Map<String, String> getAll();
+    public static Config createEmptyConfig() {
+        return createConfig(EMPTY);
+    }
 
-    String getProperty(String key);
+    public static Config createLastConnectedDefault() {
+        return createConfig(LAST_CONNECTED_DEFAULT);
+    }
 
-    String getProperty(String key, String defaultValue);
-
-    void setProperty(String key, String value);
-
-    void setProperties(Map<String, String> values);
-
-    void setProperties(Profile copyFrom);
+    private static Config createConfig(String resource) {
+        return StateConfig.getStateConfig(TestConfigManagerHelper.copyResourceToTempFile(resource));
+    }
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/config/Profile.java b/modules/cli/src/test/java/org/apache/ignite/cli/config/TestStateConfigProvider.java
similarity index 68%
copy from modules/cli/src/main/java/org/apache/ignite/cli/config/Profile.java
copy to modules/cli/src/test/java/org/apache/ignite/cli/config/TestStateConfigProvider.java
index 31d47a5fb..7b74b8d95 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/config/Profile.java
+++ b/modules/cli/src/test/java/org/apache/ignite/cli/config/TestStateConfigProvider.java
@@ -17,23 +17,20 @@
 
 package org.apache.ignite.cli.config;
 
-import java.util.Map;
+import io.micronaut.context.annotation.Replaces;
+import jakarta.inject.Singleton;
 
 /**
- * Ignite CLI Profile.
+ * Test implementation of {@link StateConfigProvider}.
  */
-public interface Profile {
-    String getName();
+@Singleton
+@Replaces(StateConfigProvider.class)
+public class TestStateConfigProvider implements StateConfigProvider {
 
-    Map<String, String> getAll();
+    public Config config = TestStateConfigHelper.createEmptyConfig();
 
-    String getProperty(String key);
-
-    String getProperty(String key, String defaultValue);
-
-    void setProperty(String key, String value);
-
-    void setProperties(Map<String, String> values);
-
-    void setProperties(Profile copyFrom);
+    @Override
+    public Config get() {
+        return config;
+    }
 }
diff --git a/modules/cli/src/test/resources/cluster_url_non_default.ini b/modules/cli/src/test/resources/cluster_url_non_default.ini
new file mode 100644
index 000000000..ebdf2299c
--- /dev/null
+++ b/modules/cli/src/test/resources/cluster_url_non_default.ini
@@ -0,0 +1,5 @@
+[ignitecli_internal]
+current_profile = default
+
+[default]
+ignite.cluster-endpoint-url = http://localhost:10301
diff --git a/modules/cli/src/test/resources/last_connected_default.ini b/modules/cli/src/test/resources/last_connected_default.ini
new file mode 100644
index 000000000..7c7493766
--- /dev/null
+++ b/modules/cli/src/test/resources/last_connected_default.ini
@@ -0,0 +1 @@
+ignite.last-connected-url = http://localhost:10300