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 2020/12/18 22:44:59 UTC

[ignite-3] 01/01: IGNITE-13610 Added initial version of unified CLI tool. Fixes #4

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

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

commit dd017fe9cca88d6165116c3a1ad50f9785813676
Author: Kirill Gusakov <kg...@gmail.com>
AuthorDate: Sat Dec 19 01:44:22 2020 +0300

    IGNITE-13610 Added initial version of unified CLI tool. Fixes #4
    
    Signed-off-by: Slava Koptilin <sl...@gmail.com>
---
 .gitignore                                         |   1 +
 modules/cli-demo/cli-common/pom.xml                |  33 ++
 .../apache/ignite/cli/common/IgniteCommand.java    |  21 +
 modules/cli-demo/cli/ignite.sh                     |  11 +
 modules/cli-demo/cli/pom.xml                       | 234 ++++++++++++
 .../apache/ignite/cli/CliPathsConfigLoader.java    |  79 ++++
 .../java/org/apache/ignite/cli/CliVersionInfo.java |  46 +++
 .../java/org/apache/ignite/cli/CommandFactory.java |  36 ++
 .../java/org/apache/ignite/cli/ErrorHandler.java   |  54 +++
 .../org/apache/ignite/cli/HelpFactoryImpl.java     | 167 ++++++++
 .../org/apache/ignite/cli/IgniteCLIException.java  |  30 ++
 .../java/org/apache/ignite/cli/IgniteCliApp.java   |  30 ++
 .../java/org/apache/ignite/cli/IgnitePaths.java    |  60 +++
 .../org/apache/ignite/cli/InteractiveWrapper.java  |  93 +++++
 .../src/main/java/org/apache/ignite/cli/Table.java |  99 +++++
 .../org/apache/ignite/cli/VersionProvider.java     |  39 ++
 .../ignite/cli/builtins/SystemPathResolver.java    |  46 +++
 .../cli/builtins/config/ConfigurationClient.java   | 110 ++++++
 .../cli/builtins/config/HttpClientFactory.java     |  34 ++
 .../cli/builtins/init/InitIgniteCommand.java       | 141 +++++++
 .../cli/builtins/module/MavenArtifactResolver.java | 256 +++++++++++++
 .../cli/builtins/module/MavenCoordinates.java      |  45 +++
 .../ignite/cli/builtins/module/ModuleManager.java  | 183 +++++++++
 .../ignite/cli/builtins/module/ModuleStorage.java  | 120 ++++++
 .../ignite/cli/builtins/module/ResolveResult.java  |  33 ++
 .../builtins/module/StandardModuleDefinition.java  |  38 ++
 .../ignite/cli/builtins/node/NodeManager.java      | 231 +++++++++++
 .../ignite/cli/spec/AbstractCommandSpec.java       |  28 ++
 .../apache/ignite/cli/spec/ConfigCommandSpec.java  | 107 ++++++
 .../org/apache/ignite/cli/spec/IgniteCliSpec.java  | 128 +++++++
 .../ignite/cli/spec/InitIgniteCommandSpec.java     |  36 ++
 .../apache/ignite/cli/spec/ModuleCommandSpec.java  | 108 ++++++
 .../apache/ignite/cli/spec/NodeCommandSpec.java    | 139 +++++++
 .../cli/src/main/resources/builtin_modules.conf    |  13 +
 .../cli/src/main/resources/default-config.xml      |  29 ++
 .../cli-demo/cli/src/main/resources/logback.xml    |  14 +
 .../cli/src/main/resources/version.properties      |  18 +
 .../apache/ignite/cli/IgniteCliInterfaceTest.java  | 425 +++++++++++++++++++++
 .../demo-module-all/demo-module-cli/pom.xml        |  55 +++
 .../ignite/snapshot/cli/SnapshotCommand.java       |  65 ++++
 .../org.apache.ignite.cli.common.IgniteCommand     |   1 +
 .../cli-demo/demo-module-all/demo-module/pom.xml   |  34 ++
 .../org/apache/ignite/snapshot/IgniteSnapshot.java |  25 ++
 modules/cli-demo/demo-module-all/pom.xml           |  56 +++
 modules/cli-demo/pom.xml                           |  90 +++++
 pom.xml                                            |   1 +
 46 files changed, 3642 insertions(+)

diff --git a/.gitignore b/.gitignore
index 5231862..c3484a1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
 .idea
 *.iml
 target
+.DS_Store
diff --git a/modules/cli-demo/cli-common/pom.xml b/modules/cli-demo/cli-common/pom.xml
new file mode 100644
index 0000000..add7fab
--- /dev/null
+++ b/modules/cli-demo/cli-common/pom.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>ignite-cli-demo</artifactId>
+        <groupId>org.apache.ignite</groupId>
+        <version>3.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>ignite-cli-common</artifactId>
+
+
+</project>
diff --git a/modules/cli-demo/cli-common/src/main/java/org/apache/ignite/cli/common/IgniteCommand.java b/modules/cli-demo/cli-common/src/main/java/org/apache/ignite/cli/common/IgniteCommand.java
new file mode 100644
index 0000000..b7088db
--- /dev/null
+++ b/modules/cli-demo/cli-common/src/main/java/org/apache/ignite/cli/common/IgniteCommand.java
@@ -0,0 +1,21 @@
+/*
+ * 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.common;
+
+public interface IgniteCommand {
+}
diff --git a/modules/cli-demo/cli/ignite.sh b/modules/cli-demo/cli/ignite.sh
new file mode 100644
index 0000000..64c177b
--- /dev/null
+++ b/modules/cli-demo/cli/ignite.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+set -o nounset; set -o errexit; set -o pipefail; set -o errtrace; set -o functrace
+
+MYSELF=`which "${0}" 2>/dev/null`
+[ $? -gt 0 -a -f "${0}" ] && MYSELF="./${0}"
+java=java
+if test -n "${JAVA_HOME:-}"; then
+    java="${JAVA_HOME}/bin/java"
+fi
+exec "${java}" ${java_args:-} -jar ${MYSELF} "$@"
+exit 1
diff --git a/modules/cli-demo/cli/pom.xml b/modules/cli-demo/cli/pom.xml
new file mode 100644
index 0000000..e73714f
--- /dev/null
+++ b/modules/cli-demo/cli/pom.xml
@@ -0,0 +1,234 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ 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.
+-->
+
+<!--
+    POM file.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.ignite</groupId>
+        <artifactId>ignite-cli-demo</artifactId>
+        <version>3.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>ignite</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <version>1.2.3</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>2.11.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-cli-common</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.ivy</groupId>
+            <artifactId>ivy</artifactId>
+            <version>2.5.0</version>
+        </dependency>
+        <dependency>
+            <groupId>info.picocli</groupId>
+            <artifactId>picocli-shell-jline3</artifactId>
+            <version>${picocli.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.fusesource.jansi</groupId>
+            <artifactId>jansi</artifactId>
+            <version>1.18</version>
+        </dependency>
+        <dependency>
+            <groupId>info.picocli</groupId>
+            <artifactId>picocli</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.jetbrains</groupId>
+            <artifactId>annotations</artifactId>
+            <version>16.0.3</version>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <version>5.7.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-junit-jupiter</artifactId>
+            <version>3.3.3</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>io.micronaut</groupId>
+            <artifactId>micronaut-inject-java</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.micronaut</groupId>
+            <artifactId>micronaut-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.typesafe</groupId>
+            <artifactId>config</artifactId>
+            <version>1.4.1</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.freva</groupId>
+            <artifactId>ascii-table</artifactId>
+            <version>1.1.0</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+                <filtering>true</filtering>
+            </resource>
+        </resources>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <version>3.2.0</version>
+
+                <configuration>
+                    <finalName>ignite</finalName>
+                    <appendAssemblyId>false</appendAssemblyId>
+                    <descriptorRefs>
+                        <descriptorRef>jar-with-dependencies</descriptorRef>
+                    </descriptorRefs>
+                    <archive>
+                        <manifest>
+                            <mainClass>org.apache.ignite.cli.IgniteCliApp</mainClass>
+                        </manifest>
+                    </archive>
+                </configuration>
+
+                <executions>
+                    <execution>
+                        <id>make-assembly</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <!-- Uncomment to enable incremental compilation -->
+                    <!-- <useIncrementalCompilation>false</useIncrementalCompilation> -->
+                    <annotationProcessorPaths>
+                        <path>
+                            <groupId>io.micronaut</groupId>
+                            <artifactId>micronaut-inject-java</artifactId>
+                            <version>${micronaut.version}</version>
+                        </path>
+                        <path>
+                            <groupId>info.picocli</groupId>
+                            <artifactId>picocli-codegen</artifactId>
+                            <version>${picocli.version}</version>
+                        </path>
+                    </annotationProcessorPaths>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>test-compile</id>
+                        <goals>
+                            <goal>testCompile</goal>
+                        </goals>
+                        <configuration>
+                            <annotationProcessorPaths>
+                                <path>
+                                    <groupId>io.micronaut</groupId>
+                                    <artifactId>micronaut-inject-java</artifactId>
+                                    <version>${micronaut.version}</version>
+                                </path>
+                            </annotationProcessorPaths>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>2.22.2</version>
+            </plugin>
+
+            <plugin>
+                <artifactId>maven-antrun-plugin</artifactId>
+                <version>3.0.0</version>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <configuration>
+                            <target>
+                                <concat destfile="${project.build.directory}/ignite" binary="true">
+                                    <filelist dir="${project.build.directory}/"
+                                        files="../ignite.sh,ignite.jar"/>
+                                </concat>
+                                <chmod file="${project.build.directory}/ignite" perm="+x"/>
+                            </target>
+                        </configuration>
+                        <goals>
+                            <goal>run</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>com.akathist.maven.plugins.launch4j</groupId>
+                <artifactId>launch4j-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>l4j-clui</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>launch4j</goal>
+                        </goals>
+                        <configuration>
+                            <headerType>console</headerType>
+                            <jar>${project.build.directory}/ignite.jar</jar>
+                            <outfile>${project.build.directory}/ignite.exe</outfile>
+                            <jre>
+                                <path>%JAVA_HOME%</path>
+                                <minVersion>11</minVersion>
+                            </jre>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/CliPathsConfigLoader.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/CliPathsConfigLoader.java
new file mode 100644
index 0000000..794086a
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/CliPathsConfigLoader.java
@@ -0,0 +1,79 @@
+/*
+ * 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;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.util.Optional;
+import java.util.Properties;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.apache.ignite.cli.builtins.SystemPathResolver;
+
+@Singleton
+public class CliPathsConfigLoader {
+
+    private final SystemPathResolver pathResolver;
+    private final String version;
+
+    @Inject
+    public CliPathsConfigLoader(SystemPathResolver pathResolver,
+        CliVersionInfo cliVersionInfo) {
+        this.pathResolver = pathResolver;
+        this.version = cliVersionInfo.version;
+    }
+
+    public Optional<IgnitePaths> loadIgnitePathsConfig() {
+        return searchConfigPathsFile()
+            .map(f -> CliPathsConfigLoader.readConfigFile(f, version));
+    }
+
+    public IgnitePaths loadIgnitePathsOrThrowError() {
+        Optional<IgnitePaths> ignitePaths = loadIgnitePathsConfig();
+        if (ignitePaths.isPresent())
+            return ignitePaths.get();
+        else
+            throw new IgniteCLIException("To execute node module/node management commands you must run 'init' first");
+    }
+
+    public Optional<File> searchConfigPathsFile() {
+        File homeDirCfg = pathResolver.osHomeDirectoryPath().resolve(".ignitecfg").toFile();
+        if (homeDirCfg.exists())
+            return Optional.of(homeDirCfg);
+
+        return Optional.empty();
+    }
+
+    private static IgnitePaths readConfigFile(File configFile, String version) {
+        try (InputStream inputStream = new FileInputStream(configFile)) {
+            Properties properties = new Properties();
+            properties.load(inputStream);
+            if ((properties.getProperty("bin") == null) || (properties.getProperty("work") == null))
+                throw new IgniteCLIException("Config file has wrong format. " +
+                    "It must contain correct paths to bin and work dirs");
+            return new IgnitePaths(Path.of(properties.getProperty("bin")),
+                Path.of(properties.getProperty("work")), version);
+        }
+        catch (IOException e) {
+            throw new IgniteCLIException("Can't read config file");
+        }
+    }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/CliVersionInfo.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/CliVersionInfo.java
new file mode 100644
index 0000000..95d24b7
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/CliVersionInfo.java
@@ -0,0 +1,46 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+import javax.inject.Singleton;
+import io.micronaut.core.annotation.Introspected;
+
+@Singleton
+@Introspected
+public class CliVersionInfo {
+
+    public final String version;
+
+    public CliVersionInfo() {
+        try (InputStream inputStream = CliVersionInfo.class.getResourceAsStream("/version.properties")) {
+            Properties prop = new Properties();
+            prop.load(inputStream);
+            version = prop.getProperty("version", "undefined");
+        }
+        catch (IOException e) {
+            throw new IgniteCLIException("Can' read ignite version info");
+        }
+    }
+
+    public CliVersionInfo(String version) {
+        this.version = version;
+    }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/CommandFactory.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/CommandFactory.java
new file mode 100644
index 0000000..f57e0a8
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/CommandFactory.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+import java.util.Optional;
+import io.micronaut.context.ApplicationContext;
+import picocli.CommandLine;
+
+public class CommandFactory implements CommandLine.IFactory {
+
+    private final ApplicationContext applicationContext;
+
+    public CommandFactory(ApplicationContext applicationContext) {
+        this.applicationContext = applicationContext;
+    }
+
+    @Override public <K> K create(Class<K> cls) throws Exception {
+        Optional<K> bean = applicationContext.findOrInstantiateBean(cls);
+        return bean.isPresent() ? bean.get() : CommandLine.defaultFactory().create(cls);// custom factory lookup or instantiation
+    }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/ErrorHandler.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/ErrorHandler.java
new file mode 100644
index 0000000..eba44d7
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/ErrorHandler.java
@@ -0,0 +1,54 @@
+/*
+ * 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;
+
+import javax.inject.Inject;
+import io.micronaut.context.ApplicationContext;
+import org.apache.ignite.cli.spec.IgniteCliSpec;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import picocli.CommandLine;
+
+public class ErrorHandler implements CommandLine.IExecutionExceptionHandler, CommandLine.IParameterExceptionHandler {
+    Logger logger = LoggerFactory.getLogger(ErrorHandler.class);
+
+    @Inject
+    private ApplicationContext applicationContext;
+
+    @Override public int handleExecutionException(Exception ex, CommandLine cmd,
+        CommandLine.ParseResult parseResult) throws Exception {
+        if (ex instanceof IgniteCLIException)
+            cmd.getErr().println(cmd.getColorScheme().errorText(ex.getMessage()));
+        else
+            logger.error("", ex);
+
+        return cmd.getExitCodeExceptionMapper() != null
+            ? cmd.getExitCodeExceptionMapper().getExitCode(ex)
+            : cmd.getCommandSpec().exitCodeOnExecutionException();
+    }
+
+    @Override public int handleParseException(CommandLine.ParameterException ex, String[] args) {
+        CommandLine cli = ex.getCommandLine();
+
+        cli.getErr().println(cli.getColorScheme().errorText("ERROR: ") + ex.getMessage() + '\n');
+
+        cli.usage(cli.getOut());
+
+        return cli.getCommandSpec().exitCodeOnInvalidInput();
+    }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/HelpFactoryImpl.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/HelpFactoryImpl.java
new file mode 100644
index 0000000..a5c40b5
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/HelpFactoryImpl.java
@@ -0,0 +1,167 @@
+package org.apache.ignite.cli;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Optional;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.ToIntFunction;
+import java.util.stream.Collectors;
+import picocli.CommandLine;
+
+public class HelpFactoryImpl implements CommandLine.IHelpFactory {
+
+    public static final String SECTION_KEY_BANNER = "banner";
+    public static final String SECTION_KEY_SYNOPSIS_EXTENSION = "synopsisExt";
+
+    private static final String[] BANNER = new String[] {
+        "                       ___                         __",
+        "                      /   |   ____   ____ _ _____ / /_   ___",
+        "  @|red,bold       ⣠⣶⣿|@          / /| |  / __ \\ / __ `// ___// __ \\ / _ \\",
+        "  @|red,bold      ⣿⣿⣿⣿|@         / ___ | / /_/ // /_/ // /__ / / / //  __/",
+        "  @|red,bold  ⢠⣿⡏⠈⣿⣿⣿⣿⣷|@       /_/  |_|/ .___/ \\__,_/ \\___//_/ /_/ \\___/",
+        "  @|red,bold ⢰⣿⣿⣿⣧⠈⢿⣿⣿⣿⣿⣦|@            /_/",
+        "  @|red,bold ⠘⣿⣿⣿⣿⣿⣦⠈⠛⢿⣿⣿⣿⡄|@       ____               _  __           _____",
+        "  @|red,bold  ⠈⠛⣿⣿⣿⣿⣿⣿⣦⠉⢿⣿⡟|@      /  _/____ _ ____   (_)/ /_ ___     |__  /",
+        "  @|red,bold ⢰⣿⣶⣀⠈⠙⠿⣿⣿⣿⣿ ⠟⠁|@      / / / __ `// __ \\ / // __// _ \\     /_ <",
+        "  @|red,bold ⠈⠻⣿⣿⣿⣿⣷⣤⠙⢿⡟|@       _/ / / /_/ // / / // // /_ /  __/   ___/ /",
+        "  @|red,bold       ⠉⠉⠛⠏⠉|@      /___/ \\__, //_/ /_//_/ \\__/ \\___/   /____/",
+        "                        /____/\n"};
+
+    @Override public CommandLine.Help create(CommandLine.Model.CommandSpec commandSpec,
+        CommandLine.Help.ColorScheme colorScheme) {
+        commandSpec.usageMessage().sectionKeys(Arrays.asList(
+            SECTION_KEY_BANNER,
+            CommandLine.Model.UsageMessageSpec.SECTION_KEY_HEADER,
+            CommandLine.Model.UsageMessageSpec.SECTION_KEY_DESCRIPTION,
+            CommandLine.Model.UsageMessageSpec.SECTION_KEY_SYNOPSIS_HEADING,
+            CommandLine.Model.UsageMessageSpec.SECTION_KEY_SYNOPSIS,
+            SECTION_KEY_SYNOPSIS_EXTENSION,
+            CommandLine.Model.UsageMessageSpec.SECTION_KEY_PARAMETER_LIST_HEADING,
+            CommandLine.Model.UsageMessageSpec.SECTION_KEY_PARAMETER_LIST,
+            CommandLine.Model.UsageMessageSpec.SECTION_KEY_OPTION_LIST_HEADING,
+            CommandLine.Model.UsageMessageSpec.SECTION_KEY_OPTION_LIST,
+            CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST_HEADING,
+            CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST));
+
+        var sectionMap = new HashMap<String, CommandLine.IHelpSectionRenderer>();
+
+        boolean hasCommands = !commandSpec.subcommands().isEmpty();
+        boolean hasOptions = commandSpec.options().stream().anyMatch(o -> !o.hidden());
+        boolean hasParameters = !commandSpec.positionalParameters().isEmpty();
+
+        sectionMap.put(SECTION_KEY_BANNER,
+            help -> Arrays
+                .stream(BANNER)
+                .map(CommandLine.Help.Ansi.AUTO::string)
+                .collect(Collectors.joining("\n")) +
+                "\n"
+        );
+
+        sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_HEADER,
+            help -> CommandLine.Help.Ansi.AUTO.string(
+                Arrays.stream(help.commandSpec().version())
+                    .collect(Collectors.joining("\n")) + "\n\n")
+        );
+
+        sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_DESCRIPTION,
+            help -> CommandLine.Help.Ansi.AUTO.string("@|bold,green " + help.commandSpec().qualifiedName() +
+                "|@\n  " + help.description() + "\n"));
+
+        sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_SYNOPSIS_HEADING,
+            help -> CommandLine.Help.Ansi.AUTO.string("@|bold USAGE|@\n"));
+
+        sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_SYNOPSIS,
+            help -> {
+                StringBuilder sb = new StringBuilder();
+
+                sb.append("  ");
+                sb.append(help.colorScheme().commandText(help.commandSpec().qualifiedName()));
+
+                if (hasCommands) {
+                    sb.append(help.colorScheme().commandText(" <COMMAND>"));
+                }
+                else {
+                    if (hasOptions)
+                        sb.append(help.colorScheme().optionText(" [OPTIONS]"));
+
+                    if (hasParameters) {
+                        for (CommandLine.Model.PositionalParamSpec parameter : commandSpec.positionalParameters())
+                            sb.append(' ').append(help.colorScheme().parameterText(parameter.paramLabel()));
+                    }
+                }
+
+                sb.append("\n\n");
+
+                return sb.toString();
+            });
+
+
+        Optional.ofNullable(commandSpec.usageMessage().sectionMap().get(SECTION_KEY_SYNOPSIS_EXTENSION))
+            .ifPresent(v -> sectionMap.put(SECTION_KEY_SYNOPSIS_EXTENSION, v));
+
+        sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_PARAMETER_LIST_HEADING,
+            help -> hasParameters ? CommandLine.Help.Ansi.AUTO.string("@|bold REQUIRED PARAMETERS|@\n") : "");
+
+        sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_PARAMETER_LIST, new TableRenderer<>(
+            h -> h.commandSpec().positionalParameters(),
+            p -> p.paramLabel().length(),
+            (h, p) -> h.colorScheme().parameterText(p.paramLabel()),
+            (h, p) -> h.colorScheme().text(p.description()[0])));
+
+        sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_OPTION_LIST_HEADING,
+            help -> hasOptions ? CommandLine.Help.Ansi.AUTO.string("@|bold OPTIONS|@\n") : "");
+
+        if (hasOptions)
+            sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_OPTION_LIST, new TableRenderer<>(
+                h -> h.commandSpec().options(),
+                o -> o.shortestName().length() + o.paramLabel().length() + 1,
+                (h, o) -> h.colorScheme().optionText(o.shortestName()).concat("=").concat(o.paramLabel()),
+                (h, o) -> h.colorScheme().text(o.description()[0])));
+
+        sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST_HEADING,
+            help -> hasCommands ? CommandLine.Help.Ansi.AUTO.string("@|bold COMMANDS|@\n") : "");
+
+        sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST, new TableRenderer<>(
+            h -> h.subcommands().values(),
+            c -> c.commandSpec().name().length(),
+            (h, c) -> h.colorScheme().commandText(c.commandSpec().name()),
+            (h, c) -> h.colorScheme().text(c.description().stripTrailing())));
+        commandSpec.usageMessage().sectionMap(sectionMap);
+        return new CommandLine.Help(commandSpec, colorScheme);
+    }
+
+    private static class TableRenderer<T> implements CommandLine.IHelpSectionRenderer {
+        private final Function<CommandLine.Help, Collection<T>> itemsFunc;
+        private final ToIntFunction<T> nameLenFunc;
+        private final BiFunction<CommandLine.Help, T, CommandLine.Help.Ansi.Text> nameFunc;
+        private final BiFunction<CommandLine.Help, T, CommandLine.Help.Ansi.Text> descriptionFunc;
+
+        TableRenderer(Function<CommandLine.Help, Collection<T>> itemsFunc, ToIntFunction<T> nameLenFunc,
+            BiFunction<CommandLine.Help, T, CommandLine.Help.Ansi.Text> nameFunc, BiFunction<CommandLine.Help, T, CommandLine.Help.Ansi.Text> descriptionFunc) {
+            this.itemsFunc = itemsFunc;
+            this.nameLenFunc = nameLenFunc;
+            this.nameFunc = nameFunc;
+            this.descriptionFunc = descriptionFunc;
+        }
+
+        @Override public String render(CommandLine.Help help) {
+            Collection<T> items = itemsFunc.apply(help);
+
+            if (items.isEmpty())
+                return "";
+
+            int len = 2 + items.stream().mapToInt(nameLenFunc).max().getAsInt();
+
+            CommandLine.Help.TextTable table = CommandLine.Help.TextTable.forColumns(help.colorScheme(),
+                new CommandLine.Help.Column(len, 2, CommandLine.Help.Column.Overflow.SPAN),
+                new CommandLine.Help.Column(160 - len, 4, CommandLine.Help.Column.Overflow.WRAP));
+
+            for (T item : items)
+                table.addRowValues(nameFunc.apply(help, item), descriptionFunc.apply(help, item));
+
+            return table.toString() + '\n';
+        }
+    }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/IgniteCLIException.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/IgniteCLIException.java
new file mode 100644
index 0000000..1132008
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/IgniteCLIException.java
@@ -0,0 +1,30 @@
+/*
+ * 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;
+
+public class IgniteCLIException extends RuntimeException {
+    private static final long serialVersionUID = 0L;
+
+    public IgniteCLIException(String message) {
+        super(message);
+    }
+
+    public IgniteCLIException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/IgniteCliApp.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/IgniteCliApp.java
new file mode 100644
index 0000000..020e97e
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/IgniteCliApp.java
@@ -0,0 +1,30 @@
+/*
+ * 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;
+
+import io.micronaut.context.ApplicationContext;
+import org.apache.ignite.cli.spec.IgniteCliSpec;
+
+public class IgniteCliApp {
+    public static void main(String... args) {
+        ApplicationContext applicationContext = ApplicationContext.run();
+
+        System.exit(IgniteCliSpec.initCli(applicationContext).execute(args));
+    }
+
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/IgnitePaths.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/IgnitePaths.java
new file mode 100644
index 0000000..4a5ac67
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/IgnitePaths.java
@@ -0,0 +1,60 @@
+/*
+ * 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;
+
+import java.nio.file.Path;
+
+public class IgnitePaths {
+
+    public final Path binDir;
+    public final Path workDir;
+    private final String version;
+
+    public IgnitePaths(Path binDir, Path workDir, String version) {
+        this.binDir = binDir;
+        this.workDir = workDir;
+        this.version = version;
+    }
+
+
+    public Path cliLibsDir() {
+        return binDir.resolve(version).resolve("cli");
+    }
+
+    public Path libsDir() {
+        return binDir.resolve(version).resolve("libs");
+    }
+
+    public Path cliPidsDir() {
+        return workDir.resolve("cli").resolve("pids");
+    }
+
+    public Path installedModulesFile() {
+        return workDir.resolve("modules.json");
+    }
+    
+    public Path serverConfigDir() {
+        return workDir.resolve("config");
+    }
+
+    public Path serverDefaultConfigFile() {
+        return serverConfigDir().resolve("default-config.xml");
+    }
+
+
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/InteractiveWrapper.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/InteractiveWrapper.java
new file mode 100644
index 0000000..f60c942
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/InteractiveWrapper.java
@@ -0,0 +1,93 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.jline.console.SystemRegistry;
+import org.jline.console.impl.SystemRegistryImpl;
+import org.jline.keymap.KeyMap;
+import org.jline.reader.Binding;
+import org.jline.reader.EndOfFileException;
+import org.jline.reader.LineReader;
+import org.jline.reader.LineReaderBuilder;
+import org.jline.reader.MaskingCallback;
+import org.jline.reader.Parser;
+import org.jline.reader.Reference;
+import org.jline.reader.UserInterruptException;
+import org.jline.reader.impl.DefaultParser;
+import org.jline.terminal.Terminal;
+import org.jline.terminal.TerminalBuilder;
+import org.jline.widget.TailTipWidgets;
+import picocli.CommandLine;
+import picocli.shell.jline3.PicocliCommands;
+
+public class InteractiveWrapper {
+
+    public void run(CommandLine cmd) {
+        PicocliCommands picocliCommands = new PicocliCommands(workDir(), cmd) {
+            @Override public Object invoke(CommandSession ses, String cmd, Object... args) throws Exception {
+                return execute(ses, cmd, (String[])args);
+            }
+        };
+
+        Parser parser = new DefaultParser();
+        try (Terminal terminal = TerminalBuilder.builder().build()) {
+            SystemRegistry systemRegistry = new SystemRegistryImpl(parser, terminal, InteractiveWrapper::workDir, null);
+            systemRegistry.setCommandRegistries(picocliCommands);
+
+            LineReader reader = LineReaderBuilder.builder()
+                .terminal(terminal)
+                .completer(systemRegistry.completer())
+                .parser(parser)
+                .variable(LineReader.LIST_MAX, 50)   // max tab completion candidates
+                .build();
+
+            TailTipWidgets widgets = new TailTipWidgets(reader, systemRegistry::commandDescription, 5, TailTipWidgets.TipType.COMPLETER);
+            widgets.enable();
+            KeyMap<Binding> keyMap = reader.getKeyMaps().get("main");
+            keyMap.bind(new Reference("tailtip-toggle"), KeyMap.alt("s"));
+
+            String prompt = "ignite> ";
+            String rightPrompt = null;
+
+            String line;
+            while (true) {
+                try {
+                    systemRegistry.cleanUp();
+                    line = reader.readLine(prompt, rightPrompt, (MaskingCallback) null, null);
+                    systemRegistry.execute(line);
+                } catch (UserInterruptException e) {
+                    // Ignore
+                } catch (EndOfFileException e) {
+                    return;
+                } catch (Exception e) {
+                    systemRegistry.trace(e);
+                }
+            }
+        }
+        catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private static Path workDir() {
+        return Paths.get(System.getProperty("user.dir"));
+    }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/Table.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/Table.java
new file mode 100644
index 0000000..5a0f6cb
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/Table.java
@@ -0,0 +1,99 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import picocli.CommandLine.Help.Ansi.Text;
+import picocli.CommandLine.Help.ColorScheme;
+
+public class Table {
+    private final int indent;
+
+    private final ColorScheme colorScheme;
+
+    private final Collection<Text[]> data = new ArrayList<>();
+
+    private int[] lengths;
+
+    public Table(int indent, ColorScheme colorScheme) {
+        if (indent < 0)
+            throw new IllegalArgumentException("Indent can't be negative.");
+
+        this.indent = indent;
+        this.colorScheme = colorScheme;
+    }
+
+    public void addRow(Object... items) {
+        if (lengths == null) {
+            lengths = new int[items.length];
+        }
+        else if (items.length != lengths.length) {
+            throw new IllegalArgumentException("Wrong number of items.");
+        }
+
+        Text[] row = new Text[items.length];
+
+        for (int i = 0; i < items.length; i++) {
+            Text item = colorScheme.text(items[i].toString());
+
+            row[i] = item;
+
+            lengths[i] = Math.max(lengths[i], item.getCJKAdjustedLength());
+        }
+
+        data.add(row);
+    }
+
+    @Override public String toString() {
+        String indentStr = " ".repeat(indent);
+
+        StringBuilder sb = new StringBuilder();
+
+        for (Text[] row : data) {
+            sb.append(indentStr);
+
+            appendLine(sb);
+            appendRow(sb, row);
+        }
+
+        appendLine(sb);
+
+        return sb.toString();
+    }
+
+    private void appendLine(StringBuilder sb) {
+        for (int length : lengths) {
+            sb.append('+').append("-".repeat(length + 2));
+        }
+
+        sb.append("+\n");
+    }
+
+    private void appendRow(StringBuilder sb, Text[] row) {
+        assert row.length == lengths.length;
+
+        for (int i = 0; i < row.length; i++) {
+            Text item = row[i];
+
+            sb.append("| ").append(item.toString()).append(" ".repeat(lengths[i] + 1 - item.getCJKAdjustedLength()));
+        }
+
+        sb.append("|\n");
+    }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/VersionProvider.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/VersionProvider.java
new file mode 100644
index 0000000..0bb2570
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/VersionProvider.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import io.micronaut.core.annotation.Introspected;
+import picocli.CommandLine;
+
+@Singleton
+@Introspected
+public class VersionProvider implements CommandLine.IVersionProvider {
+
+    private final CliVersionInfo cliVersionInfo;
+
+    @Inject
+    public VersionProvider(CliVersionInfo cliVersionInfo) {
+        this.cliVersionInfo = cliVersionInfo;
+    }
+
+    @Override public String[] getVersion() throws Exception {
+        return new String[] { "Apache Ignite CLI ver. " + cliVersionInfo.version};
+    }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/SystemPathResolver.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/SystemPathResolver.java
new file mode 100644
index 0000000..eacef87
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/SystemPathResolver.java
@@ -0,0 +1,46 @@
+/*
+ * 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.builtins;
+
+import java.nio.file.Path;
+import javax.inject.Singleton;
+import io.micronaut.core.annotation.Introspected;
+
+public interface SystemPathResolver {
+
+    Path osHomeDirectoryPath();
+
+    Path osCurrentDirPath();
+
+    /**
+     *
+     */
+    @Singleton
+    @Introspected
+    class DefaultPathResolver implements SystemPathResolver {
+
+        @Override public Path osHomeDirectoryPath() {
+            return Path.of(System.getProperty("user.home"));
+        }
+
+        @Override public Path osCurrentDirPath() {
+            return Path.of(System.getProperty("user.dir"));
+        }
+
+    }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/config/ConfigurationClient.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/config/ConfigurationClient.java
new file mode 100644
index 0000000..26a07c0
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/config/ConfigurationClient.java
@@ -0,0 +1,110 @@
+/*
+ * 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.builtins.config;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.typesafe.config.ConfigFactory;
+import com.typesafe.config.ConfigRenderOptions;
+import org.apache.ignite.cli.IgniteCLIException;
+import org.jetbrains.annotations.Nullable;
+
+@Singleton
+public class ConfigurationClient {
+
+    private final String GET_URL = "/management/v1/configuration/";
+    private final String SET_URL = "/management/v1/configuration/";
+
+    private final HttpClient httpClient;
+    private final ObjectMapper mapper;
+
+    @Inject
+    public ConfigurationClient(HttpClient httpClient) {
+        this.httpClient = httpClient;
+        mapper = new ObjectMapper();
+    }
+
+    public String get(String host, int port,
+        @Nullable String rawHoconPath) {
+        var request = HttpRequest
+            .newBuilder()
+            .header("Content-Type", "application/json");
+
+        if (rawHoconPath == null)
+            request.uri(URI.create("http://" + host + ":" + port + GET_URL));
+        else
+            request.uri(URI.create("http://" + host + ":" + port + GET_URL +
+                rawHoconPath));
+
+        try {
+            HttpResponse<String> response =
+                httpClient.send(request.build(),
+                    HttpResponse.BodyHandlers.ofString());
+            if (response.statusCode() == HttpURLConnection.HTTP_OK)
+                return mapper.writerWithDefaultPrettyPrinter()
+                    .writeValueAsString(mapper.readValue(response.body(), JsonNode.class));
+            else
+                throw error("Can't get configuration", response);
+        }
+        catch (IOException | InterruptedException e) {
+            throw new IgniteCLIException("Connection issues while trying to send http request");
+        }
+    }
+
+    public String set(String host, int port, String rawHoconData) {
+        var request = HttpRequest
+            .newBuilder()
+            .POST(HttpRequest.BodyPublishers.ofString(renderJsonFromHocon(rawHoconData)))
+            .header("Content-Type", "application/json")
+            .uri(URI.create("http://" + host + ":" + port + SET_URL))
+            .build();
+
+        try {
+            HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+            if (response.statusCode() == HttpURLConnection.HTTP_OK)
+                return "";
+            else
+                throw error("Fail to set configuration", response);
+        }
+        catch (IOException | InterruptedException e) {
+            throw new IgniteCLIException("Connection issues while trying to send http request");
+        }
+    }
+
+    private IgniteCLIException error(String message, HttpResponse<String> response) throws JsonProcessingException {
+        var errorMessage = mapper.writerWithDefaultPrettyPrinter()
+            .writeValueAsString(mapper.readValue(response.body(), JsonNode.class));
+        return new IgniteCLIException(message + "\n\n" + errorMessage);
+    }
+
+    private static String renderJsonFromHocon(String rawHoconData) {
+        return ConfigFactory.parseString(rawHoconData)
+            .root().render(ConfigRenderOptions.concise());
+    }
+
+
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/config/HttpClientFactory.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/config/HttpClientFactory.java
new file mode 100644
index 0000000..896246b
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/config/HttpClientFactory.java
@@ -0,0 +1,34 @@
+/*
+ * 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.builtins.config;
+
+import java.net.http.HttpClient;
+import javax.inject.Singleton;
+import io.micronaut.context.annotation.Factory;
+
+@Factory
+public class HttpClientFactory {
+
+    @Singleton
+    HttpClient httpClient() {
+        return HttpClient
+            .newBuilder()
+            .version(HttpClient.Version.HTTP_1_1)
+            .build();
+    }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/init/InitIgniteCommand.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/init/InitIgniteCommand.java
new file mode 100644
index 0000000..c3eef48
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/init/InitIgniteCommand.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.builtins.init;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.Optional;
+import java.util.Properties;
+import javax.inject.Inject;
+import org.apache.ignite.cli.CliPathsConfigLoader;
+import org.apache.ignite.cli.CliVersionInfo;
+import org.apache.ignite.cli.IgniteCLIException;
+import org.apache.ignite.cli.IgnitePaths;
+import org.apache.ignite.cli.builtins.SystemPathResolver;
+import org.apache.ignite.cli.builtins.module.ModuleManager;
+import org.jetbrains.annotations.NotNull;
+
+public class InitIgniteCommand {
+
+    private final SystemPathResolver pathResolver;
+    private final CliVersionInfo cliVersionInfo;
+    private final ModuleManager moduleManager;
+    private final CliPathsConfigLoader cliPathsConfigLoader;
+
+    @Inject
+    public InitIgniteCommand(SystemPathResolver pathResolver, CliVersionInfo cliVersionInfo,
+        ModuleManager moduleManager, CliPathsConfigLoader cliPathsConfigLoader) {
+        this.pathResolver = pathResolver;
+        this.cliVersionInfo = cliVersionInfo;
+        this.moduleManager = moduleManager;
+        this.cliPathsConfigLoader = cliPathsConfigLoader;
+    }
+
+    public void init(PrintWriter out) {
+        moduleManager.setOut(out);
+        Optional<IgnitePaths> ignitePathsOpt = cliPathsConfigLoader.loadIgnitePathsConfig();
+        if (ignitePathsOpt.isEmpty()) {
+            out.println("Init ignite directories...");
+            IgnitePaths ignitePaths = initDirectories(out);
+            out.println("Download and install current ignite version...");
+            installIgnite(ignitePaths);
+            out.println("Init default Ignite configs");
+            initDefaultServerConfigs();
+            out.println();
+            out.println("Apache Ignite version " + cliVersionInfo.version + " sucessfully installed");
+        } else {
+            IgnitePaths cfg = ignitePathsOpt.get();
+            out.println("Apache Ignite was initialized earlier\n" +
+                "Configuration file: " + cliPathsConfigLoader.searchConfigPathsFile().get() + "\n" +
+                "Ignite binaries dir: " + cfg.binDir + "\n" +
+                "Ignite work dir: " + cfg.workDir);
+        }
+    }
+
+    private void initDefaultServerConfigs() {
+        Path serverCfgFile = cliPathsConfigLoader.loadIgnitePathsOrThrowError().serverDefaultConfigFile();
+        try {
+            Files.copy(InitIgniteCommand.class.getResourceAsStream("/default-config.xml"), serverCfgFile);
+        }
+        catch (IOException e) {
+            throw new IgniteCLIException("Can't create default config file for server");
+        }
+    }
+
+    private IgnitePaths initDirectories(PrintWriter out) {
+        File cfgFile = initConfigFile();
+        out.println("Configuration file initialized: " + cfgFile);
+        IgnitePaths cfg = cliPathsConfigLoader.loadIgnitePathsOrThrowError();
+        out.println("Ignite binaries dir: " + cfg.binDir);
+        out.println("Ignite work dir: " + cfg.workDir);
+
+        File igniteWork = cfg.workDir.toFile();
+        if (!(igniteWork.exists() || igniteWork.mkdirs()))
+            throw new IgniteCLIException("Can't create working directory: " + cfg.workDir);
+
+        File igniteBin = cfg.libsDir().toFile();
+        if (!(igniteBin.exists() || igniteBin.mkdirs()))
+            throw new IgniteCLIException("Can't create a directory for ignite modules: " + cfg.libsDir());
+
+        File igniteBinCli = cfg.cliLibsDir().toFile();
+        if (!(igniteBinCli.exists() || igniteBinCli.mkdirs()))
+            throw new IgniteCLIException("Can't create a directory for cli modules: " + cfg.cliLibsDir());
+
+        File serverConfig = cfg.serverConfigDir().toFile();
+        if (!(serverConfig.exists() || serverConfig.mkdirs()))
+            throw new IgniteCLIException("Can't create a directory for server configs: " + cfg.serverConfigDir());
+
+        return cfg;
+    }
+
+    private void installIgnite(IgnitePaths ignitePaths) {
+        moduleManager.addModule("_server", ignitePaths, Collections.emptyList());
+    }
+
+    private File initConfigFile() {
+        Path newCfgPath = pathResolver.osHomeDirectoryPath().resolve(".ignitecfg");
+        File newCfgFile = newCfgPath.toFile();
+        try {
+            newCfgFile.createNewFile();
+            Path binDir = pathResolver.osCurrentDirPath().resolve("ignite-bin");
+            Path workDir = pathResolver.osCurrentDirPath().resolve("ignite-work");
+            fillNewConfigFile(newCfgFile, binDir, workDir);
+            return newCfgFile;
+        }
+        catch (IOException e) {
+            throw new IgniteCLIException("Can't create configuration file in current directory: " + newCfgPath);
+        }
+    }
+
+    private void fillNewConfigFile(File f, @NotNull Path binDir, @NotNull Path workDir) {
+        try (FileWriter fileWriter = new FileWriter(f)) {
+            Properties properties = new Properties();
+            properties.setProperty("bin", binDir.toString());
+            properties.setProperty("work", workDir.toString());
+            properties.store(fileWriter, "");
+        }
+        catch (IOException e) {
+            throw new IgniteCLIException("Can't write to ignitecfg file");
+        }
+    }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/MavenArtifactResolver.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/MavenArtifactResolver.java
new file mode 100644
index 0000000..6aa026d
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/MavenArtifactResolver.java
@@ -0,0 +1,256 @@
+/*
+ * 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.builtins.module;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.text.ParseException;
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.apache.ignite.cli.IgniteCLIException;
+import org.apache.ignite.cli.builtins.SystemPathResolver;
+import org.apache.ivy.Ivy;
+import org.apache.ivy.core.IvyContext;
+import org.apache.ivy.core.event.EventManager;
+import org.apache.ivy.core.module.descriptor.DefaultDependencyDescriptor;
+import org.apache.ivy.core.module.descriptor.DefaultModuleDescriptor;
+import org.apache.ivy.core.module.descriptor.ModuleDescriptor;
+import org.apache.ivy.core.module.id.ModuleRevisionId;
+import org.apache.ivy.core.report.ResolveReport;
+import org.apache.ivy.core.resolve.ResolveOptions;
+import org.apache.ivy.core.retrieve.RetrieveOptions;
+import org.apache.ivy.core.retrieve.RetrieveReport;
+import org.apache.ivy.core.settings.IvySettings;
+import org.apache.ivy.plugins.resolver.ChainResolver;
+import org.apache.ivy.plugins.resolver.IBiblioResolver;
+import org.apache.ivy.util.AbstractMessageLogger;
+import org.apache.ivy.util.Message;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ *
+ */
+@Singleton
+public class MavenArtifactResolver {
+
+    private final SystemPathResolver pathResolver;
+    private PrintWriter out;
+
+    @Inject
+    public MavenArtifactResolver(SystemPathResolver pathResolver) {
+        this.pathResolver = pathResolver;
+    }
+
+    public void setOut(PrintWriter out) {
+        this.out = out;
+    }
+
+    public ResolveResult resolve(
+        Path mavenRoot,
+        String grpId,
+        String artifactId,
+        String version,
+        List<URL> customRepositories
+    ) throws IOException {
+        Ivy ivy = ivyInstance(customRepositories); // needed for init right output logger before any operations
+        out.print("Resolve artifact " + grpId + ":" +
+            artifactId + ":" + version);
+        ModuleDescriptor md = rootModuleDescriptor(grpId, artifactId, version);
+
+        // Step 1: you always need to resolve before you can retrieve
+        //
+        ResolveOptions ro = new ResolveOptions();
+        // this seems to have no impact, if you resolve by module descriptor
+        //
+        // (in contrast to resolve by ModuleRevisionId)
+        ro.setTransitive(true);
+        // if set to false, nothing will be downloaded
+        ro.setDownload(true);
+
+        try {
+            // now resolve
+            ResolveReport rr = ivy.resolve(md,ro);
+
+            if (rr.hasError())
+                throw new IgniteCLIException(rr.getAllProblemMessages().toString());
+
+            // Step 2: retrieve
+            ModuleDescriptor m = rr.getModuleDescriptor();
+
+            RetrieveReport retrieveReport = ivy.retrieve(
+                m.getModuleRevisionId(),
+                new RetrieveOptions()
+                    // this is from the envelop module
+                    .setConfs(new String[] {"default"})
+                    .setDestArtifactPattern(mavenRoot.toFile().getAbsolutePath() + "/[artifact](-[classifier]).[revision].[ext]")
+            );
+
+
+            return new ResolveResult(
+                retrieveReport.getRetrievedFiles().stream().map(File::toPath).collect(Collectors.toList())
+            );
+        }
+        catch (ParseException e) {
+            // TOOD
+            throw new IOException(e);
+        }
+        finally {
+            out.println();
+        }
+    }
+
+    private Ivy ivyInstance(List<URL> repositories) {
+        File tmpDir = null;
+        try {
+            tmpDir = Files.createTempDirectory("ignite-installer-cache").toFile();
+        }
+        catch (IOException e) {
+            throw new IgniteCLIException("Can't create temp directory for ivy");
+        }
+        tmpDir.deleteOnExit();
+
+        EventManager eventManager = new EventManager();
+        eventManager.addIvyListener(event -> {
+            out.print(".");
+            out.flush();
+        });
+
+        IvySettings ivySettings = new IvySettings();
+        ivySettings.setDefaultCache(tmpDir);
+        ivySettings.setDefaultCacheArtifactPattern("[artifact](-[classifier]).[revision].[ext]");
+
+        ChainResolver chainResolver = new ChainResolver();
+        chainResolver.setName("chainResolver");
+        chainResolver.setEventManager(eventManager);
+
+        for (URL repoUrl: repositories) {
+            IBiblioResolver br = new IBiblioResolver();
+            br.setEventManager(eventManager);
+            br.setM2compatible(true);
+            br.setUsepoms(true);
+            br.setRoot(repoUrl.toString());
+            br.setName(repoUrl.getPath());
+            chainResolver.add(br);
+        }
+        // use the biblio resolver, if you consider resolving
+        // POM declared dependencies
+        IBiblioResolver br = new IBiblioResolver();
+        br.setEventManager(eventManager);
+        br.setM2compatible(true);
+        br.setUsepoms(true);
+        br.setName("central");
+
+        chainResolver.add(br);
+
+        IBiblioResolver localBr = new IBiblioResolver();
+        localBr.setEventManager(eventManager);
+        localBr.setM2compatible(true);
+        localBr.setUsepoms(true);
+        localBr.setRoot("file://" + pathResolver.osHomeDirectoryPath().resolve(".m2").resolve("repository/"));
+        localBr.setName("local");
+        chainResolver.add(localBr);
+
+        ivySettings.addResolver(chainResolver);
+        ivySettings.setDefaultResolver(chainResolver.getName());
+
+        Ivy ivy = new Ivy();
+        ivy.getLoggerEngine().setDefaultLogger(new IvyLogger());
+        // needed for setting the message logger before logging info from loading settings
+        IvyContext.getContext().setIvy(ivy);
+        ivy.setSettings(ivySettings);
+        ivy.bind();
+
+        return ivy;
+    }
+
+    private ModuleDescriptor rootModuleDescriptor(String grpId, String artifactId, String version) {
+        // 1st create an ivy module (this always(!) has a "default" configuration already)
+        DefaultModuleDescriptor md = DefaultModuleDescriptor.newDefaultInstance(
+            // give it some related name (so it can be cached)
+            ModuleRevisionId.newInstance(
+                "org.apache.ignite",
+                "installer-envelope",
+                "working"
+            )
+        );
+
+        // 2. add dependencies for what we are really looking for
+        ModuleRevisionId ri = ModuleRevisionId.newInstance(
+            grpId,
+            artifactId,
+            version
+        );
+        // don't go transitive here, if you want the single artifact
+        DefaultDependencyDescriptor dd = new DefaultDependencyDescriptor(md, ri, false, true, true);
+
+        // map to master to just get the code jar. See generated ivy module xmls from maven repo
+        // on how configurations are mapped into ivy. Or check
+        // e.g. http://lightguard-jp.blogspot.de/2009/04/ivy-configurations-when-pulling-from.html
+        dd.addDependencyConfiguration("default", "master");
+        dd.addDependencyConfiguration("default", "runtime");
+        dd.addDependencyConfiguration("default", "compile");
+
+        md.addDependency(dd);
+        return md;
+    }
+
+
+    private static class IvyLogger extends AbstractMessageLogger {
+
+        private final Logger logger = LoggerFactory.getLogger(IvyLogger.class);
+
+        @Override protected void doProgress() {
+            // no-op
+        }
+
+        @Override protected void doEndProgress(String msg) {
+            // no-op
+        }
+
+        @Override public void log(String msg, int level) {
+            switch (level) {
+                case Message.MSG_ERR:
+                    logger.error(msg);
+                    break;
+                case Message.MSG_WARN:
+                    logger.warn(msg);
+                    break;
+                case Message.MSG_INFO:
+                    logger.info(msg);
+                    break;
+                case Message.MSG_VERBOSE:
+                    logger.debug(msg);
+                    break;
+                case Message.MSG_DEBUG:
+                    logger.trace(msg);
+                    break;
+            }
+        }
+
+        @Override public void rawlog(String msg, int level) {
+            log(msg, level);
+        }
+    }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/MavenCoordinates.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/MavenCoordinates.java
new file mode 100644
index 0000000..a405855
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/MavenCoordinates.java
@@ -0,0 +1,45 @@
+/*
+ * 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.builtins.module;
+
+import org.apache.ignite.cli.IgniteCLIException;
+
+public class MavenCoordinates {
+    public final String groupId;
+    public final String artifactId;
+    public final String version;
+
+    public MavenCoordinates(String groupId, String artifactId, String version) {
+        this.groupId = groupId;
+        this.artifactId = artifactId;
+        this.version = version;
+    }
+
+    static MavenCoordinates of(String mvnString) {
+        String[] coords = mvnString.split(":");
+
+        if (coords.length == 4)
+            return new MavenCoordinates(coords[1], coords[2], coords[3]);
+        else
+            throw new IgniteCLIException("Incorrect maven coordinates " + mvnString);
+    }
+
+    static MavenCoordinates of(String mvnString, String version) {
+        return of(mvnString + ":" + version);
+    }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/ModuleManager.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/ModuleManager.java
new file mode 100644
index 0000000..5add240
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/ModuleManager.java
@@ -0,0 +1,183 @@
+/*
+ * 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.builtins.module;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.URL;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import com.typesafe.config.ConfigFactory;
+import com.typesafe.config.ConfigObject;
+import com.typesafe.config.ConfigValue;
+import org.apache.ignite.cli.IgnitePaths;
+import org.apache.ignite.cli.IgniteCLIException;
+import org.apache.ignite.cli.CliVersionInfo;
+
+@Singleton
+public class ModuleManager {
+
+    private final MavenArtifactResolver mavenArtifactResolver;
+    private final CliVersionInfo cliVersionInfo;
+    private final ModuleStorage moduleStorage;
+    private final List<StandardModuleDefinition> modules;
+
+    public static final String INTERNAL_MODULE_PREFIX = "_";
+
+    @Inject
+    public ModuleManager(
+        MavenArtifactResolver mavenArtifactResolver, CliVersionInfo cliVersionInfo,
+        ModuleStorage moduleStorage) {
+        modules = readBuiltinModules();
+        this.mavenArtifactResolver = mavenArtifactResolver;
+        this.cliVersionInfo = cliVersionInfo;
+        this.moduleStorage = moduleStorage;
+    }
+
+    public void setOut(PrintWriter out) {
+        mavenArtifactResolver.setOut(out);
+    }
+
+    public void addModule(String name, IgnitePaths ignitePaths, List<URL> repositories) {
+        Path installPath = ignitePaths.libsDir();
+        if (name.startsWith("mvn:")) {
+            MavenCoordinates mavenCoordinates = MavenCoordinates.of(name);
+
+            try {
+                ResolveResult resolveResult = mavenArtifactResolver.resolve(
+                    installPath,
+                    mavenCoordinates.groupId,
+                    mavenCoordinates.artifactId,
+                    mavenCoordinates.version,
+                    repositories
+                );
+                moduleStorage.saveModule(new ModuleStorage.ModuleDefinition(
+                    name,
+                    resolveResult.artifacts(),
+                    new ArrayList<>(),
+                    ModuleStorage.SourceType.Maven,
+                    name
+                ));
+            }
+            catch (IOException e) {
+                throw new IgniteCLIException("Error during resolving maven module " + name, e);
+            }
+
+        }
+        else if (name.startsWith("file://"))
+            throw new RuntimeException("File urls is not implemented yet");
+        else if (isStandardModuleName(name)) {
+            StandardModuleDefinition moduleDescription = readBuiltinModules()
+                .stream()
+                .filter(m -> m.name.equals(name))
+                .findFirst().get();
+            List<ResolveResult> libsResolveResults = new ArrayList<>();
+            for (String artifact: moduleDescription.artifacts) {
+                MavenCoordinates mavenCoordinates = MavenCoordinates.of(artifact, cliVersionInfo.version);
+                try {
+                    libsResolveResults.add(mavenArtifactResolver.resolve(
+                        ignitePaths.libsDir(),
+                        mavenCoordinates.groupId,
+                        mavenCoordinates.artifactId,
+                        mavenCoordinates.version,
+                        repositories
+                    ));
+                }
+                catch (IOException e) {
+                    throw new IgniteCLIException("Error during resolving standard module " + name, e);
+                }
+            }
+
+            List<ResolveResult> cliResolvResults = new ArrayList<>();
+            for (String artifact: moduleDescription.cliArtifacts) {
+                MavenCoordinates mavenCoordinates = MavenCoordinates.of(artifact, cliVersionInfo.version);
+                try {
+                    cliResolvResults.add(mavenArtifactResolver.resolve(
+                        ignitePaths.cliLibsDir(),
+                        mavenCoordinates.groupId,
+                        mavenCoordinates.artifactId,
+                        mavenCoordinates.version,
+                        repositories
+                    ));
+                }
+                catch (IOException e) {
+                    throw new IgniteCLIException("Error during resolving module " + name, e);
+                }
+            }
+
+            try {
+                moduleStorage.saveModule(new ModuleStorage.ModuleDefinition(
+                    name,
+                    libsResolveResults.stream().flatMap(r -> r.artifacts().stream()).collect(Collectors.toList()),
+                    cliResolvResults.stream().flatMap(r -> r.artifacts().stream()).collect(Collectors.toList()),
+                    ModuleStorage.SourceType.Maven,
+                    name
+                ));
+            }
+            catch (IOException e) {
+                throw new IgniteCLIException("Error during saving the installed module info");
+            }
+
+        }
+        else {
+            throw new IgniteCLIException(
+                "Module coordinates for non-standard modules must be started with mvn:|file://");
+        }
+    }
+
+    public boolean removeModule(String name) {
+        try {
+            return moduleStorage.removeModule(name);
+        }
+        catch (IOException e) {
+            throw new IgniteCLIException(
+                "Can't remove module " + name, e);
+        }
+    }
+
+    public List<StandardModuleDefinition> builtinModules() {
+        return modules;
+    }
+
+    private boolean isStandardModuleName(String name) {
+        return readBuiltinModules().stream().anyMatch(m -> m.name.equals(name));
+    }
+
+
+
+    private static List<StandardModuleDefinition> readBuiltinModules() {
+        com.typesafe.config.ConfigObject config = ConfigFactory.load("builtin_modules.conf").getObject("modules");
+        List<StandardModuleDefinition> modules = new ArrayList<>();
+        for (Map.Entry<String, ConfigValue> entry: config.entrySet()) {
+            ConfigObject configObject = (ConfigObject) entry.getValue();
+            modules.add(new StandardModuleDefinition(
+                entry.getKey(),
+                configObject.toConfig().getString("description"),
+                configObject.toConfig().getStringList("artifacts"),
+                configObject.toConfig().getStringList("cli-artifacts")
+            ));
+        }
+        return modules;
+    }
+
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/ModuleStorage.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/ModuleStorage.java
new file mode 100644
index 0000000..1219327
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/ModuleStorage.java
@@ -0,0 +1,120 @@
+/*
+ * 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.builtins.module;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonGetter;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.ignite.cli.CliPathsConfigLoader;
+
+@Singleton
+public class ModuleStorage {
+
+    private final CliPathsConfigLoader cliPathsConfigLoader;
+
+    @Inject
+    public ModuleStorage(CliPathsConfigLoader cliPathsConfigLoader) {
+        this.cliPathsConfigLoader = cliPathsConfigLoader;
+    }
+
+    private Path moduleFile() {
+        return cliPathsConfigLoader.loadIgnitePathsOrThrowError().installedModulesFile();
+    }
+
+    //TODO: write-to-tmp->move approach should be used to prevent file corruption on accidental exit
+    public void saveModule(ModuleDefinition moduleDefinition) throws IOException {
+        ModuleDefinitionsRegistry moduleDefinitionsRegistry = listInstalled();
+        moduleDefinitionsRegistry.modules.add(moduleDefinition);
+        ObjectMapper objectMapper = new ObjectMapper();
+        objectMapper.writeValue(moduleFile().toFile(), moduleDefinitionsRegistry);
+    }
+
+    //TODO: write-to-tmp->move approach should be used to prevent file corruption on accidental exit
+    public boolean removeModule(String name) throws IOException {
+        ModuleDefinitionsRegistry moduleDefinitionsRegistry = listInstalled();
+        boolean removed = moduleDefinitionsRegistry.modules.removeIf(m -> m.name.equals(name));
+        ObjectMapper objectMapper = new ObjectMapper();
+        objectMapper.writeValue(moduleFile().toFile(), moduleDefinitionsRegistry);
+        return removed;
+    }
+
+    public ModuleDefinitionsRegistry listInstalled() throws IOException {
+        if (!moduleFile().toFile().exists())
+            return new ModuleDefinitionsRegistry(new ArrayList<>());
+        else {
+            ObjectMapper objectMapper = new ObjectMapper();
+            return objectMapper.readValue(
+                moduleFile().toFile(),
+                ModuleDefinitionsRegistry.class);
+        }
+    }
+
+
+    public static class ModuleDefinitionsRegistry {
+        public final List<ModuleDefinition> modules;
+
+        @JsonCreator
+        public ModuleDefinitionsRegistry(
+            @JsonProperty("modules") List<ModuleDefinition> modules) {
+            this.modules = modules;
+        }
+    }
+    public static class ModuleDefinition {
+        public final String name;
+        public final List<Path> artifacts;
+        public final List<Path> cliArtifacts;
+        public final SourceType type;
+        public final String source;
+
+        @JsonCreator
+        public ModuleDefinition(
+            @JsonProperty("name") String name, @JsonProperty("artifacts") List<Path> artifacts,
+            @JsonProperty("cliArtifacts") List<Path> cliArtifacts,
+            @JsonProperty("type") SourceType type, @JsonProperty("source") String source) {
+            this.name = name;
+            this.artifacts = artifacts;
+            this.cliArtifacts = cliArtifacts;
+            this.type = type;
+            this.source = source;
+        }
+
+        @JsonGetter("artifacts")
+        public List<String> getArtifacts() {
+            return artifacts.stream().map(a -> a.toAbsolutePath().toString()).collect(Collectors.toList());
+        }
+
+        @JsonGetter("cliArtifacts")
+        public List<String> getCliArtifacts() {
+            return cliArtifacts.stream().map(a -> a.toAbsolutePath().toString()).collect(Collectors.toList());
+        }
+    }
+
+    public enum SourceType {
+        File,
+        Maven,
+        Standard
+    }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/ResolveResult.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/ResolveResult.java
new file mode 100644
index 0000000..d058211
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/ResolveResult.java
@@ -0,0 +1,33 @@
+/*
+ * 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.builtins.module;
+
+import java.nio.file.Path;
+import java.util.List;
+
+public class ResolveResult {
+    private List<Path> artifacts;
+
+    public ResolveResult(List<Path> artifacts) {
+        this.artifacts = artifacts;
+    }
+
+    public List<Path> artifacts() {
+        return artifacts;
+    }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/StandardModuleDefinition.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/StandardModuleDefinition.java
new file mode 100644
index 0000000..0fa3eb5
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/module/StandardModuleDefinition.java
@@ -0,0 +1,38 @@
+/*
+ * 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.builtins.module;
+
+import java.util.List;
+
+public class StandardModuleDefinition {
+    public final String name;
+    public final String description;
+    public final List<String> artifacts;
+    public final List<String> cliArtifacts;
+
+    public StandardModuleDefinition(String name, String description, List<String> artifacts, List<String> cliArtifacts) {
+        this.name = name;
+        this.description = description;
+        this.artifacts = artifacts;
+        this.cliArtifacts = cliArtifacts;
+    }
+
+    public String toString() {
+        return this.name + ":\t" + this.description;
+    }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/node/NodeManager.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/node/NodeManager.java
new file mode 100644
index 0000000..13afe4c
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/builtins/node/NodeManager.java
@@ -0,0 +1,231 @@
+/*
+ * 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.builtins.node;
+
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchService;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.apache.ignite.cli.builtins.module.ModuleStorage;
+import org.apache.ignite.cli.IgniteCLIException;
+
+@Singleton
+public class NodeManager {
+
+    private static final String MAIN_CLASS = "org.apache.ignite.app.IgniteRunner";
+    private static final Duration NODE_START_TIMEOUT = Duration.ofSeconds(30);
+    private static final Duration LOG_FILE_POLL_INTERVAL = Duration.ofMillis(50);
+
+    private final ModuleStorage moduleStorage;
+
+    @Inject
+    public NodeManager(ModuleStorage moduleStorage) {
+        this.moduleStorage = moduleStorage;
+    }
+
+    public RunningNode start(String consistentId,
+        Path workDir, Path pidsDir, Path serverConfig) {
+
+        if (getRunningNodes(workDir, pidsDir).stream().anyMatch(n -> n.consistentId.equals(consistentId)))
+            throw new IgniteCLIException("Node with consistentId " + consistentId + " is already exist");
+        try {
+            Path logFile = logFile(workDir, consistentId);
+            if (Files.exists(logFile))
+                Files.delete(logFile);
+
+            Files.createFile(logFile);
+
+            var commandArgs = Arrays.asList(
+                "java", "-cp", classpath(), MAIN_CLASS,
+                "--config", serverConfig.toAbsolutePath().toString()
+            );
+
+            ProcessBuilder pb = new ProcessBuilder(
+                commandArgs
+            )
+                .redirectError(logFile.toFile())
+                .redirectOutput(logFile.toFile());
+            Process p = pb.start();
+            try {
+                if (!waitForStart("Ignite application started successfully", logFile, NODE_START_TIMEOUT)) {
+                    p.destroyForcibly();
+                    throw new IgniteCLIException("Node wasn't started during timeout period "
+                        + NODE_START_TIMEOUT.toMillis() + "ms");
+                }
+            }
+            catch (InterruptedException|IOException e) {
+                throw new IgniteCLIException("Waiting for node start was failed", e);
+            }
+            createPidFile(consistentId, p.pid(), pidsDir);
+            return new RunningNode(p.pid(), consistentId, logFile);
+        }
+        catch (IOException e) {
+            throw new IgniteCLIException("Can't load classpath", e);
+        }
+    }
+
+    // TODO: We need more robust way of checking if node successfully run
+    private static boolean waitForStart(String started, Path file, Duration timeout) throws IOException, InterruptedException {
+       try(WatchService watchService = FileSystems.getDefault().newWatchService()) {
+           file.getParent().register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
+           var beginTime = System.currentTimeMillis();
+           while ((System.currentTimeMillis() - beginTime) < timeout.toMillis()) {
+               var key = watchService.poll(LOG_FILE_POLL_INTERVAL.toMillis(), TimeUnit.MILLISECONDS);
+               if (key != null) {
+                   for (WatchEvent<?> event : key.pollEvents()) {
+                       if (event.kind().equals(StandardWatchEventKinds.ENTRY_MODIFY) &&
+                           (((WatchEvent<Path>)event).context().getFileName()).equals(file.getFileName())) {
+                           var content = Files.readString(file);
+                           if (content.contains(started))
+                               return true;
+                           else if (content.contains("Exception"))
+                               throw new IgniteCLIException("Can't start the node. Read logs for details: " + file);
+                       }
+                   }
+                   key.reset();
+               }
+           }
+           return false;
+       }
+    }
+
+    public String classpath() throws IOException {
+        return moduleStorage.listInstalled().modules.stream()
+            .flatMap(m -> m.artifacts.stream())
+            .map(m -> m.toAbsolutePath().toString())
+            .collect(Collectors.joining(":"));
+    }
+
+    public void createPidFile(String consistentId, long pid,Path pidsDir) {
+        if (!Files.exists(pidsDir)) {
+            if (!pidsDir.toFile().mkdirs())
+                throw new IgniteCLIException("Can't create directory for storing the process pids: " + pidsDir);
+        }
+
+        Path pidPath = pidsDir.resolve(consistentId + "_" + System.currentTimeMillis() + ".pid");
+
+        try (FileWriter fileWriter = new FileWriter(pidPath.toFile())) {
+            fileWriter.write(String.valueOf(pid));
+        }
+        catch (IOException e) {
+            throw new IgniteCLIException("Can't write pid file " + pidPath);
+        }
+    }
+
+    public List<RunningNode> getRunningNodes(Path worksDir, Path pidsDir) {
+        if (Files.exists(pidsDir)) {
+            try (Stream<Path> files = Files.find(pidsDir, 1, (f, attrs) ->  f.getFileName().toString().endsWith(".pid"))) {
+                    return files
+                        .map(f -> {
+                            long pid = 0;
+                            try {
+                                pid = Long.parseLong(Files.readAllLines(f).get(0));
+                                if (!ProcessHandle.of(pid).map(ProcessHandle::isAlive).orElse(false))
+                                    return Optional.<RunningNode>empty();
+                            }
+                            catch (IOException e) {
+                                throw new IgniteCLIException("Can't parse pid file " + f);
+                            }
+                            String filename = f.getFileName().toString();
+                            if (filename.lastIndexOf("_") == -1)
+                                return Optional.<RunningNode>empty();
+                            else {
+                                String consistentId = filename.substring(0, filename.lastIndexOf("_"));
+                                return Optional.of(new RunningNode(pid, consistentId, logFile(worksDir, consistentId)));
+                            }
+
+                        })
+                        .filter(Optional::isPresent)
+                        .map(Optional::get).collect(Collectors.toList());
+            }
+            catch (IOException e) {
+                throw new IgniteCLIException("Can't find directory with pid files for running nodes " + pidsDir);
+            }
+        }
+        else
+            return Collections.emptyList();
+    }
+
+    public boolean stopWait(String consistentId, Path pidsDir) {
+        if (Files.exists(pidsDir)) {
+            try {
+                List<Path> files = Files.find(pidsDir, 1,
+                    (f, attrs) ->
+                        f.getFileName().toString().startsWith(consistentId + "_")).collect(Collectors.toList());
+                if (files.size() > 0) {
+                    return files.stream().map(f -> {
+                        try {
+                            long pid = Long.parseLong(Files.readAllLines(f).get(0));
+                            boolean result = stopWait(pid);
+                            Files.delete(f);
+                            return result;
+                        }
+                        catch (IOException e) {
+                            throw new IgniteCLIException("Can't read pid file " + f);
+                        }
+                    }).reduce((a, b) -> a && b).orElse(false);
+                }
+                else
+                    throw new IgniteCLIException("Can't find node with consistent id " + consistentId);
+            }
+            catch (IOException e) {
+                throw new IgniteCLIException("Can't open directory with pid files " + pidsDir);
+            }
+        }
+        else
+            return false;
+    }
+
+    private boolean stopWait(long pid) {
+        return ProcessHandle
+            .of(pid)
+            .map(ProcessHandle::destroy)
+            .orElse(false);
+    }
+    
+    private static Path logFile(Path workDir, String consistentId) {
+          return workDir.resolve(consistentId + ".log");
+    }
+
+    public static class RunningNode {
+
+        public final long pid;
+        public final String consistentId;
+        public final Path logFile;
+
+        public RunningNode(long pid, String consistentId, Path logFile) {
+            this.pid = pid;
+            this.consistentId = consistentId;
+            this.logFile = logFile;
+        }
+    }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/AbstractCommandSpec.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/AbstractCommandSpec.java
new file mode 100644
index 0000000..4cc905d
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/AbstractCommandSpec.java
@@ -0,0 +1,28 @@
+/*
+ * 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.spec;
+
+import org.apache.ignite.cli.VersionProvider;
+import picocli.CommandLine;
+import picocli.CommandLine.Model.CommandSpec;
+
+@CommandLine.Command(versionProvider = VersionProvider.class)
+public abstract class AbstractCommandSpec implements Runnable {
+    @CommandLine.Spec
+    protected CommandSpec spec;
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/ConfigCommandSpec.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/ConfigCommandSpec.java
new file mode 100644
index 0000000..aa08afc
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/ConfigCommandSpec.java
@@ -0,0 +1,107 @@
+/*
+ * 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.spec;
+
+import javax.inject.Inject;
+import io.micronaut.context.ApplicationContext;
+import org.apache.ignite.cli.IgniteCLIException;
+import org.apache.ignite.cli.builtins.config.ConfigurationClient;
+import picocli.CommandLine;
+
+@CommandLine.Command(
+    name = "config",
+    description = "Inspect and update Ignite cluster configuration.",
+    subcommands = {
+        ConfigCommandSpec.GetConfigCommandSpec.class,
+        ConfigCommandSpec.SetConfigCommandSpec.class
+    }
+)
+public class ConfigCommandSpec extends AbstractCommandSpec {
+
+    @Override public void run() {
+        spec.commandLine().usage(spec.commandLine().getOut());
+    }
+
+    @CommandLine.Command(name = "get", description = "Get current Ignite cluster configuration values.")
+    public static class GetConfigCommandSpec extends AbstractCommandSpec {
+
+        @Inject private ConfigurationClient configurationClient;
+
+        @CommandLine.Mixin CfgHostnameOptions cfgHostnameOptions;
+
+        @CommandLine.Option(names = {"--subtree"},
+            description = "any text representation of hocon for querying considered subtree of config " +
+                "(example: local.baseline)")
+        private String subtree;
+
+        @Override public void run() {
+            spec.commandLine().getOut().println(
+                configurationClient.get(cfgHostnameOptions.host(), cfgHostnameOptions.port(), subtree));
+        }
+    }
+
+    @CommandLine.Command(
+        name = "set",
+        description = "Update Ignite cluster configuration values."
+    )
+    public static class SetConfigCommandSpec extends AbstractCommandSpec {
+
+        @Inject private ConfigurationClient configurationClient;
+
+        @CommandLine.Parameters(paramLabel = "hocon-string", description = "any text representation of hocon config")
+        private String config;
+
+        @CommandLine.Mixin CfgHostnameOptions cfgHostnameOptions;
+
+        @Override public void run() {
+            spec.commandLine().getOut().println(
+                configurationClient
+                    .set(cfgHostnameOptions.host(), cfgHostnameOptions.port(), config));
+        }
+    }
+
+    private static class CfgHostnameOptions {
+
+        @CommandLine.Option(names = "--node-endpoint", required = true,
+            description = "host:port of node for configuration")
+        String cfgHostPort;
+
+
+        int port() {
+            var hostPort = parse();
+
+            try {
+                return Integer.parseInt(hostPort[1]);
+            } catch (NumberFormatException ex) {
+                throw new IgniteCLIException("Can't parse port from " + hostPort[1] + " value");
+            }
+        }
+
+        String host() {
+            return parse()[0];
+        }
+
+        private String[] parse() {
+            var hostPort = cfgHostPort.split(":");
+            if (hostPort.length != 2)
+                throw new IgniteCLIException("Incorrect host:port pair provided " +
+                    "(example of valid value 'localhost:8080')");
+           return hostPort;
+        }
+    }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/IgniteCliSpec.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/IgniteCliSpec.java
new file mode 100644
index 0000000..ba25e51
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/IgniteCliSpec.java
@@ -0,0 +1,128 @@
+/*
+ * 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.spec;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.ServiceLoader;
+import java.util.stream.Collectors;
+import io.micronaut.context.ApplicationContext;
+import org.apache.ignite.cli.CliPathsConfigLoader;
+import org.apache.ignite.cli.CommandFactory;
+import org.apache.ignite.cli.ErrorHandler;
+import org.apache.ignite.cli.HelpFactoryImpl;
+import org.apache.ignite.cli.IgniteCLIException;
+import org.apache.ignite.cli.InteractiveWrapper;
+import org.apache.ignite.cli.builtins.module.ModuleStorage;
+import org.apache.ignite.cli.common.IgniteCommand;
+import picocli.CommandLine;
+
+import static org.apache.ignite.cli.HelpFactoryImpl.SECTION_KEY_SYNOPSIS_EXTENSION;
+
+/**
+ *
+ */
+@CommandLine.Command(
+    name = "ignite",
+    description = "Entry point.",
+    subcommands = {
+        InitIgniteCommandSpec.class,
+        ModuleCommandSpec.class,
+        NodeCommandSpec.class,
+        ConfigCommandSpec.class,
+    }
+)
+public class IgniteCliSpec extends AbstractCommandSpec {
+
+    @CommandLine.Option(names = "-i", hidden = true, required = false)
+    boolean interactive;
+
+    @Override public void run() {
+        spec.usageMessage().sectionMap().put(SECTION_KEY_SYNOPSIS_EXTENSION,
+            help -> " Or type " + help.colorScheme().commandText(spec.qualifiedName()) +
+                    ' ' + help.colorScheme().parameterText("-i") + " to enter interactive mode.\n\n");
+
+        CommandLine cli = spec.commandLine();
+
+        if (interactive)
+            new InteractiveWrapper().run(cli);
+        else
+            cli.usage(cli.getOut());
+    }
+
+    public static CommandLine initCli(ApplicationContext applicationContext) {
+        CommandLine.IFactory factory = new CommandFactory(applicationContext);
+        ErrorHandler errorHandler = applicationContext.createBean(ErrorHandler.class);
+        CommandLine cli = new CommandLine(IgniteCliSpec.class, factory)
+            .setExecutionExceptionHandler(errorHandler)
+            .setParameterExceptionHandler(errorHandler);
+
+        cli.setHelpFactory(new HelpFactoryImpl());
+
+        cli.setColorScheme(new CommandLine.Help.ColorScheme.Builder()
+            .commands(CommandLine.Help.Ansi.Style.fg_green)
+            .options(CommandLine.Help.Ansi.Style.fg_yellow)
+            .parameters(CommandLine.Help.Ansi.Style.fg_cyan)
+            .errors(CommandLine.Help.Ansi.Style.fg_red, CommandLine.Help.Ansi.Style.bold)
+            .build());
+
+        applicationContext.createBean(CliPathsConfigLoader.class)
+            .loadIgnitePathsConfig()
+            .ifPresent(ignitePaths ->
+                {
+                    try {
+                        loadSubcommands(
+                            cli,
+                            applicationContext.createBean(ModuleStorage.class)
+                                .listInstalled()
+                                .modules
+                                .stream()
+                                .flatMap(m -> m.cliArtifacts.stream())
+                                .collect(Collectors.toList()));
+                    }
+                    catch (IOException e) {
+                        throw new IgniteCLIException("Can't load cli modules due to IO error");
+                    }
+                }
+            );
+        return cli;
+    }
+
+    public static void loadSubcommands(CommandLine commandLine, List<Path> cliLibs) {
+        URL[] urls = cliLibs.stream()
+            .map(p -> {
+                try {
+                    return p.toUri().toURL();
+                }
+                catch (MalformedURLException e) {
+                    throw new IgniteCLIException("Can't convert cli module path to URL for loading by classloader");
+                }
+            }).toArray(URL[]::new);
+        ClassLoader classLoader = new URLClassLoader(urls,
+            IgniteCliSpec.class.getClassLoader());
+        ServiceLoader<IgniteCommand> loader = ServiceLoader.load(IgniteCommand.class, classLoader);
+        loader.reload();
+        for (IgniteCommand igniteCommand: loader) {
+            commandLine.addSubcommand(igniteCommand);
+        }
+    }
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/InitIgniteCommandSpec.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/InitIgniteCommandSpec.java
new file mode 100644
index 0000000..af1aefd
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/InitIgniteCommandSpec.java
@@ -0,0 +1,36 @@
+/*
+ * 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.spec;
+
+import javax.inject.Inject;
+import io.micronaut.context.ApplicationContext;
+import org.apache.ignite.cli.common.IgniteCommand;
+import org.apache.ignite.cli.builtins.init.InitIgniteCommand;
+import picocli.CommandLine;
+
+@CommandLine.Command(name = "init", description = "Install Ignite core modules locally.")
+public class InitIgniteCommandSpec extends AbstractCommandSpec implements IgniteCommand {
+
+    @Inject
+    InitIgniteCommand command;
+
+    @Override public void run() {
+        command.init(spec.commandLine().getOut());
+    }
+
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/ModuleCommandSpec.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/ModuleCommandSpec.java
new file mode 100644
index 0000000..ab114ff
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/ModuleCommandSpec.java
@@ -0,0 +1,108 @@
+/*
+ * 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.spec;
+
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.stream.Collectors;
+import javax.inject.Inject;
+import com.github.freva.asciitable.AsciiTable;
+import com.github.freva.asciitable.Column;
+import com.github.freva.asciitable.HorizontalAlign;
+import io.micronaut.context.ApplicationContext;
+import org.apache.ignite.cli.CliPathsConfigLoader;
+import org.apache.ignite.cli.builtins.module.ModuleManager;
+import org.apache.ignite.cli.common.IgniteCommand;
+import picocli.CommandLine;
+
+@CommandLine.Command(
+    name = "module",
+    description = "Manage optional Ignite modules and external artifacts.",
+    subcommands = {
+        ModuleCommandSpec.ListModuleCommandSpec.class,
+        ModuleCommandSpec.AddModuleCommandSpec.class,
+        ModuleCommandSpec.RemoveModuleCommandSpec.class
+    }
+)
+public class ModuleCommandSpec extends AbstractCommandSpec implements IgniteCommand {
+
+    @Override public void run() {
+        spec.commandLine().usage(spec.commandLine().getOut());
+    }
+
+    @CommandLine.Command(name = "add", description = "Add an optional Ignite module or an external artifact.")
+    public static class AddModuleCommandSpec extends AbstractCommandSpec {
+
+        @Inject private ModuleManager moduleManager;
+
+        @Inject
+        private CliPathsConfigLoader cliPathsConfigLoader;
+
+        @CommandLine.Option(names = "--repo",
+            description = "Url to custom maven repo")
+        public URL[] urls;
+
+        @CommandLine.Parameters(paramLabel = "module",
+            description = "can be a 'builtin module name (see module list)'|'mvn:groupId:artifactId:version'")
+        public String moduleName;
+
+        @Override public void run() {
+            var ignitePaths = cliPathsConfigLoader.loadIgnitePathsOrThrowError();
+            moduleManager.setOut(spec.commandLine().getOut());
+            moduleManager.addModule(moduleName,
+                ignitePaths,
+                (urls == null)? Collections.emptyList() : Arrays.asList(urls));
+        }
+    }
+
+    @CommandLine.Command(name = "remove", description = "Add an optional Ignite module or an external artifact.")
+    public static class RemoveModuleCommandSpec extends AbstractCommandSpec {
+
+        @Inject private ModuleManager moduleManager;
+
+        @CommandLine.Parameters(paramLabel = "module",
+            description = "can be a 'builtin module name (see module list)'|'mvn:groupId:artifactId:version'")
+        public String moduleName;
+
+        @Override public void run() {
+            if (moduleManager.removeModule(moduleName))
+                spec.commandLine().getOut().println("Module " + moduleName + " was removed successfully");
+            else
+                spec.commandLine().getOut().println("Module " + moduleName + " is not found");
+        }
+    }
+
+    @CommandLine.Command(name = "list", description = "Show the list of available optional Ignite modules.")
+    public static class ListModuleCommandSpec extends AbstractCommandSpec {
+
+        @Inject private ModuleManager moduleManager;
+
+        @Override public void run() {
+            var builtinModules = moduleManager.builtinModules()
+                .stream()
+                .filter(m -> !m.name.startsWith(ModuleManager.INTERNAL_MODULE_PREFIX));
+            String table = AsciiTable.getTable(builtinModules.collect(Collectors.toList()), Arrays.asList(
+                new Column().header("Name").dataAlign(HorizontalAlign.LEFT).with(m -> m.name),
+                new Column().header("Description").dataAlign(HorizontalAlign.LEFT).with(m -> m.description)
+            ));
+            spec.commandLine().getOut().println(table);
+        }
+    }
+
+}
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/NodeCommandSpec.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/NodeCommandSpec.java
new file mode 100644
index 0000000..cc13eac
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/NodeCommandSpec.java
@@ -0,0 +1,139 @@
+/*
+ * 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.spec;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.List;
+import javax.inject.Inject;
+import io.micronaut.context.ApplicationContext;
+import org.apache.ignite.cli.CliPathsConfigLoader;
+import org.apache.ignite.cli.IgniteCLIException;
+import org.apache.ignite.cli.IgnitePaths;
+import org.apache.ignite.cli.Table;
+import org.apache.ignite.cli.builtins.node.NodeManager;
+import picocli.CommandLine;
+
+@CommandLine.Command(
+    name = "node",
+    description = "Start, stop and manage locally running Ignite nodes.",
+    subcommands = {
+        NodeCommandSpec.StartNodeCommandSpec.class,
+        NodeCommandSpec.StopNodeCommandSpec.class,
+        NodeCommandSpec.NodesClasspathCommandSpec.class,
+        NodeCommandSpec.ListNodesCommandSpec.class
+    }
+)
+public class NodeCommandSpec extends AbstractCommandSpec {
+
+    @Override public void run() {
+        spec.commandLine().usage(spec.commandLine().getOut());
+    }
+
+    @CommandLine.Command(name = "start", description = "Start an Ignite node locally.")
+    public static class StartNodeCommandSpec extends AbstractCommandSpec {
+
+        @Inject private CliPathsConfigLoader cliPathsConfigLoader;
+
+        @Inject private NodeManager nodeManager;
+
+        @CommandLine.Parameters(paramLabel = "consistent-id", description = "ConsistentId for new node")
+        public String consistentId;
+
+        @CommandLine.Option(names = {"--config"}, required = true,
+            description = "path to configuration file")
+        public Path configPath;
+
+        @Override public void run() {
+            IgnitePaths ignitePaths = cliPathsConfigLoader.loadIgnitePathsOrThrowError();
+
+            NodeManager.RunningNode node = nodeManager.start(consistentId, ignitePaths.workDir,
+                ignitePaths.cliPidsDir(),
+                configPath);
+
+            spec.commandLine().getOut().println("Started ignite node.\nPID: " + node.pid +
+                "\nConsistent Id: " + node.consistentId + "\nLog file: " + node.logFile);
+        }
+    }
+
+    @CommandLine.Command(name = "stop", description = "Stop a locally running Ignite node.")
+    public static class StopNodeCommandSpec extends AbstractCommandSpec {
+
+        @Inject private NodeManager nodeManager;
+        @Inject private CliPathsConfigLoader cliPathsConfigLoader;
+
+        @CommandLine.Parameters(arity = "1..*", paramLabel = "consistent-ids",
+            description = "consistent ids of nodes to start")
+        public List<String> consistentIds;
+
+        @Override public void run() {
+            IgnitePaths ignitePaths = cliPathsConfigLoader.loadIgnitePathsOrThrowError();
+
+            consistentIds.forEach(p -> {
+                if (nodeManager.stopWait(p, ignitePaths.cliPidsDir()))
+                    spec.commandLine().getOut().println("Node with consistent id " + p + " was stopped");
+                else
+                    spec.commandLine().getOut().println("Stop of node " + p + " was failed");
+            });
+        }
+    }
+
+    @CommandLine.Command(name = "list", description = "Show the list of currently running local Ignite nodes.")
+    public static class ListNodesCommandSpec extends AbstractCommandSpec {
+
+        @Inject private NodeManager nodeManager;
+        @Inject private CliPathsConfigLoader cliPathsConfigLoader;
+
+        @Override public void run() {
+            IgnitePaths paths = cliPathsConfigLoader.loadIgnitePathsOrThrowError();
+
+            List<NodeManager.RunningNode> nodes = nodeManager
+                .getRunningNodes(paths.workDir, paths.cliPidsDir());
+
+            if (nodes.isEmpty())
+                spec.commandLine().getOut().println("No running nodes");
+            else {
+                Table table = new Table(0, spec.commandLine().getColorScheme());
+
+                table.addRow("@|bold PID|@", "@|bold Consistent ID|@", "@|bold Log|@");
+
+                for (NodeManager.RunningNode node : nodes) {
+                    table.addRow(node.pid, node.consistentId, node.logFile);
+                }
+
+                spec.commandLine().getOut().println(table);
+            }
+        }
+    }
+
+    @CommandLine.Command(name = "classpath", description = "Show the current classpath used by the Ignite nodes.")
+    public static class NodesClasspathCommandSpec extends AbstractCommandSpec {
+
+        @Inject private NodeManager nodeManager;
+
+        @Override public void run() {
+            try {
+                spec.commandLine().getOut().println(nodeManager.classpath());
+            }
+            catch (IOException e) {
+                throw new IgniteCLIException("Can't get current classpath", e);
+            }
+        }
+    }
+
+}
diff --git a/modules/cli-demo/cli/src/main/resources/builtin_modules.conf b/modules/cli-demo/cli/src/main/resources/builtin_modules.conf
new file mode 100644
index 0000000..c7f4e80
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/resources/builtin_modules.conf
@@ -0,0 +1,13 @@
+modules : {
+    _server: {
+        description: "Collection of base modules, which needed for start ignite server",
+        artifacts: ["mvn:org.apache.ignite:ignite-runner"],
+        cli-artifacts: []
+    },
+
+    ignite-demo-cli {
+        description: "Demo Ignite module with additional cli command 'snapshot'",
+        artifacts: ["mvn:org.apache.ignite:ignite-demo-module"],
+        cli-artifacts: ["mvn:org.apache.ignite:ignite-demo-module-cli"]
+    }
+}
\ No newline at end of file
diff --git a/modules/cli-demo/cli/src/main/resources/default-config.xml b/modules/cli-demo/cli/src/main/resources/default-config.xml
new file mode 100644
index 0000000..5da95ba
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/resources/default-config.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  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.
+-->
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="
+       http://www.springframework.org/schema/beans
+       http://www.springframework.org/schema/beans/spring-beans.xsd">
+    <!--
+        Alter configuration below as needed.
+    -->
+    <bean id="grid.cfg" class="org.apache.ignite.configuration.IgniteConfiguration"/>
+</beans>
diff --git a/modules/cli-demo/cli/src/main/resources/logback.xml b/modules/cli-demo/cli/src/main/resources/logback.xml
new file mode 100644
index 0000000..6a1caa1
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/resources/logback.xml
@@ -0,0 +1,14 @@
+<configuration>
+
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <!-- encoders are assigned the type
+             ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
+        <encoder>
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <root level="error">
+        <appender-ref ref="STDOUT" />
+    </root>
+</configuration>
\ No newline at end of file
diff --git a/modules/cli-demo/cli/src/main/resources/version.properties b/modules/cli-demo/cli/src/main/resources/version.properties
new file mode 100644
index 0000000..0d488d3
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/resources/version.properties
@@ -0,0 +1,18 @@
+#
+# 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.
+#
+
+version=${project.version}
diff --git a/modules/cli-demo/cli/src/test/java/org/apache/ignite/cli/IgniteCliInterfaceTest.java b/modules/cli-demo/cli/src/test/java/org/apache/ignite/cli/IgniteCliInterfaceTest.java
new file mode 100644
index 0000000..4a8ce62
--- /dev/null
+++ b/modules/cli-demo/cli/src/test/java/org/apache/ignite/cli/IgniteCliInterfaceTest.java
@@ -0,0 +1,425 @@
+package org.apache.ignite.cli;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.http.HttpClient;
+import java.net.http.HttpResponse;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import io.micronaut.context.ApplicationContext;
+import io.micronaut.context.env.Environment;
+import org.apache.ignite.cli.builtins.init.InitIgniteCommand;
+import org.apache.ignite.cli.builtins.module.ModuleManager;
+import org.apache.ignite.cli.builtins.module.StandardModuleDefinition;
+import org.apache.ignite.cli.builtins.node.NodeManager;
+import org.apache.ignite.cli.spec.IgniteCliSpec;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import picocli.CommandLine;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@DisplayName("ignite")
+@ExtendWith(MockitoExtension.class)
+public class IgniteCliInterfaceTest {
+
+    ApplicationContext applicationContext;
+    ByteArrayOutputStream err;
+    ByteArrayOutputStream out;
+
+    @Mock CliPathsConfigLoader cliPathsConfigLoader;
+
+    @BeforeEach
+    void setup() {
+        applicationContext = ApplicationContext.run(Environment.TEST);
+        applicationContext.registerSingleton(cliPathsConfigLoader);
+        err = new ByteArrayOutputStream();
+        out = new ByteArrayOutputStream();
+    }
+
+    CommandLine commandLine(ApplicationContext applicationContext) {
+        CommandLine.IFactory factory = new CommandFactory(applicationContext);
+        return new CommandLine(IgniteCliSpec.class, factory)
+            .setErr(new PrintWriter(err, true))
+            .setOut(new PrintWriter(out, true));
+    }
+
+    @DisplayName("init")
+    @Nested
+    class Init {
+
+        @Test
+        @DisplayName("init")
+        void init() {
+            var initIgniteCommand = mock(InitIgniteCommand.class);
+            applicationContext.registerSingleton(InitIgniteCommand.class, initIgniteCommand);
+            assertEquals(0, commandLine(applicationContext).execute("init"));
+            verify(initIgniteCommand).init(any());
+        }
+    }
+
+    @DisplayName("module")
+    @Nested
+    class Module {
+
+        @Mock ModuleManager moduleManager;
+
+        @BeforeEach
+        void setUp() {
+            applicationContext.registerSingleton(moduleManager);
+        }
+
+        @Test
+        @DisplayName("add mvn:groupId:artifact:version")
+        void add() {
+            IgnitePaths paths = new IgnitePaths(Path.of("binDir"),
+                Path.of("worksDir"), "version");
+            when(cliPathsConfigLoader.loadIgnitePathsOrThrowError()).thenReturn(paths);
+
+            var exitCode =
+                commandLine(applicationContext).execute("module add mvn:groupId:artifactId:version".split(" "));
+            verify(moduleManager).addModule("mvn:groupId:artifactId:version", paths, Arrays.asList());
+            assertEquals(0, exitCode);
+        }
+
+        @Test
+        @DisplayName("add mvn:groupId:artifact:version --repo http://mvnrepo.com/repostiory")
+        void addWithCustomRepo() throws MalformedURLException {
+            doNothing().when(moduleManager).addModule(any(), any(), any());
+
+            IgnitePaths paths = new IgnitePaths(Path.of("binDir"),
+                Path.of("worksDir"), "version");
+            when(cliPathsConfigLoader.loadIgnitePathsOrThrowError()).thenReturn(paths);
+
+            var exitCode =
+                commandLine(applicationContext)
+                    .execute("module add mvn:groupId:artifactId:version --repo http://mvnrepo.com/repostiory".split(" "));
+            verify(moduleManager).addModule("mvn:groupId:artifactId:version", paths,
+                Arrays.asList(new URL("http://mvnrepo.com/repostiory")));
+            assertEquals(0, exitCode);
+        }
+
+        @Test
+        @DisplayName("add demo-module")
+        void addBuiltinModule() {
+            doNothing().when(moduleManager).addModule(any(), any(), any());
+
+            IgnitePaths paths = new IgnitePaths(Path.of("binDir"),
+                Path.of("worksDir"), "version");
+            when(cliPathsConfigLoader.loadIgnitePathsOrThrowError()).thenReturn(paths);
+
+            var exitCode =
+                commandLine(applicationContext).execute("module add demo-module".split(" "));
+            verify(moduleManager).addModule("demo-module", paths, Collections.emptyList());
+            assertEquals(0, exitCode);
+        }
+
+        @Test
+        @DisplayName("remove builtin-module")
+        void remove() {
+            var moduleName = "builtin-module";
+            when(moduleManager.removeModule(moduleName)).thenReturn(true);
+
+            var exitCode =
+                commandLine(applicationContext).execute("module remove builtin-module".split(" "));
+            verify(moduleManager).removeModule(moduleName);
+            assertEquals(0, exitCode);
+            assertEquals("Module " + moduleName + " was removed successfully\n", out.toString());
+        }
+
+        @Test
+        @DisplayName("remove unknown-module")
+        void removeUnknownModule() {
+            var moduleName = "unknown-module";
+            when(moduleManager.removeModule(moduleName)).thenReturn(false);
+
+            var exitCode =
+                commandLine(applicationContext).execute("module remove unknown-module".split(" "));
+            verify(moduleManager).removeModule(moduleName);
+            assertEquals(0, exitCode);
+            assertEquals("Module " + moduleName + " is not found\n", out.toString());
+        }
+
+        @Test
+        @DisplayName("list")
+        void list() {
+           when(moduleManager.builtinModules()).thenReturn(Arrays.asList(
+               new StandardModuleDefinition("module1", "description1", Arrays.asList("artifact1"), Arrays.asList("cli-artifact1") ),
+               new StandardModuleDefinition("module2", "description2", Arrays.asList("artifact2"), Arrays.asList("cli-artifact2") )
+           ));
+
+           var exitCode =
+               commandLine(applicationContext).execute("module list".split(" "));
+           verify(moduleManager).builtinModules();
+           assertEquals(0, exitCode);
+
+           var expectedOutput =
+               "+---------+--------------+\n" +
+               "| Name    | Description  |\n" +
+               "+---------+--------------+\n" +
+               "| module1 | description1 |\n" +
+               "+---------+--------------+\n" +
+               "| module2 | description2 |\n" +
+               "+---------+--------------+\n";
+           assertEquals(expectedOutput, out.toString());
+        }
+    }
+
+    @Nested
+    @DisplayName("node")
+    class Node {
+
+        @Mock NodeManager nodeManager;
+
+        @BeforeEach
+        void setUp() {
+            applicationContext.registerSingleton(nodeManager);
+        }
+
+        @Test
+        @DisplayName("start node1 --config conf.json")
+        void start() {
+           var ignitePaths = new IgnitePaths(Path.of(""), Path.of(""), "version");
+           var nodeName = "node1";
+           var node =
+               new NodeManager.RunningNode(1, nodeName, Path.of("logfile"));
+           when(nodeManager.start(any(), any(), any(), any()))
+               .thenReturn(node);
+
+            when(cliPathsConfigLoader.loadIgnitePathsOrThrowError())
+                .thenReturn(ignitePaths);
+
+            var exitCode =
+                commandLine(applicationContext).execute(("node start " + nodeName + " --config conf.json").split(" "));
+
+            assertEquals(0, exitCode);
+            verify(nodeManager).start(nodeName, ignitePaths.workDir, ignitePaths.cliPidsDir(), Path.of("conf.json"));
+            assertEquals("Started ignite node.\n" +
+                "PID: 1\n" +
+                "Consistent Id: node1\n" +
+                "Log file: logfile\n", out.toString());
+        }
+
+        @Test
+        @DisplayName("stop node1")
+        void stopRunning() {
+            var ignitePaths = new IgnitePaths(Path.of(""), Path.of(""), "version");
+            var nodeName = "node1";
+            when(nodeManager.stopWait(any(), any()))
+                .thenReturn(true);
+
+            when(cliPathsConfigLoader.loadIgnitePathsOrThrowError())
+                .thenReturn(ignitePaths);
+
+            var exitCode =
+                commandLine(applicationContext).execute(("node stop " + nodeName).split(" "));
+
+            assertEquals(0, exitCode);
+            verify(nodeManager).stopWait(nodeName, ignitePaths.cliPidsDir());
+            assertEquals("Node with consistent id " + nodeName + " was stopped\n", out.toString());
+        }
+
+        @Test
+        @DisplayName("stop unknown-node")
+        void stopUnknown() {
+            var ignitePaths = new IgnitePaths(Path.of(""), Path.of(""), "version");
+            var nodeName = "unknown-node";
+            when(nodeManager.stopWait(any(), any()))
+                .thenReturn(false);
+
+            when(cliPathsConfigLoader.loadIgnitePathsOrThrowError())
+                .thenReturn(ignitePaths);
+
+            var exitCode =
+                commandLine(applicationContext).execute(("node stop " + nodeName).split(" "));
+
+            assertEquals(0, exitCode);
+            verify(nodeManager).stopWait(nodeName, ignitePaths.cliPidsDir());
+            assertEquals("Stop of node " + nodeName + " was failed\n", out.toString());
+        }
+
+        @Test
+        @DisplayName("list")
+        void list() {
+            var ignitePaths = new IgnitePaths(Path.of(""), Path.of(""), "version");
+            when(nodeManager.getRunningNodes(any(), any()))
+                .thenReturn(Arrays.asList(
+                    new NodeManager.RunningNode(1, "new1", Path.of("logFile1")),
+                    new NodeManager.RunningNode(2, "new2", Path.of("logFile2"))
+                ));
+
+            when(cliPathsConfigLoader.loadIgnitePathsOrThrowError())
+                .thenReturn(ignitePaths);
+
+            var exitCode =
+                commandLine(applicationContext).execute("node list".split(" "));
+
+            assertEquals(0, exitCode);
+            verify(nodeManager).getRunningNodes(ignitePaths.workDir, ignitePaths.cliPidsDir());
+            assertEquals("+-----+---------------+----------+\n" +
+                "| PID | Consistent ID | Log      |\n" +
+                "+-----+---------------+----------+\n" +
+                "| 1   | new1          | logFile1 |\n" +
+                "+-----+---------------+----------+\n" +
+                "| 2   | new2          | logFile2 |\n" +
+                "+-----+---------------+----------+\n\n", out.toString());
+        }
+
+        @Test
+        @DisplayName("list")
+        void listEmpty() {
+            var ignitePaths = new IgnitePaths(Path.of(""), Path.of(""), "version");
+            when(nodeManager.getRunningNodes(any(), any()))
+                .thenReturn(Arrays.asList());
+
+            when(cliPathsConfigLoader.loadIgnitePathsOrThrowError())
+                .thenReturn(ignitePaths);
+
+            var exitCode =
+                commandLine(applicationContext).execute("node list".split(" "));
+
+            assertEquals(0, exitCode);
+            verify(nodeManager).getRunningNodes(ignitePaths.workDir, ignitePaths.cliPidsDir());
+            assertEquals("No running nodes\n", out.toString());
+        }
+
+        @Test
+        @DisplayName("classpath")
+        void classpath() throws IOException {
+            when(nodeManager.classpath())
+                .thenReturn("classpath");
+
+            var exitCode =
+                commandLine(applicationContext).execute("node classpath".split(" "));
+
+            assertEquals(0, exitCode);
+            verify(nodeManager).classpath();
+            assertEquals("classpath\n", out.toString());
+        }
+    }
+
+    @Nested
+    @DisplayName("config")
+    class Config {
+
+        @Mock private HttpClient httpClient;
+        @Mock private HttpResponse<String> response;
+
+        @BeforeEach
+        void setUp() {
+            applicationContext.registerSingleton(httpClient);
+        }
+
+        @Test
+        @DisplayName("get --node-endpoint localhost:8081")
+        void get() throws IOException, InterruptedException {
+            when(response.statusCode()).thenReturn(HttpURLConnection.HTTP_OK);
+            when(response.body()).thenReturn("{\"baseline\":{\"autoAdjust\":{\"enabled\":true}}}");
+            when(httpClient.<String>send(any(), any())).thenReturn(response);
+
+            var exitCode =
+                commandLine(applicationContext).execute("config get --node-endpoint localhost:8081".split(" "));
+
+            assertEquals(0, exitCode);
+            verify(httpClient).send(
+                argThat(r -> r.uri().toString().equals("http://localhost:8081/management/v1/configuration/") &&
+                    r.headers().firstValue("Content-Type").get().equals("application/json")),
+                any());
+            assertEquals("{\n" +
+                "  \"baseline\" : {\n" +
+                "    \"autoAdjust\" : {\n" +
+                "      \"enabled\" : true\n" +
+                "    }\n" +
+                "  }\n" +
+                "}\n", out.toString());
+        }
+
+        @Test
+        @DisplayName("get --node-endpoint localhost:8081 --subtree local.baseline")
+        void getSubtree() throws IOException, InterruptedException {
+            when(response.statusCode()).thenReturn(HttpURLConnection.HTTP_OK);
+            when(response.body()).thenReturn("{\"autoAdjust\":{\"enabled\":true}}");
+            when(httpClient.<String>send(any(), any())).thenReturn(response);
+
+            var exitCode =
+                commandLine(applicationContext).execute(("config get --node-endpoint localhost:8081 " +
+                    "--subtree local.baseline").split(" "));
+
+            assertEquals(0, exitCode);
+            verify(httpClient).send(
+                argThat(r -> r.uri().toString().equals("http://localhost:8081/management/v1/configuration/local.baseline") &&
+                    r.headers().firstValue("Content-Type").get().equals("application/json")),
+                any());
+            assertEquals("{\n" +
+                "  \"autoAdjust\" : {\n" +
+                "    \"enabled\" : true\n" +
+                "  }\n" +
+                "}\n", out.toString());
+        }
+
+        @Test
+        @DisplayName("set --node-endpoint localhost:8081 local.baseline.autoAdjust.enabled=true")
+        void setHocon() throws IOException, InterruptedException {
+            when(response.statusCode()).thenReturn(HttpURLConnection.HTTP_OK);
+            when(httpClient.<String>send(any(), any())).thenReturn(response);
+
+            var expectedSentContent = "{\"local\":{\"baseline\":{\"autoAdjust\":{\"enabled\":true}}}}";
+
+            var exitCode =
+                commandLine(applicationContext).execute(("config set --node-endpoint localhost:8081 " +
+                    "local.baseline.autoAdjust.enabled=true"
+                    ).split(" "));
+
+            assertEquals(0, exitCode);
+            verify(httpClient).send(
+                argThat(r -> r.uri().toString().equals("http://localhost:8081/management/v1/configuration/") &&
+                    r.method().equals("POST") &&
+                    // TODO: body matcher should be fixed to more appropriate
+                    r.bodyPublisher().get().contentLength() == expectedSentContent.getBytes().length &&
+                    r.headers().firstValue("Content-Type").get().equals("application/json")),
+                any());
+            assertEquals("\n", out.toString());
+        }
+
+        @Test
+        @DisplayName("set --node-endpoint localhost:8081 {\"local\":{\"baseline\":{\"autoAdjust\":{\"enabled\":true}}}}")
+        void setJson() throws IOException, InterruptedException {
+            when(response.statusCode()).thenReturn(HttpURLConnection.HTTP_OK);
+            when(httpClient.<String>send(any(), any())).thenReturn(response);
+
+            var expectedSentContent = "{\"local\":{\"baseline\":{\"autoAdjust\":{\"enabled\":true}}}}";
+
+            var exitCode =
+                commandLine(applicationContext).execute(("config set --node-endpoint localhost:8081 " +
+                    "local.baseline.autoAdjust.enabled=true"
+                ).split(" "));
+
+            assertEquals(0, exitCode);
+            verify(httpClient).send(
+                argThat(r -> r.uri().toString().equals("http://localhost:8081/management/v1/configuration/") &&
+                    r.method().equals("POST") &&
+                    // TODO: body matcher should be fixed to more appropriate
+                    r.bodyPublisher().get().contentLength() == expectedSentContent.getBytes().length &&
+                    r.headers().firstValue("Content-Type").get().equals("application/json")),
+                any());
+            assertEquals("\n", out.toString());
+
+        }
+    }
+}
diff --git a/modules/cli-demo/demo-module-all/demo-module-cli/pom.xml b/modules/cli-demo/demo-module-all/demo-module-cli/pom.xml
new file mode 100644
index 0000000..5904b10
--- /dev/null
+++ b/modules/cli-demo/demo-module-all/demo-module-cli/pom.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>ignite-cli-demo</artifactId>
+        <groupId>org.apache.ignite</groupId>
+        <version>3.0-SNAPSHOT</version>
+        <relativePath>../../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>ignite-demo-module-cli</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-cli-common</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>info.picocli</groupId>
+            <artifactId>picocli</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+                <filtering>true</filtering>
+            </resource>
+        </resources>
+    </build>
+
+
+</project>
diff --git a/modules/cli-demo/demo-module-all/demo-module-cli/src/main/java/org/apache/ignite/snapshot/cli/SnapshotCommand.java b/modules/cli-demo/demo-module-all/demo-module-cli/src/main/java/org/apache/ignite/snapshot/cli/SnapshotCommand.java
new file mode 100644
index 0000000..9b1316c
--- /dev/null
+++ b/modules/cli-demo/demo-module-all/demo-module-cli/src/main/java/org/apache/ignite/snapshot/cli/SnapshotCommand.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.snapshot.cli;
+
+import org.apache.ignite.cli.common.IgniteCommand;
+import picocli.CommandLine;
+
+@CommandLine.Command(
+    name = "snapshot",
+    description = "Snapshots management",
+    subcommands = {
+        SnapshotCommand.CreateSnashotCommand.class,
+        SnapshotCommand.CancelSnapshotCommand.class}
+)
+public class SnapshotCommand implements IgniteCommand, Runnable {
+
+    @CommandLine.Spec CommandLine.Model.CommandSpec spec;
+
+    @Override public void run() {
+        spec.commandLine().usage(spec.commandLine().getOut());
+    }
+
+    @CommandLine.Command(
+        name = "create",
+        description = "Create snapshot"
+    )
+    public static class CreateSnashotCommand implements Runnable {
+
+        @CommandLine.Spec CommandLine.Model.CommandSpec spec;
+
+        @Override public void run() {
+            spec.commandLine().getOut().println("Create snapshot command was executed");
+        }
+    }
+
+
+    @CommandLine.Command(
+        name = "cancel",
+        description = "Cancel snapshot"
+    )
+    public static class CancelSnapshotCommand implements Runnable {
+
+        @CommandLine.Spec CommandLine.Model.CommandSpec spec;
+
+        @Override public void run() {
+            spec.commandLine().getOut().println("Cancel snapshot command was executed");
+        }
+
+    }
+}
diff --git a/modules/cli-demo/demo-module-all/demo-module-cli/src/main/resources/META-INF/services/org.apache.ignite.cli.common.IgniteCommand b/modules/cli-demo/demo-module-all/demo-module-cli/src/main/resources/META-INF/services/org.apache.ignite.cli.common.IgniteCommand
new file mode 100644
index 0000000..d916aa9
--- /dev/null
+++ b/modules/cli-demo/demo-module-all/demo-module-cli/src/main/resources/META-INF/services/org.apache.ignite.cli.common.IgniteCommand
@@ -0,0 +1 @@
+org.apache.ignite.snapshot.cli.SnapshotCommand
\ No newline at end of file
diff --git a/modules/cli-demo/demo-module-all/demo-module/pom.xml b/modules/cli-demo/demo-module-all/demo-module/pom.xml
new file mode 100644
index 0000000..c4ea834
--- /dev/null
+++ b/modules/cli-demo/demo-module-all/demo-module/pom.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>ignite-cli-demo</artifactId>
+        <groupId>org.apache.ignite</groupId>
+        <version>3.0-SNAPSHOT</version>
+        <relativePath>../../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>ignite-demo-module</artifactId>
+    <name>ignite-demo-module</name>
+
+
+</project>
diff --git a/modules/cli-demo/demo-module-all/demo-module/src/main/java/org/apache/ignite/snapshot/IgniteSnapshot.java b/modules/cli-demo/demo-module-all/demo-module/src/main/java/org/apache/ignite/snapshot/IgniteSnapshot.java
new file mode 100644
index 0000000..a885054
--- /dev/null
+++ b/modules/cli-demo/demo-module-all/demo-module/src/main/java/org/apache/ignite/snapshot/IgniteSnapshot.java
@@ -0,0 +1,25 @@
+/*
+ * 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.snapshot;
+
+public class IgniteSnapshot {
+
+    static {
+        System.out.println("Snapshot module loaded");
+    }
+}
diff --git a/modules/cli-demo/demo-module-all/pom.xml b/modules/cli-demo/demo-module-all/pom.xml
new file mode 100644
index 0000000..1f63a6d
--- /dev/null
+++ b/modules/cli-demo/demo-module-all/pom.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  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.
+-->
+
+<!--
+    POM file.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.ignite</groupId>
+        <artifactId>ignite-cli-demo</artifactId>
+        <version>3.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>ignite-demo-module-all</artifactId>
+    <version>3.0-SNAPSHOT</version>
+    <url>http://ignite.apache.org</url>
+    <name>ignite-demo-module-all</name>
+    <packaging>pom</packaging>
+
+    <modules>
+        <module>demo-module-cli</module>
+        <module>demo-module</module>
+    </modules>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-demo-module</artifactId>
+            <version>3.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-demo-module-cli</artifactId>
+            <version>3.0-SNAPSHOT</version>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/modules/cli-demo/pom.xml b/modules/cli-demo/pom.xml
new file mode 100644
index 0000000..c5236ca
--- /dev/null
+++ b/modules/cli-demo/pom.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  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.
+ -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>org.apache.ignite</groupId>
+    <artifactId>ignite-cli-demo</artifactId>
+    <version>3.0-SNAPSHOT</version>
+    <url>http://ignite.apache.org</url>
+    <packaging>pom</packaging>
+    <name>ignite-cli-demo</name>
+
+    <modules>
+        <module>cli-common</module>
+        <module>cli</module>
+        <module>demo-module-all</module>
+    </modules>
+
+    <properties>
+        <micronaut.version>2.1.3</micronaut.version>
+        <picocli.version>4.5.2</picocli.version>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>info.picocli</groupId>
+                <artifactId>picocli</artifactId>
+                <version>${picocli.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.micronaut</groupId>
+                <artifactId>micronaut-inject-java</artifactId>
+                <version>${micronaut.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.micronaut</groupId>
+                <artifactId>micronaut-core</artifactId>
+                <version>${micronaut.version}</version>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <!-- annotationProcessorPaths requires maven-compiler-plugin version 3.5 or higher -->
+                <version>3.8.1</version>
+                <configuration>
+                    <source>11</source>
+                    <target>11</target>
+                    <annotationProcessorPaths>
+                        <path>
+                            <groupId>io.micronaut</groupId>
+                            <artifactId>micronaut-inject-java</artifactId>
+                            <version>${micronaut.version}</version>
+                        </path>
+                        <path>
+                            <groupId>info.picocli</groupId>
+                            <artifactId>picocli-codegen</artifactId>
+                            <version>4.5.2</version>
+                        </path>
+                    </annotationProcessorPaths>
+                    <compilerArgs>
+                        <arg>-Aproject=${project.groupId}/${project.artifactId}</arg>
+                    </compilerArgs>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/pom.xml b/pom.xml
index 64f2265..9e93eef 100644
--- a/pom.xml
+++ b/pom.xml
@@ -41,5 +41,6 @@
         <module>modules/configuration</module>
         <module>modules/configuration-annotation-processor</module>
         <module>modules/ignite-runner</module>
+        <module>modules/cli-demo</module>
     </modules>
 </project>