You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by cs...@apache.org on 2024/11/13 14:15:01 UTC

(maven) branch master updated: [MNG-8375] CLIng diet (#1897)

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

cstamas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven.git


The following commit(s) were added to refs/heads/master by this push:
     new a67d2746af [MNG-8375] CLIng diet (#1897)
a67d2746af is described below

commit a67d2746af8c498689d26419c818611e51ae8f14
Author: Tamas Cservenak <ta...@cservenak.net>
AuthorDate: Wed Nov 13 15:14:56 2024 +0100

    [MNG-8375] CLIng diet (#1897)
    
    First part was to "reverse" MavenCli into CLIng, that also became a huge and complex beast. Now, sanitization comes, tearing down unneeded stuff. CLIng should be simple and straightforward.
    
    Now the **invoker** fully parses args, creates Maven instance (ie. local, using Maven components on classpath) and invokes Maven. The new **executor** in turn does NOT fully parses args and is logical equivalent of maven-invoker.
    
    Changes:
    * radically simplify CLIng existing classes (and especially API - the fact we have "local", "resident" etc invoker is actually implementation detail).
    * introduce "executor", a "lower level" tool that does not parse args (and is logical equivalent of maven-invoker) and support Maven 4.x and Maven 3.x.
    
    ---
    
    https://issues.apache.org/jira/browse/MNG-8375
---
 .mvn/maven.config                                  |   1 +
 .../java/org/apache/maven/api/cli/Executor.java    |  55 +++++
 ...nvokerException.java => ExecutorException.java} |  10 +-
 .../org/apache/maven/api/cli/ExecutorRequest.java  |  82 +++++++
 .../java/org/apache/maven/api/cli/Invoker.java     |   8 +-
 .../org/apache/maven/api/cli/InvokerException.java |  17 ++
 .../org/apache/maven/api/cli/InvokerRequest.java   |  43 +---
 .../main/java/org/apache/maven/api/cli/Parser.java |  36 +--
 .../org/apache/maven/api/cli/ParserRequest.java    |  10 +-
 .../local/LocalMavenInvoker.java => Tools.java}    |  19 +-
 .../org/apache/maven/api/cli/mvn/MavenInvoker.java |  47 ----
 .../maven/api/cli/mvn/MavenInvokerRequest.java     |  39 ---
 .../org/apache/maven/api/cli/mvn/MavenParser.java  |  51 ----
 .../cli/mvn/forked/ForkedMavenInvokerRequest.java  |  45 ----
 .../api/cli/mvn/forked/ForkedMavenParser.java      |  49 ----
 .../maven/api/cli/mvnenc/EncryptInvoker.java       |  47 ----
 .../api/cli/mvnenc/EncryptInvokerRequest.java      |  37 ---
 .../apache/maven/api/cli/mvnenc/EncryptParser.java |  49 ----
 .../java/org/apache/maven/cling/ClingSupport.java  |  12 +-
 .../java/org/apache/maven/cling/MavenCling.java    |  20 +-
 .../java/org/apache/maven/cling/MavenEncCling.java |  19 +-
 .../maven/cling/invoker/BaseExecutorRequest.java   |  77 ++++++
 .../maven/cling/invoker/BaseInvokerRequest.java    |  40 +--
 .../org/apache/maven/cling/invoker/BaseParser.java |  50 +++-
 .../maven/cling/invoker/ContainerCapsule.java      |   8 +
 .../cling/invoker/ContainerCapsuleFactory.java     |  11 +-
 .../apache/maven/cling/invoker/LookupContext.java  | 109 +++++++++
 .../apache/maven/cling/invoker/LookupInvoker.java  | 155 +++---------
 .../cling/invoker/PlexusContainerCapsule.java      |   7 +
 .../invoker/PlexusContainerCapsuleFactory.java     |  35 ++-
 .../apache/maven/cling/invoker/ProtoLogger.java    |   2 +-
 .../cling/invoker/mvn/DefaultMavenParser.java      |  62 -----
 .../maven/cling/invoker/mvn/MavenContext.java      |  42 ++++
 ...{DefaultMavenInvoker.java => MavenInvoker.java} |  79 +++---
 ...nvokerRequest.java => MavenInvokerRequest.java} |  18 +-
 .../mvn/{BaseMavenParser.java => MavenParser.java} |  49 +++-
 .../mvn/embedded/EmbeddedMavenExecutor.java        | 267 +++++++++++++++++++++
 .../forked/DefaultForkedMavenInvokerRequest.java   |  78 ------
 .../mvn/forked/DefaultForkedMavenParser.java       |  96 --------
 .../invoker/mvn/forked/ForkedMavenExecutor.java    |  73 ++++++
 ...edMavenInvoker.java => ForkedMavenInvoker.java} |  14 +-
 .../mvn/local/DefaultLocalMavenInvoker.java        |  54 -----
 .../cling/invoker/mvn/local/LocalMavenInvoker.java |  28 +--
 .../{mvnenc/Goal.java => mvn/package-info.java}    |   7 +-
 .../mvn/resident/DefaultResidentMavenInvoker.java  | 139 -----------
 .../invoker/mvn/resident/ResidentMavenContext.java |  67 ++++++
 .../invoker/mvn/resident/ResidentMavenInvoker.java |  86 +++++++
 .../invoker/mvnenc/CommonsCliEncryptOptions.java   |   3 +
 .../maven/cling/invoker/mvnenc/EncryptContext.java |  54 +++++
 ...aultEncryptInvoker.java => EncryptInvoker.java} |  60 +----
 ...okerRequest.java => EncryptInvokerRequest.java} |  14 +-
 ...efaultEncryptParser.java => EncryptParser.java} |  14 +-
 .../apache/maven/cling/invoker/mvnenc/Goal.java    |   2 +-
 .../mvnenc/goals/ConfiguredGoalSupport.java        |  13 +-
 .../maven/cling/invoker/mvnenc/goals/Decrypt.java  |   8 +-
 .../maven/cling/invoker/mvnenc/goals/Diag.java     |   6 +-
 .../maven/cling/invoker/mvnenc/goals/Encrypt.java  |   6 +-
 .../maven/cling/invoker/mvnenc/goals/Init.java     |  18 +-
 .../maven/cling/invoker/mvnenc/package-info.java   |  11 +-
 .../{mvnenc/Goal.java => package-info.java}        |   8 +-
 .../invoker/mvn/MavenExecutorTestSupport.java      |  64 +++++
 .../cling/invoker/mvn/MavenInvokerTestSupport.java |  65 +----
 .../maven/cling/invoker/mvn/MavenTestSupport.java  |  69 ++++++
 .../EmbeddedMavenExecutorTest.java}                |  33 ++-
 ...nvokerTest.java => ForkedMavenInvokerTest.java} |  16 +-
 .../mvn/local/DefaultLocalMavenInvokerTest.java    |  15 +-
 .../resident/DefaultResidentMavenInvokerTest.java  |  15 +-
 67 files changed, 1451 insertions(+), 1392 deletions(-)

diff --git a/.mvn/maven.config b/.mvn/maven.config
new file mode 100644
index 0000000000..a4e2970377
--- /dev/null
+++ b/.mvn/maven.config
@@ -0,0 +1 @@
+-Daether.transport.jdk.httpVersion=HTTP_1_1
\ No newline at end of file
diff --git a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Executor.java b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Executor.java
new file mode 100644
index 0000000000..60cd38921a
--- /dev/null
+++ b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Executor.java
@@ -0,0 +1,55 @@
+/*
+ * 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.maven.api.cli;
+
+import org.apache.maven.api.annotations.Experimental;
+import org.apache.maven.api.annotations.Nonnull;
+
+/**
+ * Defines the contract for a component responsible for executing a Maven tool
+ * using the information provided in an {@link ExecutorRequest}. This interface is central
+ * to the execution of Maven commands and builds, but it does not construct nor fully parses arguments.
+ *
+ * @since 4.0.0
+ */
+@Experimental
+public interface Executor extends AutoCloseable {
+    /**
+     * Invokes the tool application using the provided {@link ExecutorRequest}.
+     * This method is responsible for executing the command or build
+     * process based on the information contained in the request.
+     *
+     * @param executorRequest the request containing all necessary information for the execution
+     * @return an integer representing the exit code of the execution (0 typically indicates success)
+     * @throws ExecutorException if an error occurs during the execution process
+     */
+    int execute(@Nonnull ExecutorRequest executorRequest) throws ExecutorException;
+
+    /**
+     * Closes and disposes of this {@link Executor} instance, releasing any resources it may hold.
+     * This method is called automatically when using try-with-resources statements.
+     *
+     * <p>The default implementation does nothing. Subclasses should override this method
+     * if they need to perform cleanup operations.</p>
+     *
+     * @throws ExecutorException if an error occurs while closing the {@link Executor}
+     */
+    @Override
+    default void close() throws ExecutorException {}
+}
diff --git a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/InvokerException.java b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/ExecutorException.java
similarity index 81%
copy from api/maven-api-cli/src/main/java/org/apache/maven/api/cli/InvokerException.java
copy to api/maven-api-cli/src/main/java/org/apache/maven/api/cli/ExecutorException.java
index 08c8df20ef..8ce5bf157d 100644
--- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/InvokerException.java
+++ b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/ExecutorException.java
@@ -23,20 +23,20 @@ import org.apache.maven.api.annotations.Nullable;
 import org.apache.maven.api.services.MavenException;
 
 /**
- * Represents an exception that occurs during the invocation of a Maven build or command.
+ * Represents an exception that occurs during the execution of a Maven build or command.
  * This exception is typically thrown when there are errors during the execution of a Maven
- * process, such as build failures, plugin errors, or other runtime issues.
+ * process, such as wrong cwd, non-existent installation directory, or other runtime issues.
  *
  * @since 4.0.0
  */
 @Experimental
-public class InvokerException extends MavenException {
+public class ExecutorException extends MavenException {
     /**
      * Constructs a new {@code InvokerException} with the specified detail message.
      *
      * @param message the detail message explaining the cause of the exception
      */
-    public InvokerException(@Nullable String message) {
+    public ExecutorException(@Nullable String message) {
         super(message);
     }
 
@@ -46,7 +46,7 @@ public class InvokerException extends MavenException {
      * @param message the detail message explaining the cause of the exception
      * @param cause the underlying cause of the exception
      */
-    public InvokerException(@Nullable String message, @Nullable Throwable cause) {
+    public ExecutorException(@Nullable String message, @Nullable Throwable cause) {
         super(message, cause);
     }
 }
diff --git a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/ExecutorRequest.java b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/ExecutorRequest.java
new file mode 100644
index 0000000000..811fe87686
--- /dev/null
+++ b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/ExecutorRequest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.maven.api.cli;
+
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.maven.api.annotations.Experimental;
+import org.apache.maven.api.annotations.Immutable;
+import org.apache.maven.api.annotations.Nonnull;
+
+/**
+ * Represents a request to execute Maven with command-line arguments.
+ * This interface encapsulates all the necessary information needed to execute
+ * Maven command with arguments. The arguments were not parsed, they are just passed over
+ * to executed tool.
+ *
+ * @since 4.0.0
+ */
+@Immutable
+@Experimental
+public interface ExecutorRequest {
+    /**
+     * The parser request this instance was created from.
+     */
+    @Nonnull
+    ParserRequest parserRequest();
+
+    /**
+     * Returns the current working directory for the Maven execution.
+     * This is typically the directory from which Maven was invoked.
+     *
+     * @return the current working directory path
+     */
+    @Nonnull
+    Path cwd();
+
+    /**
+     * Returns the Maven installation directory.
+     * This is usually set by the Maven launcher script using the "maven.home" system property.
+     *
+     * @return the Maven installation directory path
+     */
+    @Nonnull
+    Path installationDirectory();
+
+    /**
+     * Returns the user's home directory.
+     * This is typically obtained from the "user.home" system property.
+     *
+     * @return the user's home directory path
+     */
+    @Nonnull
+    Path userHomeDirectory();
+
+    /**
+     * Returns the list of extra JVM arguments to be passed to the forked process.
+     * These arguments allow for customization of the JVM environment in which tool will run.
+     * This property is used ONLY by executors and invokers that spawn a new JVM.
+     *
+     * @return an Optional containing the list of extra JVM arguments, or empty if not specified
+     */
+    @Nonnull
+    Optional<List<String>> jvmArguments();
+}
diff --git a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Invoker.java b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Invoker.java
index bc5f879895..70b54ba204 100644
--- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Invoker.java
+++ b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Invoker.java
@@ -24,18 +24,16 @@ import org.apache.maven.api.annotations.Nonnull;
 /**
  * Defines the contract for a component responsible for invoking a Maven application
  * using the information provided in an {@link InvokerRequest}. This interface is central
- * to the execution of Maven commands and builds.
+ * to the construction and invocation of Maven commands and builds, and it fully parses arguments.
  *
  * <p>The Invoker is designed to be flexible, allowing for different implementations
  * that can handle various types of {@link InvokerRequest InvokerRequests}. It also implements
  * {@link AutoCloseable} to ensure proper resource management.</p>
  *
- * @param <R> The specific type of {@link InvokerRequest} this {@code Invoker} can handle, extending {@link InvokerRequest}
- *
  * @since 4.0.0
  */
 @Experimental
-public interface Invoker<R extends InvokerRequest<? extends Options>> extends AutoCloseable {
+public interface Invoker extends AutoCloseable {
     /**
      * Invokes the Maven application using the provided {@link InvokerRequest}.
      * This method is responsible for executing the Maven command or build
@@ -45,7 +43,7 @@ public interface Invoker<R extends InvokerRequest<? extends Options>> extends Au
      * @return an integer representing the exit code of the invocation (0 typically indicates success)
      * @throws InvokerException if an error occurs during the invocation process
      */
-    int invoke(@Nonnull R invokerRequest) throws InvokerException;
+    int invoke(@Nonnull InvokerRequest invokerRequest) throws InvokerException;
 
     /**
      * Closes and disposes of this {@link Invoker} instance, releasing any resources it may hold.
diff --git a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/InvokerException.java b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/InvokerException.java
index 08c8df20ef..d1a479b711 100644
--- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/InvokerException.java
+++ b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/InvokerException.java
@@ -49,4 +49,21 @@ public class InvokerException extends MavenException {
     public InvokerException(@Nullable String message, @Nullable Throwable cause) {
         super(message, cause);
     }
+
+    /**
+     * Exception for intentional exit: No message or anything will be displayed, just the
+     * carried exit code will be returned from invoker {@link Invoker#invoke(InvokerRequest)} method.
+     */
+    public static final class ExitException extends InvokerException {
+        private final int exitCode;
+
+        public ExitException(int exitCode) {
+            super("EXIT");
+            this.exitCode = exitCode;
+        }
+
+        public int getExitCode() {
+            return exitCode;
+        }
+    }
 }
diff --git a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/InvokerRequest.java b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/InvokerRequest.java
index d38d0cc633..ed019663c6 100644
--- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/InvokerRequest.java
+++ b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/InvokerRequest.java
@@ -33,22 +33,14 @@ import org.apache.maven.api.services.Lookup;
 import org.apache.maven.api.services.MessageBuilderFactory;
 
 /**
- * Represents a Maven execution request, encapsulating all necessary information
- * for invoking a Maven build or command.
- *
- * @param <O> the type of {@link Options} used for this request, extending the base {@link Options} interface
+ * Represents a Maven invocation request, encapsulating all necessary information
+ * for invoking a Maven build or command. Arguments are parsed and exposed via methods.
  *
  * @since 4.0.0
  */
 @Immutable
 @Experimental
-public interface InvokerRequest<O extends Options> {
-    /**
-     * The parser request this instance was created from.
-     */
-    @Nonnull
-    ParserRequest parserRequest();
-
+public interface InvokerRequest extends ExecutorRequest {
     /**
      * Shorthand for {@link Logger} to use.
      */
@@ -70,33 +62,6 @@ public interface InvokerRequest<O extends Options> {
         return parserRequest().lookup();
     }
 
-    /**
-     * Returns the current working directory for the Maven execution.
-     * This is typically the directory from which Maven was invoked.
-     *
-     * @return the current working directory path
-     */
-    @Nonnull
-    Path cwd();
-
-    /**
-     * Returns the Maven installation directory.
-     * This is usually set by the Maven launcher script using the "maven.home" system property.
-     *
-     * @return the Maven installation directory path
-     */
-    @Nonnull
-    Path installationDirectory();
-
-    /**
-     * Returns the user's home directory.
-     * This is typically obtained from the "user.home" system property.
-     *
-     * @return the user's home directory path
-     */
-    @Nonnull
-    Path userHomeDirectory();
-
     /**
      * Returns a map of user-defined properties for the Maven execution.
      * These properties can be set using the -D command-line option.
@@ -172,5 +137,5 @@ public interface InvokerRequest<O extends Options> {
      * @return the options object
      */
     @Nonnull
-    O options();
+    Options options();
 }
diff --git a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Parser.java b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Parser.java
index a47a76f4e7..6b791cbaae 100644
--- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Parser.java
+++ b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Parser.java
@@ -22,47 +22,35 @@ import java.io.IOException;
 
 import org.apache.maven.api.annotations.Experimental;
 import org.apache.maven.api.annotations.Nonnull;
-import org.apache.maven.api.services.MessageBuilderFactory;
 
 /**
- * Defines the contract for parsing Maven command-line arguments and creating an InvokerRequest.
- * This interface is responsible for interpreting the command-line input and constructing
- * the appropriate {@link InvokerRequest} object.
- *
- * @param <R> the type of {@link InvokerRequest} produced by this parser, extending {@link InvokerRequest}
+ * Defines the contract for parsing Maven command-line arguments and creating an execution or invoker requests.
  *
  * @since 4.0.0
  */
 @Experimental
-public interface Parser<R extends InvokerRequest<? extends Options>> {
+public interface Parser {
     /**
-     * Parses the given Maven arguments to create an InvokerRequest.
-     * This is a convenience method that internally creates a ParserRequest using
-     * {@link ParserRequest#mvn(String[], Logger, MessageBuilderFactory)}.
+     * Parses the given ParserRequest to create an {@link ExecutorRequest}.
+     * This method does not interpret tool arguments.
      *
-     * @param args the command-line arguments
-     * @param logger the logger to use during parsing
-     * @param messageBuilderFactory the factory for creating message builders
-     * @return the parsed InvokerRequest
-     * @throws ParserException if there's an error during parsing of the command or arguments
+     * @param parserRequest the request containing all necessary information for parsing
+     * @return the parsed executor request
+     * @throws ParserException if there's an error during parsing of the request
      * @throws IOException if there's an I/O error during the parsing process
      */
     @Nonnull
-    default R mvn(@Nonnull String[] args, @Nonnull Logger logger, @Nonnull MessageBuilderFactory messageBuilderFactory)
-            throws ParserException, IOException {
-        return parse(ParserRequest.mvn(args, logger, messageBuilderFactory).build());
-    }
+    ExecutorRequest parseExecution(@Nonnull ParserRequest parserRequest) throws ParserException, IOException;
 
     /**
-     * Parses the given ParserRequest to create an InvokerRequest.
-     * This method is responsible for interpreting the contents of the ParserRequest
-     * and constructing the appropriate InvokerRequest object.
+     * Parses the given ParserRequest to create an {@link InvokerRequest}.
+     * This method does interpret tool arguments.
      *
      * @param parserRequest the request containing all necessary information for parsing
-     * @return the parsed InvokerRequest
+     * @return the parsed invoker request
      * @throws ParserException if there's an error during parsing of the request
      * @throws IOException if there's an I/O error during the parsing process
      */
     @Nonnull
-    R parse(@Nonnull ParserRequest parserRequest) throws ParserException, IOException;
+    InvokerRequest parseInvocation(@Nonnull ParserRequest parserRequest) throws ParserException, IOException;
 }
diff --git a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/ParserRequest.java b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/ParserRequest.java
index ae5f83ec4b..014576b492 100644
--- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/ParserRequest.java
+++ b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/ParserRequest.java
@@ -46,12 +46,6 @@ import static java.util.Objects.requireNonNull;
 @Immutable
 @Experimental
 public interface ParserRequest {
-    String MVN_CMD = "mvn";
-    String MVN_NAME = "Maven";
-
-    String MVNENC_CMD = "mvnenc";
-    String MVNENC_NAME = "Maven Password Encrypting Tool";
-
     /**
      * Returns the Maven command to be executed. This command is used in some invokers (ie forked) but also to
      * present help to user.
@@ -179,7 +173,7 @@ public interface ParserRequest {
     @Nonnull
     static Builder mvn(
             @Nonnull List<String> args, @Nonnull Logger logger, @Nonnull MessageBuilderFactory messageBuilderFactory) {
-        return builder(MVN_CMD, MVN_NAME, args, logger, messageBuilderFactory);
+        return builder(Tools.MVN_CMD, Tools.MVN_NAME, args, logger, messageBuilderFactory);
     }
 
     /**
@@ -207,7 +201,7 @@ public interface ParserRequest {
     @Nonnull
     static Builder mvnenc(
             @Nonnull List<String> args, @Nonnull Logger logger, @Nonnull MessageBuilderFactory messageBuilderFactory) {
-        return builder(MVNENC_CMD, MVNENC_NAME, args, logger, messageBuilderFactory);
+        return builder(Tools.MVNENC_CMD, Tools.MVNENC_NAME, args, logger, messageBuilderFactory);
     }
 
     /**
diff --git a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/local/LocalMavenInvoker.java b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Tools.java
similarity index 67%
rename from api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/local/LocalMavenInvoker.java
rename to api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Tools.java
index ed429c2220..b237dc5414 100644
--- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/local/LocalMavenInvoker.java
+++ b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Tools.java
@@ -16,17 +16,24 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.maven.api.cli.mvn.local;
+package org.apache.maven.api.cli;
 
 import org.apache.maven.api.annotations.Experimental;
-import org.apache.maven.api.cli.mvn.MavenInvoker;
-import org.apache.maven.api.cli.mvn.MavenInvokerRequest;
-import org.apache.maven.api.cli.mvn.MavenOptions;
+import org.apache.maven.api.annotations.Immutable;
 
 /**
- * Local Maven invoker, it expects all the Maven be present in classpath.
+ * Represents most common tools supported by CLIng.
  *
  * @since 4.0.0
  */
+@Immutable
 @Experimental
-public interface LocalMavenInvoker extends MavenInvoker<MavenInvokerRequest<MavenOptions>> {}
+public final class Tools {
+    private Tools() {}
+
+    public static final String MVN_CMD = "mvn";
+    public static final String MVN_NAME = "Maven";
+
+    public static final String MVNENC_CMD = "mvnenc";
+    public static final String MVNENC_NAME = "Maven Password Encrypting Tool";
+}
diff --git a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/MavenInvoker.java b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/MavenInvoker.java
deleted file mode 100644
index c7e9673064..0000000000
--- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/MavenInvoker.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.maven.api.cli.mvn;
-
-import org.apache.maven.api.annotations.Experimental;
-import org.apache.maven.api.annotations.Nonnull;
-import org.apache.maven.api.cli.Invoker;
-import org.apache.maven.api.cli.InvokerException;
-
-/**
- * Defines the contract for a component responsible for invoking Maven using information provided in an invoker request.
- * This interface extends the general {@link Invoker} interface, specializing it for Maven-specific operations.
- *
- * @param <R> The specific type of {@link MavenInvokerRequest} this invoker can handle
- *
- * @since 4.0.0
- */
-@Experimental
-public interface MavenInvoker<R extends MavenInvokerRequest<? extends MavenOptions>> extends Invoker<R> {
-    /**
-     * Invokes Maven using the provided MavenInvokerRequest.
-     * This method is responsible for executing the Maven build process
-     * based on the information contained in the request.
-     *
-     * @param invokerRequest the request containing all necessary information for the Maven invocation
-     * @return an integer representing the exit code of the Maven invocation (0 typically indicates success)
-     * @throws InvokerException if an error occurs during the Maven invocation process
-     */
-    @Override
-    int invoke(@Nonnull R invokerRequest) throws InvokerException;
-}
diff --git a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/MavenInvokerRequest.java b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/MavenInvokerRequest.java
deleted file mode 100644
index 4030c5320b..0000000000
--- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/MavenInvokerRequest.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.maven.api.cli.mvn;
-
-import org.apache.maven.api.annotations.Experimental;
-import org.apache.maven.api.cli.InvokerRequest;
-
-/**
- * Represents a request to invoke Maven.
- * This interface extends the general {@link InvokerRequest}, specializing it for Maven-specific operations.
- *
- * <p>A {@link MavenInvokerRequest} encapsulates all the necessary information needed to perform
- * a Maven build, including any Maven-specific options defined in {@link MavenOptions}.</p>
- *
- * @param <O> The specific Options type this request carries
- *
- * @since 4.0.0
- */
-@Experimental
-public interface MavenInvokerRequest<O extends MavenOptions> extends InvokerRequest<O> {
-    // This interface doesn't declare any additional methods beyond those inherited from InvokerRequest.
-    // It serves to type-specify the Options as MavenOptions for Maven-specific requests.
-}
diff --git a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/MavenParser.java b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/MavenParser.java
deleted file mode 100644
index bb9e5bcb8c..0000000000
--- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/MavenParser.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.maven.api.cli.mvn;
-
-import java.io.IOException;
-
-import org.apache.maven.api.annotations.Experimental;
-import org.apache.maven.api.annotations.Nonnull;
-import org.apache.maven.api.cli.Parser;
-import org.apache.maven.api.cli.ParserException;
-import org.apache.maven.api.cli.ParserRequest;
-
-/**
- * Defines the contract for parsing Maven-specific command-line arguments and creating a MavenInvokerRequest.
- * This interface extends the general {@link Parser} interface, specializing it for Maven operations.
- *
- * @param <R> The specific type of MavenInvokerRequest this parser produces
- *
- * @since 4.0.0
- */
-@Experimental
-public interface MavenParser<R extends MavenInvokerRequest<? extends MavenOptions>> extends Parser<R> {
-    /**
-     * Parses the given {@link ParserRequest} to create a {@link MavenInvokerRequest}.
-     * This method is responsible for interpreting the contents of the ParserRequest
-     * and constructing the appropriate {@link MavenInvokerRequest} object for Maven operations.
-     *
-     * @param parserRequest the request containing all necessary information for parsing
-     * @return the parsed {@link MavenInvokerRequest}
-     * @throws ParserException if there's an error during parsing of the request
-     * @throws IOException if there's an I/O error during the parsing process
-     */
-    @Nonnull
-    R parse(@Nonnull ParserRequest parserRequest) throws ParserException, IOException;
-}
diff --git a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/forked/ForkedMavenInvokerRequest.java b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/forked/ForkedMavenInvokerRequest.java
deleted file mode 100644
index f8a363763f..0000000000
--- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/forked/ForkedMavenInvokerRequest.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.maven.api.cli.mvn.forked;
-
-import java.util.List;
-import java.util.Optional;
-
-import org.apache.maven.api.annotations.Experimental;
-import org.apache.maven.api.annotations.Nonnull;
-import org.apache.maven.api.cli.mvn.MavenInvokerRequest;
-import org.apache.maven.api.cli.mvn.MavenOptions;
-
-/**
- * Represents a request to invoke Maven in a forked JVM.
- * This interface extends the {@link MavenInvokerRequest}, adding capabilities specific to forked Maven executions.
- *
- * @since 4.0.0
- */
-@Experimental
-public interface ForkedMavenInvokerRequest extends MavenInvokerRequest<MavenOptions> {
-    /**
-     * Returns the list of extra JVM arguments to be passed to the forked Maven process.
-     * These arguments allow for customization of the JVM environment in which Maven will run.
-     *
-     * @return an Optional containing the list of extra JVM arguments, or empty if not specified
-     */
-    @Nonnull
-    Optional<List<String>> jvmArguments();
-}
diff --git a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/forked/ForkedMavenParser.java b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/forked/ForkedMavenParser.java
deleted file mode 100644
index 8d21f864c0..0000000000
--- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/forked/ForkedMavenParser.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.maven.api.cli.mvn.forked;
-
-import java.io.IOException;
-
-import org.apache.maven.api.annotations.Experimental;
-import org.apache.maven.api.annotations.Nonnull;
-import org.apache.maven.api.cli.ParserException;
-import org.apache.maven.api.cli.ParserRequest;
-import org.apache.maven.api.cli.mvn.MavenParser;
-
-/**
- * Defines the contract for parsing command-line arguments specific to forked Maven executions.
- * This interface extends the {@link MavenParser}, specializing it for creating {@link ForkedMavenInvokerRequest} objects.
- *
- * @since 4.0.0
- */
-@Experimental
-public interface ForkedMavenParser extends MavenParser<ForkedMavenInvokerRequest> {
-    /**
-     * Parses the given ParserRequest to create a ForkedMavenInvokerRequest.
-     * This method is responsible for interpreting the contents of the ParserRequest
-     * and constructing the appropriate ForkedMavenInvokerRequest object for forked Maven operations.
-     *
-     * @param parserRequest the request containing all necessary information for parsing
-     * @return the parsed ForkedMavenInvokerRequest
-     * @throws ParserException if there's an error during parsing of the request
-     * @throws IOException if there's an I/O error during the parsing process
-     */
-    @Nonnull
-    ForkedMavenInvokerRequest parse(@Nonnull ParserRequest parserRequest) throws ParserException, IOException;
-}
diff --git a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnenc/EncryptInvoker.java b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnenc/EncryptInvoker.java
deleted file mode 100644
index 875b4c1c01..0000000000
--- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnenc/EncryptInvoker.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.maven.api.cli.mvnenc;
-
-import org.apache.maven.api.annotations.Experimental;
-import org.apache.maven.api.cli.Invoker;
-import org.apache.maven.api.cli.InvokerException;
-
-/**
- * Defines the contract for a component responsible for invoking the Maven encryption tool.
- * This interface extends the general Invoker interface, specializing it for encryption-related operations.
- *
- * <p>The EncryptInvoker is designed to handle encryption tasks within the Maven ecosystem,
- * such as encrypting passwords or other sensitive information in Maven settings files.</p>
- *
- * @since 4.0.0
- */
-@Experimental
-public interface EncryptInvoker extends Invoker<EncryptInvokerRequest> {
-    /**
-     * Invokes the encryption tool using the provided EncryptInvokerRequest.
-     * This method is responsible for executing the encryption command or process
-     * based on the information contained in the request.
-     *
-     * @param invokerRequest the request containing all necessary information for the encryption invocation
-     * @return an integer representing the exit code of the invocation (0 typically indicates success)
-     * @throws InvokerException if an error occurs during the encryption process
-     */
-    @Override
-    int invoke(EncryptInvokerRequest invokerRequest) throws InvokerException;
-}
diff --git a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnenc/EncryptInvokerRequest.java b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnenc/EncryptInvokerRequest.java
deleted file mode 100644
index 229ee7d22c..0000000000
--- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnenc/EncryptInvokerRequest.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.maven.api.cli.mvnenc;
-
-import org.apache.maven.api.annotations.Experimental;
-import org.apache.maven.api.cli.InvokerRequest;
-
-/**
- * Represents a request to invoke the Maven encryption tool.
- * This interface extends the general InvokerRequest, specializing it for encryption-related operations.
- *
- * <p>An EncryptInvokerRequest encapsulates all the necessary information needed to perform
- * an encryption operation, including any encryption-specific options defined in EncryptOptions.</p>
- *
- * @since 4.0.0
- */
-@Experimental
-public interface EncryptInvokerRequest extends InvokerRequest<EncryptOptions> {
-    // This interface doesn't declare any additional methods beyond those inherited from InvokerRequest.
-    // It serves to type-specify the Options as EncryptOptions for encryption-related requests.
-}
diff --git a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnenc/EncryptParser.java b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnenc/EncryptParser.java
deleted file mode 100644
index bf3eb9d7e6..0000000000
--- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnenc/EncryptParser.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.maven.api.cli.mvnenc;
-
-import java.io.IOException;
-
-import org.apache.maven.api.annotations.Experimental;
-import org.apache.maven.api.annotations.Nonnull;
-import org.apache.maven.api.cli.Parser;
-import org.apache.maven.api.cli.ParserException;
-import org.apache.maven.api.cli.ParserRequest;
-
-/**
- * Defines the contract for parsing encryption-related command-line arguments and creating an EncryptInvokerRequest.
- * This interface extends the general {@link Parser} interface, specializing it for encryption operations.
- *
- * @since 4.0.0
- */
-@Experimental
-public interface EncryptParser extends Parser<EncryptInvokerRequest> {
-    /**
-     * Parses the given ParserRequest to create an EncryptInvokerRequest.
-     * This method is responsible for interpreting the contents of the ParserRequest
-     * and constructing the appropriate EncryptInvokerRequest object for encryption operations.
-     *
-     * @param parserRequest the request containing all necessary information for parsing
-     * @return the parsed EncryptInvokerRequest
-     * @throws ParserException if there's an error during parsing of the request
-     * @throws IOException if there's an I/O error during the parsing process
-     */
-    @Nonnull
-    EncryptInvokerRequest parse(@Nonnull ParserRequest parserRequest) throws ParserException, IOException;
-}
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/ClingSupport.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/ClingSupport.java
index ba65331388..3f1a207520 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/ClingSupport.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/ClingSupport.java
@@ -23,7 +23,6 @@ import java.io.IOException;
 import org.apache.maven.api.cli.Invoker;
 import org.apache.maven.api.cli.InvokerException;
 import org.apache.maven.api.cli.InvokerRequest;
-import org.apache.maven.api.cli.Options;
 import org.apache.maven.api.cli.ParserException;
 import org.codehaus.plexus.classworlds.ClassWorld;
 
@@ -31,11 +30,8 @@ import static java.util.Objects.requireNonNull;
 
 /**
  * The CLI "new-gen".
- *
- * @param <O> the options type
- * @param <R> the request type
  */
-public abstract class ClingSupport<O extends Options, R extends InvokerRequest<O>> {
+public abstract class ClingSupport {
     static final String CORE_CLASS_REALM_ID = "plexus.core";
 
     protected final ClassWorld classWorld;
@@ -64,7 +60,7 @@ public abstract class ClingSupport<O extends Options, R extends InvokerRequest<O
      * The main entry point.
      */
     public int run(String[] args) throws IOException {
-        try (Invoker<R> invoker = createInvoker()) {
+        try (Invoker invoker = createInvoker()) {
             return invoker.invoke(parseArguments(args));
         } catch (ParserException e) {
             System.err.println(e.getMessage());
@@ -78,7 +74,7 @@ public abstract class ClingSupport<O extends Options, R extends InvokerRequest<O
         }
     }
 
-    protected abstract Invoker<R> createInvoker();
+    protected abstract Invoker createInvoker();
 
-    protected abstract R parseArguments(String[] args) throws ParserException, IOException;
+    protected abstract InvokerRequest parseArguments(String[] args) throws ParserException, IOException;
 }
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/MavenCling.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/MavenCling.java
index b8b204d5f4..d4869f2186 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/MavenCling.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/MavenCling.java
@@ -21,20 +21,20 @@ package org.apache.maven.cling;
 import java.io.IOException;
 
 import org.apache.maven.api.cli.Invoker;
+import org.apache.maven.api.cli.InvokerRequest;
 import org.apache.maven.api.cli.ParserException;
-import org.apache.maven.api.cli.mvn.MavenInvokerRequest;
-import org.apache.maven.api.cli.mvn.MavenOptions;
+import org.apache.maven.api.cli.ParserRequest;
 import org.apache.maven.cling.invoker.ProtoLogger;
 import org.apache.maven.cling.invoker.ProtoLookup;
-import org.apache.maven.cling.invoker.mvn.DefaultMavenParser;
-import org.apache.maven.cling.invoker.mvn.local.DefaultLocalMavenInvoker;
+import org.apache.maven.cling.invoker.mvn.MavenParser;
+import org.apache.maven.cling.invoker.mvn.local.LocalMavenInvoker;
 import org.apache.maven.jline.JLineMessageBuilderFactory;
 import org.codehaus.plexus.classworlds.ClassWorld;
 
 /**
  * Maven CLI "new-gen".
  */
-public class MavenCling extends ClingSupport<MavenOptions, MavenInvokerRequest<MavenOptions>> {
+public class MavenCling extends ClingSupport {
     /**
      * "Normal" Java entry point. Note: Maven uses ClassWorld Launcher and this entry point is NOT used under normal
      * circumstances.
@@ -60,13 +60,15 @@ public class MavenCling extends ClingSupport<MavenOptions, MavenInvokerRequest<M
     }
 
     @Override
-    protected Invoker<MavenInvokerRequest<MavenOptions>> createInvoker() {
-        return new DefaultLocalMavenInvoker(
+    protected Invoker createInvoker() {
+        return new LocalMavenInvoker(
                 ProtoLookup.builder().addMapping(ClassWorld.class, classWorld).build());
     }
 
     @Override
-    protected MavenInvokerRequest<MavenOptions> parseArguments(String[] args) throws ParserException, IOException {
-        return new DefaultMavenParser().mvn(args, new ProtoLogger(), new JLineMessageBuilderFactory());
+    protected InvokerRequest parseArguments(String[] args) throws ParserException, IOException {
+        return new MavenParser()
+                .parseInvocation(ParserRequest.mvn(args, new ProtoLogger(), new JLineMessageBuilderFactory())
+                        .build());
     }
 }
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/MavenEncCling.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/MavenEncCling.java
index 488eb9eae4..b1f2f861ba 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/MavenEncCling.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/MavenEncCling.java
@@ -21,21 +21,20 @@ package org.apache.maven.cling;
 import java.io.IOException;
 
 import org.apache.maven.api.cli.Invoker;
+import org.apache.maven.api.cli.InvokerRequest;
 import org.apache.maven.api.cli.ParserException;
 import org.apache.maven.api.cli.ParserRequest;
-import org.apache.maven.api.cli.mvnenc.EncryptInvokerRequest;
-import org.apache.maven.api.cli.mvnenc.EncryptOptions;
 import org.apache.maven.cling.invoker.ProtoLogger;
 import org.apache.maven.cling.invoker.ProtoLookup;
-import org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker;
-import org.apache.maven.cling.invoker.mvnenc.DefaultEncryptParser;
+import org.apache.maven.cling.invoker.mvnenc.EncryptInvoker;
+import org.apache.maven.cling.invoker.mvnenc.EncryptParser;
 import org.apache.maven.jline.JLineMessageBuilderFactory;
 import org.codehaus.plexus.classworlds.ClassWorld;
 
 /**
  * Maven encrypt CLI "new-gen".
  */
-public class MavenEncCling extends ClingSupport<EncryptOptions, EncryptInvokerRequest> {
+public class MavenEncCling extends ClingSupport {
     /**
      * "Normal" Java entry point. Note: Maven uses ClassWorld Launcher and this entry point is NOT used under normal
      * circumstances.
@@ -61,15 +60,15 @@ public class MavenEncCling extends ClingSupport<EncryptOptions, EncryptInvokerRe
     }
 
     @Override
-    protected Invoker<EncryptInvokerRequest> createInvoker() {
-        return new DefaultEncryptInvoker(
+    protected Invoker createInvoker() {
+        return new EncryptInvoker(
                 ProtoLookup.builder().addMapping(ClassWorld.class, classWorld).build());
     }
 
     @Override
-    protected EncryptInvokerRequest parseArguments(String[] args) throws ParserException, IOException {
-        return new DefaultEncryptParser()
-                .parse(ParserRequest.mvnenc(args, new ProtoLogger(), new JLineMessageBuilderFactory())
+    protected InvokerRequest parseArguments(String[] args) throws ParserException, IOException {
+        return new EncryptParser()
+                .parseInvocation(ParserRequest.mvnenc(args, new ProtoLogger(), new JLineMessageBuilderFactory())
                         .build());
     }
 }
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseExecutorRequest.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseExecutorRequest.java
new file mode 100644
index 0000000000..00dbef9177
--- /dev/null
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseExecutorRequest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.maven.cling.invoker;
+
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.annotations.Nullable;
+import org.apache.maven.api.cli.ExecutorRequest;
+import org.apache.maven.api.cli.ParserRequest;
+
+import static java.util.Objects.requireNonNull;
+
+public class BaseExecutorRequest implements ExecutorRequest {
+    private final ParserRequest parserRequest;
+    private final Path cwd;
+    private final Path installationDirectory;
+    private final Path userHomeDirectory;
+    private final List<String> jvmArguments;
+
+    @SuppressWarnings("ParameterNumber")
+    public BaseExecutorRequest(
+            @Nonnull ParserRequest parserRequest,
+            @Nonnull Path cwd,
+            @Nonnull Path installationDirectory,
+            @Nonnull Path userHomeDirectory,
+            @Nullable List<String> jvmArguments) {
+        this.parserRequest = requireNonNull(parserRequest);
+        this.cwd = requireNonNull(cwd);
+        this.installationDirectory = requireNonNull(installationDirectory);
+        this.userHomeDirectory = requireNonNull(userHomeDirectory);
+        this.jvmArguments = jvmArguments;
+    }
+
+    @Override
+    public ParserRequest parserRequest() {
+        return parserRequest;
+    }
+
+    @Override
+    public Path cwd() {
+        return cwd;
+    }
+
+    @Override
+    public Path installationDirectory() {
+        return installationDirectory;
+    }
+
+    @Override
+    public Path userHomeDirectory() {
+        return userHomeDirectory;
+    }
+
+    @Override
+    public Optional<List<String>> jvmArguments() {
+        return Optional.ofNullable(jvmArguments);
+    }
+}
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseInvokerRequest.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseInvokerRequest.java
index 0b1bd847d7..25dcf754a7 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseInvokerRequest.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseInvokerRequest.java
@@ -28,26 +28,20 @@ import java.util.Optional;
 import org.apache.maven.api.annotations.Nonnull;
 import org.apache.maven.api.annotations.Nullable;
 import org.apache.maven.api.cli.InvokerRequest;
-import org.apache.maven.api.cli.Options;
 import org.apache.maven.api.cli.ParserRequest;
 import org.apache.maven.api.cli.extensions.CoreExtension;
 
 import static java.util.Objects.requireNonNull;
 
-public abstract class BaseInvokerRequest<T extends Options> implements InvokerRequest<T> {
-    private final ParserRequest parserRequest;
-    private final Path cwd;
-    private final Path installationDirectory;
-    private final Path userHomeDirectory;
+public abstract class BaseInvokerRequest extends BaseExecutorRequest implements InvokerRequest {
     private final Map<String, String> userProperties;
     private final Map<String, String> systemProperties;
     private final Path topDirectory;
     private final Path rootDirectory;
-
+    private final List<CoreExtension> coreExtensions;
     private final InputStream in;
     private final OutputStream out;
     private final OutputStream err;
-    private final List<CoreExtension> coreExtensions;
 
     @SuppressWarnings("ParameterNumber")
     public BaseInvokerRequest(
@@ -62,40 +56,18 @@ public abstract class BaseInvokerRequest<T extends Options> implements InvokerRe
             @Nullable InputStream in,
             @Nullable OutputStream out,
             @Nullable OutputStream err,
-            @Nullable List<CoreExtension> coreExtensions) {
-        this.parserRequest = requireNonNull(parserRequest);
-        this.cwd = requireNonNull(cwd);
-        this.installationDirectory = requireNonNull(installationDirectory);
-        this.userHomeDirectory = requireNonNull(userHomeDirectory);
+            @Nullable List<CoreExtension> coreExtensions,
+            @Nullable List<String> jvmArguments) {
+        super(parserRequest, cwd, installationDirectory, userHomeDirectory, jvmArguments);
         this.userProperties = requireNonNull(userProperties);
         this.systemProperties = requireNonNull(systemProperties);
         this.topDirectory = requireNonNull(topDirectory);
         this.rootDirectory = rootDirectory;
+        this.coreExtensions = coreExtensions;
 
         this.in = in;
         this.out = out;
         this.err = err;
-        this.coreExtensions = coreExtensions;
-    }
-
-    @Override
-    public ParserRequest parserRequest() {
-        return parserRequest;
-    }
-
-    @Override
-    public Path cwd() {
-        return cwd;
-    }
-
-    @Override
-    public Path installationDirectory() {
-        return installationDirectory;
-    }
-
-    @Override
-    public Path userHomeDirectory() {
-        return userHomeDirectory;
     }
 
     @Override
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseParser.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseParser.java
index 4dc2550d11..853fea00d8 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseParser.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseParser.java
@@ -33,9 +33,11 @@ import java.util.List;
 import java.util.Map;
 import java.util.Properties;
 import java.util.function.Function;
+import java.util.stream.Collectors;
 
 import org.apache.maven.api.Constants;
 import org.apache.maven.api.annotations.Nullable;
+import org.apache.maven.api.cli.ExecutorRequest;
 import org.apache.maven.api.cli.InvokerRequest;
 import org.apache.maven.api.cli.Options;
 import org.apache.maven.api.cli.Parser;
@@ -57,7 +59,7 @@ import static org.apache.maven.cling.invoker.Utils.prefix;
 import static org.apache.maven.cling.invoker.Utils.stripLeadingAndTrailingQuotes;
 import static org.apache.maven.cling.invoker.Utils.toMap;
 
-public abstract class BaseParser<O extends Options, R extends InvokerRequest<O>> implements Parser<R> {
+public abstract class BaseParser implements Parser {
 
     @SuppressWarnings("VisibilityModifier")
     public static class LocalContext {
@@ -93,7 +95,26 @@ public abstract class BaseParser<O extends Options, R extends InvokerRequest<O>>
     }
 
     @Override
-    public R parse(ParserRequest parserRequest) throws ParserException, IOException {
+    public ExecutorRequest parseExecution(ParserRequest parserRequest) throws ParserException, IOException {
+        requireNonNull(parserRequest);
+
+        LocalContext context = new LocalContext(parserRequest);
+
+        // the basics
+        context.cwd = requireNonNull(getCwd(context));
+        context.installationDirectory = requireNonNull(getInstallationDirectory(context));
+        context.userHomeDirectory = requireNonNull(getUserHomeDirectory(context));
+
+        return getExecutionRequest(context);
+    }
+
+    protected ExecutorRequest getExecutionRequest(LocalContext context) {
+        return new BaseExecutorRequest(
+                context.parserRequest, context.cwd, context.installationDirectory, context.userHomeDirectory, null);
+    }
+
+    @Override
+    public InvokerRequest parseInvocation(ParserRequest parserRequest) throws ParserException, IOException {
         requireNonNull(parserRequest);
 
         LocalContext context = new LocalContext(parserRequest);
@@ -108,7 +129,7 @@ public abstract class BaseParser<O extends Options, R extends InvokerRequest<O>>
         context.rootDirectory = getRootDirectory(context);
 
         // options
-        List<O> parsedOptions = parseCliOptions(context);
+        List<Options> parsedOptions = parseCliOptions(context);
 
         // warn about deprecated options
         PrintWriter printWriter = new PrintWriter(parserRequest.out() != null ? parserRequest.out() : System.out, true);
@@ -131,7 +152,7 @@ public abstract class BaseParser<O extends Options, R extends InvokerRequest<O>>
         return getInvokerRequest(context);
     }
 
-    protected abstract R getInvokerRequest(LocalContext context);
+    protected abstract InvokerRequest getInvokerRequest(LocalContext context);
 
     protected Path getCwd(LocalContext context) throws ParserException {
         if (context.parserRequest.cwd() != null) {
@@ -271,9 +292,9 @@ public abstract class BaseParser<O extends Options, R extends InvokerRequest<O>>
         return toMap(userProperties);
     }
 
-    protected abstract List<O> parseCliOptions(LocalContext context) throws ParserException, IOException;
+    protected abstract List<Options> parseCliOptions(LocalContext context) throws ParserException, IOException;
 
-    protected abstract O assembleOptions(List<O> parsedOptions);
+    protected abstract Options assembleOptions(List<Options> parsedOptions);
 
     protected List<CoreExtension> readCoreExtensionsDescriptor(LocalContext context)
             throws ParserException, IOException {
@@ -304,4 +325,21 @@ public abstract class BaseParser<O extends Options, R extends InvokerRequest<O>>
             throw new ParserException("Failed to parse extensions file: " + extensionsFile, e);
         }
     }
+
+    protected List<String> getJvmArguments(Path rootDirectory) throws ParserException {
+        if (rootDirectory != null) {
+            Path jvmConfig = rootDirectory.resolve(".mvn/jvm.config");
+            if (Files.exists(jvmConfig)) {
+                try {
+                    return Files.readAllLines(jvmConfig).stream()
+                            .filter(l -> !l.isBlank() && !l.startsWith("#"))
+                            .flatMap(l -> Arrays.stream(l.split(" ")))
+                            .collect(Collectors.toList());
+                } catch (IOException e) {
+                    throw new ParserException("Failed to read JVM configuration file: " + jvmConfig, e);
+                }
+            }
+        }
+        return null;
+    }
 }
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/ContainerCapsule.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/ContainerCapsule.java
index 39836fe043..a8c80ed983 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/ContainerCapsule.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/ContainerCapsule.java
@@ -18,6 +18,8 @@
  */
 package org.apache.maven.cling.invoker;
 
+import java.util.Optional;
+
 import org.apache.maven.api.annotations.Nonnull;
 import org.apache.maven.api.cli.InvokerException;
 import org.apache.maven.api.services.Lookup;
@@ -32,6 +34,12 @@ public interface ContainerCapsule extends AutoCloseable {
     @Nonnull
     Lookup getLookup();
 
+    /**
+     * The TCCL, if implementation requires it.
+     */
+    @Nonnull
+    Optional<ClassLoader> currentThreadClassLoader();
+
     /**
      * Performs a clean shutdown of backing container.
      */
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/ContainerCapsuleFactory.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/ContainerCapsuleFactory.java
index ee01e3036f..171980fea7 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/ContainerCapsuleFactory.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/ContainerCapsuleFactory.java
@@ -20,21 +20,16 @@ package org.apache.maven.cling.invoker;
 
 import org.apache.maven.api.annotations.Nonnull;
 import org.apache.maven.api.cli.InvokerException;
-import org.apache.maven.api.cli.InvokerRequest;
-import org.apache.maven.api.cli.Options;
 
 /**
  * Container capsule factory.
  *
- * @param <O> the options type
- * @param <R> the invoker request type
- * @param <C> the invoker context type
+ * @param <C> The context type.
  */
-public interface ContainerCapsuleFactory<
-        O extends Options, R extends InvokerRequest<O>, C extends LookupInvoker.LookupInvokerContext<O, R, C>> {
+public interface ContainerCapsuleFactory<C extends LookupContext> {
     /**
      * Creates container capsule.
      */
     @Nonnull
-    ContainerCapsule createContainerCapsule(C context) throws InvokerException;
+    ContainerCapsule createContainerCapsule(LookupInvoker<C> invoker, C context) throws InvokerException;
 }
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupContext.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupContext.java
new file mode 100644
index 0000000000..e5af6e8513
--- /dev/null
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupContext.java
@@ -0,0 +1,109 @@
+/*
+ * 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.maven.cling.invoker;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import org.apache.maven.api.Session;
+import org.apache.maven.api.cli.InvokerException;
+import org.apache.maven.api.cli.InvokerRequest;
+import org.apache.maven.api.cli.Logger;
+import org.apache.maven.api.services.Lookup;
+import org.apache.maven.api.settings.Settings;
+import org.apache.maven.cling.invoker.mvn.ProtoSession;
+import org.apache.maven.cling.logging.Slf4jConfiguration;
+import org.jline.terminal.Terminal;
+import org.slf4j.ILoggerFactory;
+
+import static java.util.Objects.requireNonNull;
+
+@SuppressWarnings("VisibilityModifier")
+public class LookupContext implements AutoCloseable {
+    public final InvokerRequest invokerRequest;
+    public final Function<String, Path> cwdResolver;
+    public final Function<String, Path> installationResolver;
+    public final Function<String, Path> userResolver;
+    public final Session session;
+
+    protected LookupContext(InvokerRequest invokerRequest) {
+        this.invokerRequest = requireNonNull(invokerRequest);
+        this.cwdResolver = s -> invokerRequest.cwd().resolve(s).normalize().toAbsolutePath();
+        this.installationResolver = s ->
+                invokerRequest.installationDirectory().resolve(s).normalize().toAbsolutePath();
+        this.userResolver =
+                s -> invokerRequest.userHomeDirectory().resolve(s).normalize().toAbsolutePath();
+        this.logger = invokerRequest.parserRequest().logger();
+
+        Map<String, String> user = new HashMap<>(invokerRequest.userProperties());
+        user.put("session.rootDirectory", invokerRequest.rootDirectory().toString());
+        user.put("session.topDirectory", invokerRequest.topDirectory().toString());
+        Map<String, String> system = new HashMap<>(invokerRequest.systemProperties());
+        this.session = ProtoSession.create(user, system);
+    }
+
+    public Logger logger;
+    public ILoggerFactory loggerFactory;
+    public Slf4jConfiguration slf4jConfiguration;
+    public Slf4jConfiguration.Level loggerLevel;
+    public Boolean coloredOutput;
+    public Terminal terminal;
+    public Consumer<String> writer;
+    public ContainerCapsule containerCapsule;
+    public Lookup lookup;
+
+    public boolean interactive;
+    public Path localRepositoryPath;
+    public Path installationSettingsPath;
+    public Path projectSettingsPath;
+    public Path userSettingsPath;
+    public Settings effectiveSettings;
+
+    public final List<AutoCloseable> closeables = new ArrayList<>();
+
+    @Override
+    public void close() throws InvokerException {
+        List<Exception> causes = null;
+        List<AutoCloseable> cs = new ArrayList<>(closeables);
+        Collections.reverse(cs);
+        for (AutoCloseable c : cs) {
+            if (c != null) {
+                try {
+                    c.close();
+                } catch (Exception e) {
+                    if (causes == null) {
+                        causes = new ArrayList<>();
+                    }
+                    causes.add(e);
+                }
+            }
+        }
+        if (causes != null) {
+            InvokerException exception = new InvokerException("Unable to close context");
+            causes.forEach(exception::addSuppressed);
+            throw exception;
+        }
+    }
+}
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java
index dc6ca2a698..ae229ec7c0 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java
@@ -24,8 +24,6 @@ import java.io.PrintWriter;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
@@ -35,7 +33,6 @@ import java.util.function.Consumer;
 import java.util.function.Function;
 
 import org.apache.maven.api.Constants;
-import org.apache.maven.api.Session;
 import org.apache.maven.api.cli.Invoker;
 import org.apache.maven.api.cli.InvokerException;
 import org.apache.maven.api.cli.InvokerRequest;
@@ -43,7 +40,6 @@ import org.apache.maven.api.cli.Logger;
 import org.apache.maven.api.cli.Options;
 import org.apache.maven.api.services.BuilderProblem;
 import org.apache.maven.api.services.Interpolator;
-import org.apache.maven.api.services.Lookup;
 import org.apache.maven.api.services.MavenException;
 import org.apache.maven.api.services.MessageBuilder;
 import org.apache.maven.api.services.SettingsBuilder;
@@ -62,7 +58,6 @@ import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy;
 import org.apache.maven.artifact.repository.MavenArtifactRepository;
 import org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout;
 import org.apache.maven.bridge.MavenRepositorySystem;
-import org.apache.maven.cling.invoker.mvn.ProtoSession;
 import org.apache.maven.cling.logging.Slf4jConfiguration;
 import org.apache.maven.cling.logging.Slf4jConfigurationFactory;
 import org.apache.maven.cling.transfer.ConsoleMavenTransferListener;
@@ -82,7 +77,6 @@ import org.jline.terminal.Terminal;
 import org.jline.terminal.TerminalBuilder;
 import org.jline.terminal.impl.AbstractPosixTerminal;
 import org.jline.terminal.spi.TerminalExt;
-import org.slf4j.ILoggerFactory;
 import org.slf4j.LoggerFactory;
 import org.slf4j.spi.LocationAwareLogger;
 
@@ -91,108 +85,11 @@ import static org.apache.maven.cling.invoker.Utils.toMavenExecutionRequestLoggin
 import static org.apache.maven.cling.invoker.Utils.toProperties;
 
 /**
- * Plexus invoker implementation, that boots up Plexus DI container. This class expects fully setup ClassWorld via constructor.
+ * Lookup invoker implementation, that boots up DI container.
  *
- * @param <O> the options type
- * @param <R> the request type
- * @param <C> the context type
+ * @param <C> The context type.
  */
-public abstract class LookupInvoker<
-                O extends Options, R extends InvokerRequest<O>, C extends LookupInvoker.LookupInvokerContext<O, R, C>>
-        implements Invoker<R> {
-
-    /**
-     * Exception for intentional exit: No message or anything will be displayed, just the
-     * carried exit code will be returned from {@link #invoke(InvokerRequest)} method.
-     */
-    public static final class ExitException extends InvokerException {
-        private final int exitCode;
-
-        public ExitException(int exitCode) {
-            super("EXIT");
-            this.exitCode = exitCode;
-        }
-    }
-
-    @SuppressWarnings("VisibilityModifier")
-    public static class LookupInvokerContext<
-                    O extends Options, R extends InvokerRequest<O>, C extends LookupInvokerContext<O, R, C>>
-            implements AutoCloseable {
-        public final LookupInvoker<O, R, C> invoker;
-        public final ProtoLookup protoLookup;
-        public final R invokerRequest;
-        public final Function<String, Path> cwdResolver;
-        public final Function<String, Path> installationResolver;
-        public final Function<String, Path> userResolver;
-        public final Session session;
-
-        protected LookupInvokerContext(LookupInvoker<O, R, C> invoker, R invokerRequest) {
-            this.invoker = invoker;
-            this.protoLookup = invoker.protoLookup;
-            this.invokerRequest = requireNonNull(invokerRequest);
-            this.cwdResolver = s -> invokerRequest.cwd().resolve(s).normalize().toAbsolutePath();
-            this.installationResolver = s -> invokerRequest
-                    .installationDirectory()
-                    .resolve(s)
-                    .normalize()
-                    .toAbsolutePath();
-            this.userResolver = s ->
-                    invokerRequest.userHomeDirectory().resolve(s).normalize().toAbsolutePath();
-            this.logger = invokerRequest.parserRequest().logger();
-
-            Map<String, String> user = new HashMap<>(invokerRequest.userProperties());
-            user.put("session.rootDirectory", invokerRequest.rootDirectory().toString());
-            user.put("session.topDirectory", invokerRequest.topDirectory().toString());
-            Map<String, String> system = new HashMap<>(invokerRequest.systemProperties());
-            this.session = ProtoSession.create(user, system);
-        }
-
-        public Logger logger;
-        public ILoggerFactory loggerFactory;
-        public Slf4jConfiguration slf4jConfiguration;
-        public Slf4jConfiguration.Level loggerLevel;
-        public Boolean coloredOutput;
-        public Terminal terminal;
-        public Consumer<String> writer;
-        public ClassLoader currentThreadContextClassLoader;
-        public ContainerCapsule containerCapsule;
-        public Lookup lookup;
-        public SettingsBuilder settingsBuilder;
-
-        public boolean interactive;
-        public Path localRepositoryPath;
-        public Path installationSettingsPath;
-        public Path projectSettingsPath;
-        public Path userSettingsPath;
-        public Settings effectiveSettings;
-
-        public final List<AutoCloseable> closeables = new ArrayList<>();
-
-        @Override
-        public void close() throws InvokerException {
-            List<Exception> causes = null;
-            List<AutoCloseable> cs = new ArrayList<>(closeables);
-            Collections.reverse(cs);
-            for (AutoCloseable c : cs) {
-                if (c != null) {
-                    try {
-                        c.close();
-                    } catch (Exception e) {
-                        if (causes == null) {
-                            causes = new ArrayList<>();
-                        }
-                        causes.add(e);
-                    }
-                }
-            }
-            if (causes != null) {
-                InvokerException exception = new InvokerException("Unable to close context");
-                causes.forEach(exception::addSuppressed);
-                throw exception;
-            }
-        }
-    }
-
+public abstract class LookupInvoker<C extends LookupContext> implements Invoker {
     protected final ProtoLookup protoLookup;
 
     public LookupInvoker(ProtoLookup protoLookup) {
@@ -200,19 +97,23 @@ public abstract class LookupInvoker<
     }
 
     @Override
-    public int invoke(R invokerRequest) throws InvokerException {
+    public int invoke(InvokerRequest invokerRequest) throws InvokerException {
         requireNonNull(invokerRequest);
 
         Properties oldProps = (Properties) System.getProperties().clone();
         ClassLoader oldCL = Thread.currentThread().getContextClassLoader();
         try (C context = createContext(invokerRequest)) {
             try {
-                if (context.currentThreadContextClassLoader != null) {
-                    Thread.currentThread().setContextClassLoader(context.currentThreadContextClassLoader);
+                if (context.containerCapsule != null
+                        && context.containerCapsule.currentThreadClassLoader().isPresent()) {
+                    Thread.currentThread()
+                            .setContextClassLoader(context.containerCapsule
+                                    .currentThreadClassLoader()
+                                    .get());
                 }
                 return doInvoke(context);
-            } catch (ExitException e) {
-                return e.exitCode;
+            } catch (InvokerException.ExitException e) {
+                return e.getExitCode();
             } catch (Exception e) {
                 throw handleException(context, e);
             }
@@ -238,8 +139,7 @@ public abstract class LookupInvoker<
         return execute(context);
     }
 
-    protected InvokerException handleException(LookupInvokerContext<O, R, C> context, Exception e)
-            throws InvokerException {
+    protected InvokerException handleException(C context, Exception e) throws InvokerException {
         boolean showStackTrace = context.invokerRequest.options().showErrors().orElse(false);
         if (showStackTrace) {
             context.logger.error(
@@ -255,10 +155,10 @@ public abstract class LookupInvoker<
         return new InvokerException(e.getMessage(), e);
     }
 
-    protected abstract C createContext(R invokerRequest) throws InvokerException;
+    protected abstract C createContext(InvokerRequest invokerRequest) throws InvokerException;
 
     protected void pushProperties(C context) throws Exception {
-        R invokerRequest = context.invokerRequest;
+        InvokerRequest invokerRequest = context.invokerRequest;
         HashSet<String> sys = new HashSet<>(invokerRequest.systemProperties().keySet());
         invokerRequest.userProperties().entrySet().stream()
                 .filter(k -> !sys.contains(k.getKey()))
@@ -272,7 +172,7 @@ public abstract class LookupInvoker<
     protected void prepare(C context) throws Exception {}
 
     protected void configureLogging(C context) throws Exception {
-        R invokerRequest = context.invokerRequest;
+        InvokerRequest invokerRequest = context.invokerRequest;
         // LOG COLOR
         Options mavenOptions = invokerRequest.options();
         Map<String, String> userProperties = invokerRequest.userProperties();
@@ -338,7 +238,7 @@ public abstract class LookupInvoker<
     }
 
     protected void doConfigureWithTerminal(C context, Terminal terminal) {
-        O options = context.invokerRequest.options();
+        Options options = context.invokerRequest.options();
         if (options.rawStreams().isEmpty() || !options.rawStreams().get()) {
             MavenSimpleLogger stdout = (MavenSimpleLogger) context.loggerFactory.getLogger("stdout");
             MavenSimpleLogger stderr = (MavenSimpleLogger) context.loggerFactory.getLogger("stderr");
@@ -358,7 +258,7 @@ public abstract class LookupInvoker<
     }
 
     protected Consumer<String> doDetermineWriter(C context) {
-        O options = context.invokerRequest.options();
+        Options options = context.invokerRequest.options();
         if (options.logFile().isPresent()) {
             Path logFile = context.cwdResolver.apply(options.logFile().get());
             try {
@@ -380,7 +280,7 @@ public abstract class LookupInvoker<
     }
 
     protected void activateLogging(C context) throws Exception {
-        R invokerRequest = context.invokerRequest;
+        InvokerRequest invokerRequest = context.invokerRequest;
         Options mavenOptions = invokerRequest.options();
 
         context.slf4jConfiguration.activate();
@@ -412,21 +312,21 @@ public abstract class LookupInvoker<
     }
 
     protected void helpOrVersionAndMayExit(C context) throws Exception {
-        R invokerRequest = context.invokerRequest;
+        InvokerRequest invokerRequest = context.invokerRequest;
         if (invokerRequest.options().help().isPresent()) {
             Consumer<String> writer = determineWriter(context);
             invokerRequest.options().displayHelp(context.invokerRequest.parserRequest(), writer);
-            throw new ExitException(0);
+            throw new InvokerException.ExitException(0);
         }
         if (invokerRequest.options().showVersionAndExit().isPresent()) {
             showVersion(context);
-            throw new ExitException(0);
+            throw new InvokerException.ExitException(0);
         }
     }
 
     protected void showVersion(C context) {
         Consumer<String> writer = determineWriter(context);
-        R invokerRequest = context.invokerRequest;
+        InvokerRequest invokerRequest = context.invokerRequest;
         if (invokerRequest.options().quiet().orElse(false)) {
             writer.accept(CLIReportingUtils.showVersionMinimal());
         } else if (invokerRequest.options().verbose().orElse(false)) {
@@ -466,10 +366,9 @@ public abstract class LookupInvoker<
     }
 
     protected void container(C context) throws Exception {
-        context.containerCapsule = createContainerCapsuleFactory().createContainerCapsule(context);
+        context.containerCapsule = createContainerCapsuleFactory().createContainerCapsule(this, context);
         context.closeables.add(context.containerCapsule);
         context.lookup = context.containerCapsule.getLookup();
-        context.settingsBuilder = context.lookup.lookup(SettingsBuilder.class);
 
         // refresh logger in case container got customized by spy
         org.slf4j.Logger l = context.loggerFactory.getLogger(this.getClass().getName());
@@ -478,7 +377,7 @@ public abstract class LookupInvoker<
                 .log(message);
     }
 
-    protected ContainerCapsuleFactory<O, R, C> createContainerCapsuleFactory() {
+    protected ContainerCapsuleFactory<C> createContainerCapsuleFactory() {
         return new PlexusContainerCapsuleFactory<>();
     }
 
@@ -487,7 +386,7 @@ public abstract class LookupInvoker<
     protected void init(C context) throws Exception {}
 
     protected void postCommands(C context) throws Exception {
-        R invokerRequest = context.invokerRequest;
+        InvokerRequest invokerRequest = context.invokerRequest;
         Logger logger = context.logger;
         if (invokerRequest.options().showErrors().orElse(false)) {
             logger.info("Error stacktraces are turned on.");
@@ -513,7 +412,7 @@ public abstract class LookupInvoker<
     }
 
     protected void settings(C context) throws Exception {
-        settings(context, context.settingsBuilder);
+        settings(context, context.lookup.lookup(SettingsBuilder.class));
     }
 
     protected void settings(C context, SettingsBuilder settingsBuilder) throws Exception {
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/PlexusContainerCapsule.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/PlexusContainerCapsule.java
index bd9b9507c8..05d53272d3 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/PlexusContainerCapsule.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/PlexusContainerCapsule.java
@@ -18,6 +18,8 @@
  */
 package org.apache.maven.cling.invoker;
 
+import java.util.Optional;
+
 import org.apache.maven.api.services.Lookup;
 import org.apache.maven.internal.impl.DefaultLookup;
 import org.codehaus.plexus.PlexusContainer;
@@ -43,6 +45,11 @@ public class PlexusContainerCapsule implements ContainerCapsule {
         return lookup;
     }
 
+    @Override
+    public Optional<ClassLoader> currentThreadClassLoader() {
+        return Optional.of(plexusContainer.getContainerRealm());
+    }
+
     @Override
     public void close() {
         try {
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/PlexusContainerCapsuleFactory.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/PlexusContainerCapsuleFactory.java
index 3e70d4c971..bfcef51282 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/PlexusContainerCapsuleFactory.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/PlexusContainerCapsuleFactory.java
@@ -33,7 +33,6 @@ import org.apache.maven.api.Constants;
 import org.apache.maven.api.cli.InvokerException;
 import org.apache.maven.api.cli.InvokerRequest;
 import org.apache.maven.api.cli.Logger;
-import org.apache.maven.api.cli.Options;
 import org.apache.maven.api.cli.extensions.CoreExtension;
 import org.apache.maven.api.services.MessageBuilderFactory;
 import org.apache.maven.api.services.SettingsBuilder;
@@ -65,28 +64,26 @@ import static org.apache.maven.cling.invoker.Utils.toPlexusLoggingLevel;
 /**
  * Container capsule backed by Plexus Container.
  *
- * @param <O> the options type
- * @param <R> the invoker request type
- * @param <C> the invoker context type
+ * @param <C> The context type.
  */
-public class PlexusContainerCapsuleFactory<
-                O extends Options, R extends InvokerRequest<O>, C extends LookupInvoker.LookupInvokerContext<O, R, C>>
-        implements ContainerCapsuleFactory<O, R, C> {
+public class PlexusContainerCapsuleFactory<C extends LookupContext> implements ContainerCapsuleFactory<C> {
     @Override
-    public ContainerCapsule createContainerCapsule(C context) throws InvokerException {
+    public ContainerCapsule createContainerCapsule(LookupInvoker<C> invoker, C context) throws InvokerException {
         try {
-            return new PlexusContainerCapsule(Thread.currentThread().getContextClassLoader(), container(context));
+            return new PlexusContainerCapsule(
+                    Thread.currentThread().getContextClassLoader(), container(invoker, context));
         } catch (Exception e) {
             throw new InvokerException("Failed to create plexus container capsule", e);
         }
     }
 
-    protected PlexusContainer container(C context) throws Exception {
-        ClassWorld classWorld = context.protoLookup.lookup(ClassWorld.class);
+    protected PlexusContainer container(LookupInvoker<C> invoker, C context) throws Exception {
+        ClassWorld classWorld = invoker.protoLookup.lookup(ClassWorld.class);
         ClassRealm coreRealm = classWorld.getClassRealm("plexus.core");
         List<Path> extClassPath = parseExtClasspath(context);
         CoreExtensionEntry coreEntry = CoreExtensionEntry.discoverFrom(coreRealm);
-        List<CoreExtensionEntry> extensions = loadCoreExtensions(context, coreRealm, coreEntry.getExportedArtifacts());
+        List<CoreExtensionEntry> extensions =
+                loadCoreExtensions(invoker, context, coreRealm, coreEntry.getExportedArtifacts());
         ClassRealm containerRealm =
                 setupContainerRealm(context.logger, classWorld, coreRealm, extClassPath, extensions);
         ContainerConfiguration cc = new DefaultContainerConfiguration()
@@ -108,11 +105,10 @@ public class PlexusContainerCapsuleFactory<
 
         // NOTE: To avoid inconsistencies, we'll use the TCCL exclusively for lookups
         container.setLookupRealm(null);
-        context.currentThreadContextClassLoader = container.getContainerRealm();
         Thread.currentThread().setContextClassLoader(container.getContainerRealm());
 
         container.setLoggerManager(createLoggerManager());
-        R invokerRequest = context.invokerRequest;
+        InvokerRequest invokerRequest = context.invokerRequest;
         Function<String, String> extensionSource = expression -> {
             String value = invokerRequest.userProperties().get(expression);
             if (value == null) {
@@ -193,7 +189,7 @@ public class PlexusContainerCapsuleFactory<
     protected void customizeContainer(C context, PlexusContainer container) throws Exception {}
 
     protected List<Path> parseExtClasspath(C context) throws Exception {
-        R invokerRequest = context.invokerRequest;
+        InvokerRequest invokerRequest = context.invokerRequest;
         String extClassPath = invokerRequest.userProperties().get(Constants.MAVEN_EXT_CLASS_PATH);
         if (extClassPath == null) {
             extClassPath = invokerRequest.systemProperties().get(Constants.MAVEN_EXT_CLASS_PATH);
@@ -255,8 +251,9 @@ public class PlexusContainerCapsuleFactory<
     }
 
     protected List<CoreExtensionEntry> loadCoreExtensions(
-            C context, ClassRealm containerRealm, Set<String> providedArtifacts) throws Exception {
-        R invokerRequest = context.invokerRequest;
+            LookupInvoker<C> invoker, C context, ClassRealm containerRealm, Set<String> providedArtifacts)
+            throws Exception {
+        InvokerRequest invokerRequest = context.invokerRequest;
         if (invokerRequest.coreExtensions().isEmpty()
                 || invokerRequest.coreExtensions().get().isEmpty()) {
             return Collections.emptyList();
@@ -286,10 +283,10 @@ public class PlexusContainerCapsuleFactory<
             container.getLoggerManager().setThresholds(toPlexusLoggingLevel(context.loggerLevel));
             Thread.currentThread().setContextClassLoader(container.getContainerRealm());
 
-            context.invoker.settings(context, container.lookup(SettingsBuilder.class));
+            invoker.settings(context, container.lookup(SettingsBuilder.class));
 
             MavenExecutionRequest mer = new DefaultMavenExecutionRequest();
-            context.invoker.populateRequest(context, mer);
+            invoker.populateRequest(context, mer);
             mer = container.lookup(MavenExecutionRequestPopulator.class).populateDefaults(mer);
             return Collections.unmodifiableList(container
                     .lookup(BootstrapCoreExtensionManager.class)
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/ProtoLogger.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/ProtoLogger.java
index 0f9e6dd252..7c795e8d80 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/ProtoLogger.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/ProtoLogger.java
@@ -61,7 +61,7 @@ public class ProtoLogger implements Logger {
     public void log(Level level, String message, Throwable error) {
         PrintWriter pw = level == Level.ERROR ? err : level == Level.WARN ? out : null;
         if (pw != null) {
-            pw.print(level.name() + " " + message);
+            pw.println(level.name() + " " + message);
             if (error != null) {
                 error.printStackTrace(pw);
             }
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/DefaultMavenParser.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/DefaultMavenParser.java
deleted file mode 100644
index 78db2be084..0000000000
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/DefaultMavenParser.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.maven.cling.invoker.mvn;
-
-import java.util.List;
-
-import org.apache.commons.cli.ParseException;
-import org.apache.maven.api.cli.ParserException;
-import org.apache.maven.api.cli.mvn.MavenInvokerRequest;
-import org.apache.maven.api.cli.mvn.MavenOptions;
-import org.apache.maven.api.cli.mvn.MavenParser;
-
-public class DefaultMavenParser extends BaseMavenParser<MavenOptions, MavenInvokerRequest<MavenOptions>>
-        implements MavenParser<MavenInvokerRequest<MavenOptions>> {
-    @Override
-    protected DefaultMavenInvokerRequest<MavenOptions> getInvokerRequest(LocalContext context) {
-        return new DefaultMavenInvokerRequest<>(
-                context.parserRequest,
-                context.cwd,
-                context.installationDirectory,
-                context.userHomeDirectory,
-                context.userProperties,
-                context.systemProperties,
-                context.topDirectory,
-                context.rootDirectory,
-                context.parserRequest.in(),
-                context.parserRequest.out(),
-                context.parserRequest.err(),
-                context.extensions,
-                (MavenOptions) context.options);
-    }
-
-    @Override
-    protected MavenOptions parseArgs(String source, List<String> args) throws ParserException {
-        try {
-            return CommonsCliMavenOptions.parse(source, args.toArray(new String[0]));
-        } catch (ParseException e) {
-            throw new ParserException("Failed to parse source " + source + ": " + e.getMessage(), e.getCause());
-        }
-    }
-
-    @Override
-    protected MavenOptions assembleOptions(List<MavenOptions> parsedOptions) {
-        return LayeredMavenOptions.layerMavenOptions(parsedOptions);
-    }
-}
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenContext.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenContext.java
new file mode 100644
index 0000000000..26d9c2b512
--- /dev/null
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenContext.java
@@ -0,0 +1,42 @@
+/*
+ * 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.maven.cling.invoker.mvn;
+
+import org.apache.maven.Maven;
+import org.apache.maven.api.cli.InvokerRequest;
+import org.apache.maven.api.services.model.ModelProcessor;
+import org.apache.maven.cling.invoker.LookupContext;
+import org.apache.maven.eventspy.internal.EventSpyDispatcher;
+import org.apache.maven.execution.MavenExecutionRequest;
+import org.apache.maven.execution.MavenExecutionRequestPopulator;
+import org.apache.maven.logging.BuildEventListener;
+
+@SuppressWarnings("VisibilityModifier")
+public class MavenContext extends LookupContext {
+    public MavenContext(InvokerRequest invokerRequest) {
+        super(invokerRequest);
+    }
+
+    public BuildEventListener buildEventListener;
+    public MavenExecutionRequest mavenExecutionRequest;
+    public EventSpyDispatcher eventSpyDispatcher;
+    public MavenExecutionRequestPopulator mavenExecutionRequestPopulator;
+    public ModelProcessor modelProcessor;
+    public Maven maven;
+}
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/DefaultMavenInvoker.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java
similarity index 90%
rename from impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/DefaultMavenInvoker.java
rename to impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java
index 665256d127..9fdb40c148 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/DefaultMavenInvoker.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java
@@ -34,9 +34,8 @@ import java.util.regex.Pattern;
 import org.apache.maven.InternalErrorException;
 import org.apache.maven.Maven;
 import org.apache.maven.api.Constants;
+import org.apache.maven.api.cli.InvokerRequest;
 import org.apache.maven.api.cli.Logger;
-import org.apache.maven.api.cli.mvn.MavenInvoker;
-import org.apache.maven.api.cli.mvn.MavenInvokerRequest;
 import org.apache.maven.api.cli.mvn.MavenOptions;
 import org.apache.maven.api.services.BuilderProblem;
 import org.apache.maven.api.services.SettingsBuilderRequest;
@@ -77,32 +76,13 @@ import org.eclipse.aether.transfer.TransferListener;
 import static java.util.Comparator.comparing;
 import static org.apache.maven.cling.invoker.Utils.toProperties;
 
-public abstract class DefaultMavenInvoker<
-                O extends MavenOptions,
-                R extends MavenInvokerRequest<O>,
-                C extends DefaultMavenInvoker.MavenContext<O, R, C>>
-        extends LookupInvoker<O, R, C> implements MavenInvoker<R> {
-
-    @SuppressWarnings("VisibilityModifier")
-    public static class MavenContext<
-                    O extends MavenOptions,
-                    R extends MavenInvokerRequest<O>,
-                    C extends DefaultMavenInvoker.MavenContext<O, R, C>>
-            extends LookupInvokerContext<O, R, C> {
-        protected MavenContext(DefaultMavenInvoker<O, R, C> invoker, R invokerRequest) {
-            super(invoker, invokerRequest);
-        }
-
-        public BuildEventListener buildEventListener;
-        public MavenExecutionRequest mavenExecutionRequest;
-        public EventSpyDispatcher eventSpyDispatcher;
-        public MavenExecutionRequestPopulator mavenExecutionRequestPopulator;
-        public ToolchainsBuilder toolchainsBuilder;
-        public ModelProcessor modelProcessor;
-        public Maven maven;
-    }
-
-    public DefaultMavenInvoker(ProtoLookup protoLookup) {
+/**
+ * The "local" Maven invoker, that expects whole Maven on classpath and invokes it.
+ *
+ * @param <C> The context type.
+ */
+public abstract class MavenInvoker<C extends MavenContext> extends LookupInvoker<C> {
+    public MavenInvoker(ProtoLookup protoLookup) {
         super(protoLookup);
     }
 
@@ -136,14 +116,13 @@ public abstract class DefaultMavenInvoker<
     protected void lookup(C context) throws Exception {
         context.eventSpyDispatcher = context.lookup.lookup(EventSpyDispatcher.class);
         context.mavenExecutionRequestPopulator = context.lookup.lookup(MavenExecutionRequestPopulator.class);
-        context.toolchainsBuilder = context.lookup.lookup(ToolchainsBuilder.class);
         context.modelProcessor = context.lookup.lookup(ModelProcessor.class);
         context.maven = context.lookup.lookup(Maven.class);
     }
 
     @Override
     protected void init(C context) throws Exception {
-        MavenInvokerRequest<O> invokerRequest = context.invokerRequest;
+        InvokerRequest invokerRequest = context.invokerRequest;
         Map<String, Object> data = new HashMap<>();
         data.put("plexus", context.lookup.lookup(PlexusContainer.class));
         data.put("workingDirectory", invokerRequest.cwd().toString());
@@ -157,11 +136,12 @@ public abstract class DefaultMavenInvoker<
     protected void postCommands(C context) throws Exception {
         super.postCommands(context);
 
-        R invokerRequest = context.invokerRequest;
+        InvokerRequest invokerRequest = context.invokerRequest;
+        MavenOptions options = (MavenOptions) invokerRequest.options();
         Logger logger = context.logger;
-        if (invokerRequest.options().relaxedChecksums().orElse(false)) {
+        if (options.relaxedChecksums().orElse(false)) {
             logger.info("Disabling strict checksum verification on all artifact downloads.");
-        } else if (invokerRequest.options().strictChecksums().orElse(false)) {
+        } else if (options.strictChecksums().orElse(false)) {
             logger.info("Enabling strict checksum verification on all artifact downloads.");
         }
     }
@@ -260,7 +240,8 @@ public abstract class DefaultMavenInvoker<
         context.logger.debug("Reading installation toolchains from '" + installationToolchainsFile + "'");
         context.logger.debug("Reading user toolchains from '" + userToolchainsFile + "'");
 
-        ToolchainsBuilderResult toolchainsResult = context.toolchainsBuilder.build(toolchainsRequest);
+        ToolchainsBuilderResult toolchainsResult =
+                context.lookup.lookup(ToolchainsBuilder.class).build(toolchainsRequest);
 
         context.eventSpyDispatcher.onEvent(toolchainsResult);
 
@@ -290,7 +271,7 @@ public abstract class DefaultMavenInvoker<
             request.setRootDirectory(context.invokerRequest.topDirectory());
         }
 
-        MavenOptions options = context.invokerRequest.options();
+        MavenOptions options = (MavenOptions) context.invokerRequest.options();
         request.setNoSnapshotUpdates(options.suppressSnapshotUpdates().orElse(false));
         request.setGoals(options.goals().orElse(List.of()));
         request.setReactorFailureBehavior(determineReactorFailureBehaviour(context));
@@ -346,9 +327,9 @@ public abstract class DefaultMavenInvoker<
         // parameters but this is sufficient for now. Ultimately we want components like Builders to provide a way to
         // extend the command line to accept its own configuration parameters.
         //
-        if (context.invokerRequest.options().threads().isPresent()) {
-            int degreeOfConcurrency = calculateDegreeOfConcurrency(
-                    context.invokerRequest.options().threads().get());
+        if (options.threads().isPresent()) {
+            int degreeOfConcurrency =
+                    calculateDegreeOfConcurrency(options.threads().get());
             if (degreeOfConcurrency > 1) {
                 request.setBuilderId("multithreaded");
                 request.setDegreeOfConcurrency(degreeOfConcurrency);
@@ -358,16 +339,16 @@ public abstract class DefaultMavenInvoker<
         //
         // Allow the builder to be overridden by the user if requested. The builders are now pluggable.
         //
-        if (context.invokerRequest.options().builder().isPresent()) {
-            request.setBuilderId(context.invokerRequest.options().builder().get());
+        if (options.builder().isPresent()) {
+            request.setBuilderId(options.builder().get());
         }
     }
 
     protected Path determinePom(C context) {
         Path current = context.invokerRequest.cwd();
-        if (context.invokerRequest.options().alternatePomFile().isPresent()) {
-            current = context.cwdResolver.apply(
-                    context.invokerRequest.options().alternatePomFile().get());
+        MavenOptions options = (MavenOptions) context.invokerRequest.options();
+        if (options.alternatePomFile().isPresent()) {
+            current = context.cwdResolver.apply(options.alternatePomFile().get());
         }
         if (context.modelProcessor != null) {
             return context.modelProcessor.locateExistingPom(current);
@@ -377,7 +358,7 @@ public abstract class DefaultMavenInvoker<
     }
 
     protected String determineReactorFailureBehaviour(C context) {
-        MavenOptions mavenOptions = context.invokerRequest.options();
+        MavenOptions mavenOptions = (MavenOptions) context.invokerRequest.options();
         if (mavenOptions.failFast().isPresent()) {
             return MavenExecutionRequest.REACTOR_FAIL_FAST;
         } else if (mavenOptions.failAtEnd().isPresent()) {
@@ -390,7 +371,7 @@ public abstract class DefaultMavenInvoker<
     }
 
     protected String determineGlobalChecksumPolicy(C context) {
-        MavenOptions mavenOptions = context.invokerRequest.options();
+        MavenOptions mavenOptions = (MavenOptions) context.invokerRequest.options();
         if (mavenOptions.strictChecksums().orElse(false)) {
             return MavenExecutionRequest.CHECKSUM_POLICY_FAIL;
         } else if (mavenOptions.relaxedChecksums().orElse(false)) {
@@ -415,7 +396,7 @@ public abstract class DefaultMavenInvoker<
     }
 
     protected String determineMakeBehavior(C context) {
-        MavenOptions mavenOptions = context.invokerRequest.options();
+        MavenOptions mavenOptions = (MavenOptions) context.invokerRequest.options();
         if (mavenOptions.alsoMake().isPresent()
                 && mavenOptions.alsoMakeDependents().isEmpty()) {
             return MavenExecutionRequest.REACTOR_MAKE_UPSTREAM;
@@ -431,7 +412,7 @@ public abstract class DefaultMavenInvoker<
     }
 
     protected void performProjectActivation(C context, ProjectActivation projectActivation) {
-        MavenOptions mavenOptions = context.invokerRequest.options();
+        MavenOptions mavenOptions = (MavenOptions) context.invokerRequest.options();
         if (mavenOptions.projects().isPresent()
                 && !mavenOptions.projects().get().isEmpty()) {
             List<String> optionValues = mavenOptions.projects().get();
@@ -459,7 +440,7 @@ public abstract class DefaultMavenInvoker<
     }
 
     protected void performProfileActivation(C context, ProfileActivation profileActivation) {
-        MavenOptions mavenOptions = context.invokerRequest.options();
+        MavenOptions mavenOptions = (MavenOptions) context.invokerRequest.options();
         if (mavenOptions.activatedProfiles().isPresent()
                 && !mavenOptions.activatedProfiles().get().isEmpty()) {
             List<String> optionValues = mavenOptions.activatedProfiles().get();
@@ -549,7 +530,7 @@ public abstract class DefaultMavenInvoker<
                 }
             }
 
-            if (context.invokerRequest.options().failNever().orElse(false)) {
+            if (((MavenOptions) context.invokerRequest.options()).failNever().orElse(false)) {
                 context.logger.info("Build failures were ignored.");
                 return 0;
             } else {
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/DefaultMavenInvokerRequest.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvokerRequest.java
similarity index 85%
rename from impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/DefaultMavenInvokerRequest.java
rename to impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvokerRequest.java
index 1ae5cf7b77..788c67e40f 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/DefaultMavenInvokerRequest.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvokerRequest.java
@@ -27,7 +27,6 @@ import java.util.Map;
 import org.apache.maven.api.annotations.Nonnull;
 import org.apache.maven.api.cli.ParserRequest;
 import org.apache.maven.api.cli.extensions.CoreExtension;
-import org.apache.maven.api.cli.mvn.MavenInvokerRequest;
 import org.apache.maven.api.cli.mvn.MavenOptions;
 import org.apache.maven.cling.invoker.BaseInvokerRequest;
 
@@ -35,15 +34,12 @@ import static java.util.Objects.requireNonNull;
 
 /**
  * Maven execution request.
- *
- * @param <O> the options type this request carries
  */
-public class DefaultMavenInvokerRequest<O extends MavenOptions> extends BaseInvokerRequest<O>
-        implements MavenInvokerRequest<O> {
-    private final O options;
+public class MavenInvokerRequest extends BaseInvokerRequest {
+    private final MavenOptions options;
 
     @SuppressWarnings("ParameterNumber")
-    public DefaultMavenInvokerRequest(
+    public MavenInvokerRequest(
             ParserRequest parserRequest,
             Path cwd,
             Path installationDirectory,
@@ -56,7 +52,8 @@ public class DefaultMavenInvokerRequest<O extends MavenOptions> extends BaseInvo
             OutputStream out,
             OutputStream err,
             List<CoreExtension> coreExtensions,
-            O options) {
+            List<String> jvmArguments,
+            MavenOptions options) {
         super(
                 parserRequest,
                 cwd,
@@ -69,7 +66,8 @@ public class DefaultMavenInvokerRequest<O extends MavenOptions> extends BaseInvo
                 in,
                 out,
                 err,
-                coreExtensions);
+                coreExtensions,
+                jvmArguments);
         this.options = requireNonNull(options);
     }
 
@@ -77,7 +75,7 @@ public class DefaultMavenInvokerRequest<O extends MavenOptions> extends BaseInvo
      * The mandatory Maven options.
      */
     @Nonnull
-    public O options() {
+    public MavenOptions options() {
         return options;
     }
 }
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/BaseMavenParser.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenParser.java
similarity index 55%
rename from impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/BaseMavenParser.java
rename to impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenParser.java
index d2b5752f0c..944d0a94f9 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/BaseMavenParser.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenParser.java
@@ -26,19 +26,17 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.stream.Stream;
 
+import org.apache.commons.cli.ParseException;
 import org.apache.maven.api.cli.Options;
 import org.apache.maven.api.cli.ParserException;
-import org.apache.maven.api.cli.mvn.MavenInvokerRequest;
 import org.apache.maven.api.cli.mvn.MavenOptions;
-import org.apache.maven.api.cli.mvn.MavenParser;
 import org.apache.maven.cling.invoker.BaseParser;
 
-public abstract class BaseMavenParser<O extends MavenOptions, R extends MavenInvokerRequest<O>> extends BaseParser<O, R>
-        implements MavenParser<R> {
+public class MavenParser extends BaseParser {
 
     @Override
-    protected List<O> parseCliOptions(LocalContext context) throws ParserException, IOException {
-        ArrayList<O> result = new ArrayList<>();
+    protected List<Options> parseCliOptions(LocalContext context) throws ParserException, IOException {
+        ArrayList<Options> result = new ArrayList<>();
         // CLI args
         result.add(parseMavenCliOptions(context.parserRequest.args()));
         // maven.config; if exists
@@ -49,15 +47,15 @@ public abstract class BaseMavenParser<O extends MavenOptions, R extends MavenInv
         return result;
     }
 
-    protected O parseMavenCliOptions(List<String> args) throws ParserException {
+    protected MavenOptions parseMavenCliOptions(List<String> args) throws ParserException {
         return parseArgs(Options.SOURCE_CLI, args);
     }
 
-    protected O parseMavenConfigOptions(Path configFile) throws ParserException, IOException {
+    protected MavenOptions parseMavenConfigOptions(Path configFile) throws ParserException, IOException {
         try (Stream<String> lines = Files.lines(configFile, Charset.defaultCharset())) {
             List<String> args =
                     lines.filter(arg -> !arg.isEmpty() && !arg.startsWith("#")).toList();
-            O options = parseArgs("maven.config", args);
+            MavenOptions options = parseArgs("maven.config", args);
             if (options.goals().isPresent()) {
                 // This file can only contain options, not args (goals or phases)
                 throw new ParserException("Unrecognized maven.config file entries: "
@@ -67,5 +65,36 @@ public abstract class BaseMavenParser<O extends MavenOptions, R extends MavenInv
         }
     }
 
-    protected abstract O parseArgs(String source, List<String> args) throws ParserException;
+    protected MavenOptions parseArgs(String source, List<String> args) throws ParserException {
+        try {
+            return CommonsCliMavenOptions.parse(source, args.toArray(new String[0]));
+        } catch (ParseException e) {
+            throw new ParserException("Failed to parse source " + source + ": " + e.getMessage(), e.getCause());
+        }
+    }
+
+    @Override
+    protected MavenInvokerRequest getInvokerRequest(LocalContext context) {
+        return new MavenInvokerRequest(
+                context.parserRequest,
+                context.cwd,
+                context.installationDirectory,
+                context.userHomeDirectory,
+                context.userProperties,
+                context.systemProperties,
+                context.topDirectory,
+                context.rootDirectory,
+                context.parserRequest.in(),
+                context.parserRequest.out(),
+                context.parserRequest.err(),
+                context.extensions,
+                getJvmArguments(context.rootDirectory),
+                (MavenOptions) context.options);
+    }
+
+    @Override
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    protected MavenOptions assembleOptions(List<Options> parsedOptions) {
+        return LayeredMavenOptions.layerMavenOptions((List) parsedOptions);
+    }
 }
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/embedded/EmbeddedMavenExecutor.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/embedded/EmbeddedMavenExecutor.java
new file mode 100644
index 0000000000..ff34b8c489
--- /dev/null
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/embedded/EmbeddedMavenExecutor.java
@@ -0,0 +1,267 @@
+/*
+ * 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.maven.cling.invoker.mvn.embedded;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Properties;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+import org.apache.maven.api.cli.Executor;
+import org.apache.maven.api.cli.ExecutorException;
+import org.apache.maven.api.cli.ExecutorRequest;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Embedded executor implementation, that invokes Maven from installation directory within this same JVM but in isolated
+ * classloader. This class supports Maven 4.x and Maven 3.x as well.
+ * The class world with Maven is kept in memory as long as instance of this class is not closed. Subsequent execution
+ * requests over same installation home are cached.
+ */
+public class EmbeddedMavenExecutor implements Executor {
+    protected static final class Context {
+        private final Properties properties;
+        private final URLClassLoader bootClassLoader;
+        private final String version;
+        private final Object classWorld;
+        private final ClassLoader tccl;
+        private final Function<ExecutorRequest, Integer> exec;
+
+        public Context(
+                Properties properties,
+                URLClassLoader bootClassLoader,
+                String version,
+                Object classWorld,
+                ClassLoader tccl,
+                Function<ExecutorRequest, Integer> exec) {
+            this.properties = properties;
+            this.bootClassLoader = bootClassLoader;
+            this.version = version;
+            this.classWorld = classWorld;
+            this.tccl = tccl;
+            this.exec = exec;
+        }
+    }
+
+    private final Properties originalProperties;
+    private final ClassLoader originalClassLoader;
+    private final ConcurrentHashMap<Path, Context> contexts;
+
+    public EmbeddedMavenExecutor() {
+        this.originalClassLoader = Thread.currentThread().getContextClassLoader();
+        this.contexts = new ConcurrentHashMap<>();
+        this.originalProperties = System.getProperties();
+    }
+
+    @Override
+    public int execute(ExecutorRequest executorRequest) throws ExecutorException {
+        requireNonNull(executorRequest);
+        validate(executorRequest);
+        Context context = mayCreate(executorRequest);
+
+        System.setProperties(context.properties);
+        Thread.currentThread().setContextClassLoader(context.tccl);
+        try {
+            return context.exec.apply(executorRequest);
+        } catch (Exception e) {
+            throw new ExecutorException("Failed to execute", e);
+        } finally {
+            Thread.currentThread().setContextClassLoader(originalClassLoader);
+            System.setProperties(originalProperties);
+        }
+    }
+
+    protected Context mayCreate(ExecutorRequest executorRequest) {
+        Path installation = executorRequest.installationDirectory();
+        if (!Files.isDirectory(installation)) {
+            throw new IllegalArgumentException("Installation directory must point to existing directory");
+        }
+        return contexts.computeIfAbsent(installation, k -> {
+            Path mavenHome = installation.toAbsolutePath().normalize();
+            Path boot = mavenHome.resolve("boot");
+            Path m2conf = mavenHome.resolve("bin/m2.conf");
+            if (!Files.isDirectory(boot) || !Files.isRegularFile(m2conf)) {
+                throw new IllegalArgumentException("Installation directory does not point to Maven installation");
+            }
+
+            Properties properties = new Properties();
+            properties.putAll(System.getProperties());
+            properties.put(
+                    "user.dir",
+                    executorRequest.cwd().toAbsolutePath().normalize().toString());
+            properties.put(
+                    "maven.multiModuleProjectDirectory",
+                    executorRequest.cwd().toAbsolutePath().normalize().toString());
+            properties.put(
+                    "user.home",
+                    executorRequest
+                            .userHomeDirectory()
+                            .toAbsolutePath()
+                            .normalize()
+                            .toString());
+            properties.put("maven.home", mavenHome.toString());
+            properties.put("maven.mainClass", "org.apache.maven.cling.MavenCling");
+            properties.put(
+                    "library.jline.path", mavenHome.resolve("lib/jline-native").toString());
+
+            System.setProperties(properties);
+            URLClassLoader bootClassLoader = createMavenBootClassLoader(boot, Collections.emptyList());
+            Thread.currentThread().setContextClassLoader(bootClassLoader);
+            try {
+                Class<?> launcherClass = bootClassLoader.loadClass("org.codehaus.plexus.classworlds.launcher.Launcher");
+                Object launcher = launcherClass.getDeclaredConstructor().newInstance();
+                Method configure = launcherClass.getMethod("configure", InputStream.class);
+                try (InputStream inputStream = Files.newInputStream(m2conf)) {
+                    configure.invoke(launcher, inputStream);
+                }
+                Object classWorld = launcherClass.getMethod("getWorld").invoke(launcher);
+                Class<?> cliClass =
+                        (Class<?>) launcherClass.getMethod("getMainClass").invoke(launcher);
+                String version = getMavenVersion(cliClass.getClassLoader());
+                Function<ExecutorRequest, Integer> exec;
+
+                if (version.startsWith("3.")) {
+                    // 3.x
+                    Constructor<?> newMavenCli = cliClass.getConstructor(classWorld.getClass());
+                    Object mavenCli = newMavenCli.newInstance(classWorld);
+                    Class<?>[] parameterTypes = {String[].class, String.class, PrintStream.class, PrintStream.class};
+                    Method doMain = cliClass.getMethod("doMain", parameterTypes);
+                    exec = r -> {
+                        try {
+                            return (int) doMain.invoke(mavenCli, new Object[] {
+                                r.parserRequest().args().toArray(new String[0]),
+                                r.cwd().toString(),
+                                null,
+                                null
+                            });
+                        } catch (Exception e) {
+                            throw new ExecutorException("Failed to execute", e);
+                        }
+                    };
+                } else {
+                    // assume 4.x
+                    Method mainMethod = cliClass.getMethod("main", String[].class, classWorld.getClass());
+                    exec = r -> {
+                        try {
+                            return (int) mainMethod.invoke(
+                                    null, r.parserRequest().args().toArray(new String[0]), classWorld);
+                        } catch (Exception e) {
+                            throw new ExecutorException("Failed to execute", e);
+                        }
+                    };
+                }
+
+                return new Context(properties, bootClassLoader, version, classWorld, cliClass.getClassLoader(), exec);
+            } catch (Exception e) {
+                throw new ExecutorException("Failed to create executor", e);
+            } finally {
+                Thread.currentThread().setContextClassLoader(originalClassLoader);
+                System.setProperties(originalProperties);
+            }
+        });
+    }
+
+    @Override
+    public void close() throws ExecutorException {
+        try {
+            ArrayList<Exception> exceptions = new ArrayList<>();
+            for (Context context : contexts.values()) {
+                try {
+                    doClose(context);
+                } catch (Exception e) {
+                    exceptions.add(e);
+                }
+            }
+            if (!exceptions.isEmpty()) {
+                ExecutorException e = new ExecutorException("Could not close cleanly");
+                exceptions.forEach(e::addSuppressed);
+                throw e;
+            }
+        } finally {
+            System.setProperties(originalProperties);
+        }
+    }
+
+    protected void doClose(Context context) throws Exception {
+        Thread.currentThread().setContextClassLoader(context.bootClassLoader);
+        try {
+            try {
+                ((Closeable) context.classWorld).close();
+            } finally {
+                context.bootClassLoader.close();
+            }
+        } finally {
+            Thread.currentThread().setContextClassLoader(originalClassLoader);
+        }
+    }
+
+    protected void validate(ExecutorRequest executorRequest) throws ExecutorException {}
+
+    protected URLClassLoader createMavenBootClassLoader(Path boot, List<URL> extraClasspath) {
+        ArrayList<URL> urls = new ArrayList<>(extraClasspath);
+        try (Stream<Path> stream = Files.list(boot)) {
+            stream.filter(Files::isRegularFile)
+                    .filter(p -> p.toString().endsWith(".jar"))
+                    .forEach(f -> {
+                        try {
+                            urls.add(f.toUri().toURL());
+                        } catch (MalformedURLException e) {
+                            throw new ExecutorException("Failed to build classpath: " + f, e);
+                        }
+                    });
+        } catch (IOException e) {
+            throw new ExecutorException("Failed to build classpath: " + e, e);
+        }
+        if (urls.isEmpty()) {
+            throw new IllegalArgumentException("Invalid Maven home directory; boot is empty");
+        }
+        return new URLClassLoader(
+                urls.toArray(new URL[0]), ClassLoader.getSystemClassLoader().getParent());
+    }
+
+    public String getMavenVersion(ClassLoader classLoader) throws IOException {
+        Properties props = new Properties();
+        try (InputStream is =
+                classLoader.getResourceAsStream("/META-INF/maven/org.apache.maven/maven-core/pom.properties")) {
+            if (is != null) {
+                props.load(is);
+            }
+            String version = props.getProperty("version");
+            if (version != null) {
+                return version;
+            }
+            return "unknown";
+        }
+    }
+}
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/forked/DefaultForkedMavenInvokerRequest.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/forked/DefaultForkedMavenInvokerRequest.java
deleted file mode 100644
index d62c2b77b4..0000000000
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/forked/DefaultForkedMavenInvokerRequest.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.maven.cling.invoker.mvn.forked;
-
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.file.Path;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-
-import org.apache.maven.api.cli.ParserRequest;
-import org.apache.maven.api.cli.extensions.CoreExtension;
-import org.apache.maven.api.cli.mvn.MavenOptions;
-import org.apache.maven.api.cli.mvn.forked.ForkedMavenInvokerRequest;
-import org.apache.maven.cling.invoker.mvn.DefaultMavenInvokerRequest;
-
-/**
- * Maven execution request.
- */
-public class DefaultForkedMavenInvokerRequest extends DefaultMavenInvokerRequest<MavenOptions>
-        implements ForkedMavenInvokerRequest {
-    private final List<String> jvmArguments;
-
-    @SuppressWarnings("ParameterNumber")
-    public DefaultForkedMavenInvokerRequest(
-            ParserRequest parserRequest,
-            Path cwd,
-            Path installationDirectory,
-            Path userHomeDirectory,
-            Map<String, String> userProperties,
-            Map<String, String> systemProperties,
-            Path topDirectory,
-            Path rootDirectory,
-            InputStream in,
-            OutputStream out,
-            OutputStream err,
-            List<CoreExtension> coreExtensions,
-            List<String> jvmArguments,
-            MavenOptions options) {
-        super(
-                parserRequest,
-                cwd,
-                installationDirectory,
-                userHomeDirectory,
-                userProperties,
-                systemProperties,
-                topDirectory,
-                rootDirectory,
-                in,
-                out,
-                err,
-                coreExtensions,
-                options);
-        this.jvmArguments = jvmArguments;
-    }
-
-    @Override
-    public Optional<List<String>> jvmArguments() {
-        return Optional.ofNullable(jvmArguments);
-    }
-}
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/forked/DefaultForkedMavenParser.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/forked/DefaultForkedMavenParser.java
deleted file mode 100644
index c46164e822..0000000000
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/forked/DefaultForkedMavenParser.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.maven.cling.invoker.mvn.forked;
-
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Arrays;
-import java.util.List;
-import java.util.stream.Collectors;
-
-import org.apache.commons.cli.ParseException;
-import org.apache.maven.api.cli.ParserException;
-import org.apache.maven.api.cli.mvn.MavenOptions;
-import org.apache.maven.api.cli.mvn.forked.ForkedMavenInvokerRequest;
-import org.apache.maven.api.cli.mvn.forked.ForkedMavenParser;
-import org.apache.maven.cling.invoker.mvn.BaseMavenParser;
-import org.apache.maven.cling.invoker.mvn.CommonsCliMavenOptions;
-import org.apache.maven.cling.invoker.mvn.LayeredMavenOptions;
-
-/**
- * Forked invoker that invokes Maven in a child process.
- */
-public class DefaultForkedMavenParser extends BaseMavenParser<MavenOptions, ForkedMavenInvokerRequest>
-        implements ForkedMavenParser {
-
-    @SuppressWarnings("ParameterNumber")
-    @Override
-    protected ForkedMavenInvokerRequest getInvokerRequest(LocalContext context) {
-        return new DefaultForkedMavenInvokerRequest(
-                context.parserRequest,
-                context.cwd,
-                context.installationDirectory,
-                context.userHomeDirectory,
-                context.userProperties,
-                context.systemProperties,
-                context.topDirectory,
-                context.rootDirectory,
-                context.parserRequest.in(),
-                context.parserRequest.out(),
-                context.parserRequest.err(),
-                context.extensions,
-                getJvmArguments(context.rootDirectory),
-                (MavenOptions) context.options);
-    }
-
-    protected List<String> getJvmArguments(Path rootDirectory) {
-        if (rootDirectory != null) {
-            Path jvmConfig = rootDirectory.resolve(".mvn/jvm.config");
-            if (Files.exists(jvmConfig)) {
-                try {
-                    return Files.readAllLines(jvmConfig).stream()
-                            .filter(l -> !l.isBlank() && !l.startsWith("#"))
-                            .flatMap(l -> Arrays.stream(l.split(" ")))
-                            .collect(Collectors.toList());
-                } catch (IOException e) {
-                    throw new UncheckedIOException(e);
-                }
-            }
-        }
-        return null;
-    }
-
-    // TODO: same is in DefaultMavenParser!!! (duplication)
-    @Override
-    protected MavenOptions parseArgs(String source, List<String> args) throws ParserException {
-        try {
-            return CommonsCliMavenOptions.parse(source, args.toArray(new String[0]));
-        } catch (ParseException e) {
-            throw new ParserException("Failed to parse source " + source, e.getCause());
-        }
-    }
-
-    // TODO: same is in DefaultMavenParser!!! (duplication)
-    @Override
-    protected MavenOptions assembleOptions(List<MavenOptions> parsedOptions) {
-        return LayeredMavenOptions.layerMavenOptions(parsedOptions);
-    }
-}
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/forked/ForkedMavenExecutor.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/forked/ForkedMavenExecutor.java
new file mode 100644
index 0000000000..b8e25b84df
--- /dev/null
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/forked/ForkedMavenExecutor.java
@@ -0,0 +1,73 @@
+/*
+ * 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.maven.cling.invoker.mvn.forked;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+import org.apache.maven.api.cli.Executor;
+import org.apache.maven.api.cli.ExecutorException;
+import org.apache.maven.api.cli.ExecutorRequest;
+import org.apache.maven.internal.impl.model.profile.Os;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Forked executor implementation, that spawns a subprocess with Maven from the installation directory.
+ */
+public class ForkedMavenExecutor implements Executor {
+    @Override
+    public int execute(ExecutorRequest executorRequest) throws ExecutorException {
+        requireNonNull(executorRequest);
+        validate(executorRequest);
+
+        ArrayList<String> cmdAndArguments = new ArrayList<>();
+        cmdAndArguments.add(executorRequest
+                .installationDirectory()
+                .resolve("bin")
+                .resolve(
+                        Os.IS_WINDOWS
+                                ? executorRequest.parserRequest().command() + ".cmd"
+                                : executorRequest.parserRequest().command())
+                .toString());
+
+        cmdAndArguments.addAll(executorRequest.parserRequest().args());
+
+        try {
+            ProcessBuilder pb = new ProcessBuilder()
+                    .directory(executorRequest.cwd().toFile())
+                    .command(cmdAndArguments);
+
+            if (executorRequest.jvmArguments().isPresent()) {
+                pb.environment()
+                        .put(
+                                "MAVEN_OPTS",
+                                String.join(" ", executorRequest.jvmArguments().get()));
+            }
+
+            return pb.start().waitFor();
+        } catch (IOException e) {
+            throw new ExecutorException("IO problem while executing command: " + cmdAndArguments, e);
+        } catch (InterruptedException e) {
+            throw new ExecutorException("Interrupted while executing command: " + cmdAndArguments, e);
+        }
+    }
+
+    protected void validate(ExecutorRequest executorRequest) throws ExecutorException {}
+}
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/forked/DefaultForkedMavenInvoker.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/forked/ForkedMavenInvoker.java
similarity index 94%
rename from impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/forked/DefaultForkedMavenInvoker.java
rename to impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/forked/ForkedMavenInvoker.java
index 647c4a44d9..29ad880a2f 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/forked/DefaultForkedMavenInvoker.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/forked/ForkedMavenInvoker.java
@@ -23,21 +23,21 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Map;
 
+import org.apache.maven.api.cli.Invoker;
 import org.apache.maven.api.cli.InvokerException;
+import org.apache.maven.api.cli.InvokerRequest;
 import org.apache.maven.api.cli.mvn.MavenOptions;
-import org.apache.maven.api.cli.mvn.forked.ForkedMavenInvoker;
-import org.apache.maven.api.cli.mvn.forked.ForkedMavenInvokerRequest;
 import org.apache.maven.internal.impl.model.profile.Os;
 
 import static java.util.Objects.requireNonNull;
 
 /**
- * Forked invoker implementation, it spawns a subprocess with Maven.
+ * Forked invoker implementation, that spawns a subprocess with Maven from the installation directory.
  */
-public class DefaultForkedMavenInvoker implements ForkedMavenInvoker {
+public class ForkedMavenInvoker implements Invoker {
     @SuppressWarnings("MethodLength")
     @Override
-    public int invoke(ForkedMavenInvokerRequest invokerRequest) throws InvokerException {
+    public int invoke(InvokerRequest invokerRequest) throws InvokerException {
         requireNonNull(invokerRequest);
         validate(invokerRequest);
 
@@ -51,7 +51,7 @@ public class DefaultForkedMavenInvoker implements ForkedMavenInvoker {
                                 : invokerRequest.parserRequest().command())
                 .toString());
 
-        MavenOptions mavenOptions = invokerRequest.options();
+        MavenOptions mavenOptions = (MavenOptions) invokerRequest.options();
         if (mavenOptions.userProperties().isPresent()) {
             for (Map.Entry<String, String> entry :
                     mavenOptions.userProperties().get().entrySet()) {
@@ -216,5 +216,5 @@ public class DefaultForkedMavenInvoker implements ForkedMavenInvoker {
         }
     }
 
-    protected void validate(ForkedMavenInvokerRequest invokerRequest) throws InvokerException {}
+    protected void validate(InvokerRequest invokerRequest) throws InvokerException {}
 }
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/local/DefaultLocalMavenInvoker.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/local/DefaultLocalMavenInvoker.java
deleted file mode 100644
index 0f68baccda..0000000000
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/local/DefaultLocalMavenInvoker.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.maven.cling.invoker.mvn.local;
-
-import org.apache.maven.api.cli.mvn.MavenInvokerRequest;
-import org.apache.maven.api.cli.mvn.MavenOptions;
-import org.apache.maven.api.cli.mvn.local.LocalMavenInvoker;
-import org.apache.maven.cling.invoker.ProtoLookup;
-import org.apache.maven.cling.invoker.mvn.DefaultMavenInvoker;
-
-/**
- * Local invoker implementation, when Maven CLI is being run. System uses ClassWorld launcher, and class world
- * instance is passed in via "enhanced" main method. Hence, this class expects fully setup ClassWorld via constructor.
- *
- * @see org.apache.maven.cling.MavenCling
- */
-public class DefaultLocalMavenInvoker
-        extends DefaultMavenInvoker<
-                MavenOptions, MavenInvokerRequest<MavenOptions>, DefaultLocalMavenInvoker.LocalContext>
-        implements LocalMavenInvoker {
-
-    public static class LocalContext
-            extends DefaultMavenInvoker.MavenContext<
-                    MavenOptions, MavenInvokerRequest<MavenOptions>, DefaultLocalMavenInvoker.LocalContext> {
-        protected LocalContext(DefaultLocalMavenInvoker invoker, MavenInvokerRequest<MavenOptions> invokerRequest) {
-            super(invoker, invokerRequest);
-        }
-    }
-
-    public DefaultLocalMavenInvoker(ProtoLookup protoLookup) {
-        super(protoLookup);
-    }
-
-    @Override
-    protected LocalContext createContext(MavenInvokerRequest<MavenOptions> invokerRequest) {
-        return new LocalContext(this, invokerRequest);
-    }
-}
diff --git a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/resident/ResidentMavenInvoker.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/local/LocalMavenInvoker.java
similarity index 57%
rename from api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/resident/ResidentMavenInvoker.java
rename to impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/local/LocalMavenInvoker.java
index 5743c7bb2c..3ac8cc783b 100644
--- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/resident/ResidentMavenInvoker.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/local/LocalMavenInvoker.java
@@ -16,24 +16,24 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.maven.api.cli.mvn.resident;
+package org.apache.maven.cling.invoker.mvn.local;
 
-import org.apache.maven.api.annotations.Experimental;
 import org.apache.maven.api.cli.InvokerException;
-import org.apache.maven.api.cli.mvn.MavenInvoker;
-import org.apache.maven.api.cli.mvn.MavenInvokerRequest;
-import org.apache.maven.api.cli.mvn.MavenOptions;
+import org.apache.maven.api.cli.InvokerRequest;
+import org.apache.maven.cling.invoker.ProtoLookup;
+import org.apache.maven.cling.invoker.mvn.MavenContext;
+import org.apache.maven.cling.invoker.mvn.MavenInvoker;
 
 /**
- * Resident Maven invoker, similar to local. Instance is shut down when this instance is closed.
- *
- * @since 4.0.0
+ * Local Maven invoker implementation, that expects all the Maven to be on classpath.
  */
-@Experimental
-public interface ResidentMavenInvoker extends MavenInvoker<MavenInvokerRequest<MavenOptions>> {
-    /**
-     * Closes cleanly the daemon.
-     */
+public class LocalMavenInvoker extends MavenInvoker<MavenContext> {
+    public LocalMavenInvoker(ProtoLookup protoLookup) {
+        super(protoLookup);
+    }
+
     @Override
-    void close() throws InvokerException;
+    protected MavenContext createContext(InvokerRequest invokerRequest) throws InvokerException {
+        return new MavenContext(invokerRequest);
+    }
 }
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/Goal.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/package-info.java
similarity index 82%
copy from impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/Goal.java
copy to impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/package-info.java
index d493b289e3..60eea96850 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/Goal.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/package-info.java
@@ -16,11 +16,8 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.maven.cling.invoker.mvnenc;
 
 /**
- * The mvnenc tool goal.
+ * This package contains the {@code mvn} tool implementation.
  */
-public interface Goal {
-    int execute(DefaultEncryptInvoker.LocalContext context) throws Exception;
-}
+package org.apache.maven.cling.invoker.mvn;
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/resident/DefaultResidentMavenInvoker.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/resident/DefaultResidentMavenInvoker.java
deleted file mode 100644
index 77890e9248..0000000000
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/resident/DefaultResidentMavenInvoker.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.maven.cling.invoker.mvn.resident;
-
-import java.util.ArrayList;
-import java.util.concurrent.ConcurrentHashMap;
-
-import org.apache.maven.api.cli.InvokerException;
-import org.apache.maven.api.cli.mvn.MavenInvokerRequest;
-import org.apache.maven.api.cli.mvn.MavenOptions;
-import org.apache.maven.api.cli.mvn.resident.ResidentMavenInvoker;
-import org.apache.maven.cling.invoker.ProtoLookup;
-import org.apache.maven.cling.invoker.mvn.DefaultMavenInvoker;
-
-/**
- * Local resident invoker implementation, similar to "local" but keeps Maven instance resident. This implies, that
- * things like environment, system properties, extensions etc. are loaded only once. It is caller duty to ensure
- * that subsequent call is right for the resident instance (ie no env change or different extension needed).
- */
-public class DefaultResidentMavenInvoker
-        extends DefaultMavenInvoker<
-                MavenOptions, MavenInvokerRequest<MavenOptions>, DefaultResidentMavenInvoker.LocalContext>
-        implements ResidentMavenInvoker {
-
-    public static class LocalContext
-            extends DefaultMavenInvoker.MavenContext<
-                    MavenOptions, MavenInvokerRequest<MavenOptions>, DefaultResidentMavenInvoker.LocalContext> {
-
-        protected LocalContext(DefaultResidentMavenInvoker invoker, MavenInvokerRequest<MavenOptions> invokerRequest) {
-            super(invoker, invokerRequest);
-        }
-
-        @Override
-        public void close() throws InvokerException {
-            // we are resident, we do not shut down here
-        }
-
-        public void shutDown() throws InvokerException {
-            super.close();
-        }
-
-        public LocalContext copy(MavenInvokerRequest<MavenOptions> invokerRequest) {
-            if (invokerRequest == this.invokerRequest) {
-                return this;
-            }
-            LocalContext shadow = new LocalContext((DefaultResidentMavenInvoker) invoker, invokerRequest);
-
-            shadow.logger = logger;
-            shadow.loggerFactory = loggerFactory;
-            shadow.loggerLevel = loggerLevel;
-            shadow.containerCapsule = containerCapsule;
-            shadow.lookup = lookup;
-            shadow.settingsBuilder = settingsBuilder;
-
-            shadow.interactive = interactive;
-            shadow.localRepositoryPath = localRepositoryPath;
-            shadow.installationSettingsPath = installationSettingsPath;
-            shadow.projectSettingsPath = projectSettingsPath;
-            shadow.userSettingsPath = userSettingsPath;
-            shadow.effectiveSettings = effectiveSettings;
-
-            shadow.mavenExecutionRequest = mavenExecutionRequest;
-            shadow.eventSpyDispatcher = eventSpyDispatcher;
-            shadow.mavenExecutionRequestPopulator = mavenExecutionRequestPopulator;
-            shadow.toolchainsBuilder = toolchainsBuilder;
-            shadow.modelProcessor = modelProcessor;
-            shadow.maven = maven;
-
-            return shadow;
-        }
-    }
-
-    private final ConcurrentHashMap<String, LocalContext> residentContext;
-
-    public DefaultResidentMavenInvoker(ProtoLookup protoLookup) {
-        super(protoLookup);
-        this.residentContext = new ConcurrentHashMap<>();
-    }
-
-    @Override
-    public void close() throws InvokerException {
-        ArrayList<InvokerException> exceptions = new ArrayList<>();
-        for (LocalContext context : residentContext.values()) {
-            try {
-                context.shutDown();
-            } catch (InvokerException e) {
-                exceptions.add(e);
-            }
-        }
-        if (!exceptions.isEmpty()) {
-            InvokerException exception = new InvokerException("Could not cleanly shut down context pool");
-            exceptions.forEach(exception::addSuppressed);
-            throw exception;
-        }
-    }
-
-    @Override
-    protected LocalContext createContext(MavenInvokerRequest<MavenOptions> invokerRequest) {
-        return residentContext
-                .computeIfAbsent(getContextId(invokerRequest), k -> new LocalContext(this, invokerRequest))
-                .copy(invokerRequest);
-    }
-
-    protected String getContextId(MavenInvokerRequest<MavenOptions> invokerRequest) {
-        // TODO: in a moment Maven stop pushing user properties to system properties (and maybe something more)
-        // and allow multiple instances per JVM, this may become a pool?
-        return "resident";
-    }
-
-    @Override
-    protected void container(LocalContext context) throws Exception {
-        if (context.containerCapsule == null) {
-            super.container(context);
-        }
-    }
-
-    @Override
-    protected void lookup(LocalContext context) throws Exception {
-        if (context.maven == null) {
-            super.lookup(context);
-        }
-    }
-}
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/resident/ResidentMavenContext.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/resident/ResidentMavenContext.java
new file mode 100644
index 0000000000..aa1102baa8
--- /dev/null
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/resident/ResidentMavenContext.java
@@ -0,0 +1,67 @@
+/*
+ * 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.maven.cling.invoker.mvn.resident;
+
+import org.apache.maven.api.cli.InvokerException;
+import org.apache.maven.api.cli.InvokerRequest;
+import org.apache.maven.cling.invoker.mvn.MavenContext;
+
+public class ResidentMavenContext extends MavenContext {
+
+    protected ResidentMavenContext(InvokerRequest invokerRequest) {
+        super(invokerRequest);
+    }
+
+    @Override
+    public void close() throws InvokerException {
+        // we are resident, we do not shut down here
+    }
+
+    public void shutDown() throws InvokerException {
+        super.close();
+    }
+
+    public ResidentMavenContext copy(InvokerRequest invokerRequest) {
+        if (invokerRequest == this.invokerRequest) {
+            return this;
+        }
+        ResidentMavenContext shadow = new ResidentMavenContext(invokerRequest);
+
+        shadow.logger = logger;
+        shadow.loggerFactory = loggerFactory;
+        shadow.loggerLevel = loggerLevel;
+        shadow.containerCapsule = containerCapsule;
+        shadow.lookup = lookup;
+
+        shadow.interactive = interactive;
+        shadow.localRepositoryPath = localRepositoryPath;
+        shadow.installationSettingsPath = installationSettingsPath;
+        shadow.projectSettingsPath = projectSettingsPath;
+        shadow.userSettingsPath = userSettingsPath;
+        shadow.effectiveSettings = effectiveSettings;
+
+        shadow.mavenExecutionRequest = mavenExecutionRequest;
+        shadow.eventSpyDispatcher = eventSpyDispatcher;
+        shadow.mavenExecutionRequestPopulator = mavenExecutionRequestPopulator;
+        shadow.modelProcessor = modelProcessor;
+        shadow.maven = maven;
+
+        return shadow;
+    }
+}
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/resident/ResidentMavenInvoker.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/resident/ResidentMavenInvoker.java
new file mode 100644
index 0000000000..95132cdcc9
--- /dev/null
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/resident/ResidentMavenInvoker.java
@@ -0,0 +1,86 @@
+/*
+ * 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.maven.cling.invoker.mvn.resident;
+
+import java.util.ArrayList;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.maven.api.cli.InvokerException;
+import org.apache.maven.api.cli.InvokerRequest;
+import org.apache.maven.cling.invoker.ProtoLookup;
+import org.apache.maven.cling.invoker.mvn.MavenInvoker;
+
+/**
+ * Resident invoker implementation, similar to "local", but keeps Maven instance resident. This implies, that
+ * things like environment, system properties, extensions etc. are loaded only once. It is caller duty to ensure
+ * that subsequent call is right for the resident instance (ie no env change or different extension needed).
+ */
+public class ResidentMavenInvoker extends MavenInvoker<ResidentMavenContext> {
+
+    private final ConcurrentHashMap<String, ResidentMavenContext> residentContext;
+
+    public ResidentMavenInvoker(ProtoLookup protoLookup) {
+        super(protoLookup);
+        this.residentContext = new ConcurrentHashMap<>();
+    }
+
+    @Override
+    public void close() throws InvokerException {
+        ArrayList<InvokerException> exceptions = new ArrayList<>();
+        for (ResidentMavenContext context : residentContext.values()) {
+            try {
+                context.shutDown();
+            } catch (InvokerException e) {
+                exceptions.add(e);
+            }
+        }
+        if (!exceptions.isEmpty()) {
+            InvokerException exception = new InvokerException("Could not cleanly shut down context pool");
+            exceptions.forEach(exception::addSuppressed);
+            throw exception;
+        }
+    }
+
+    @Override
+    protected ResidentMavenContext createContext(InvokerRequest invokerRequest) {
+        return residentContext
+                .computeIfAbsent(getContextId(invokerRequest), k -> new ResidentMavenContext(invokerRequest))
+                .copy(invokerRequest);
+    }
+
+    protected String getContextId(InvokerRequest invokerRequest) {
+        // TODO: in a moment Maven stop pushing user properties to system properties (and maybe something more)
+        // and allow multiple instances per JVM, this may become a pool?
+        return "resident";
+    }
+
+    @Override
+    protected void container(ResidentMavenContext context) throws Exception {
+        if (context.containerCapsule == null) {
+            super.container(context);
+        }
+    }
+
+    @Override
+    protected void lookup(ResidentMavenContext context) throws Exception {
+        if (context.maven == null) {
+            super.lookup(context);
+        }
+    }
+}
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/CommonsCliEncryptOptions.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/CommonsCliEncryptOptions.java
index 9ee6ecd1f5..7ec71cb787 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/CommonsCliEncryptOptions.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/CommonsCliEncryptOptions.java
@@ -35,6 +35,9 @@ import org.codehaus.plexus.interpolation.InterpolationException;
 
 import static org.apache.maven.cling.invoker.Utils.createInterpolator;
 
+/**
+ * Implementation of {@link EncryptOptions} (base + mvnenc).
+ */
 public class CommonsCliEncryptOptions extends CommonsCliOptions implements EncryptOptions {
     public static CommonsCliEncryptOptions parse(String[] args) throws ParseException {
         CLIManager cliManager = new CLIManager();
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/EncryptContext.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/EncryptContext.java
new file mode 100644
index 0000000000..41845f48d2
--- /dev/null
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/EncryptContext.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.maven.cling.invoker.mvnenc;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.maven.api.cli.InvokerRequest;
+import org.apache.maven.cling.invoker.LookupContext;
+import org.jline.consoleui.prompt.ConsolePrompt;
+import org.jline.reader.LineReader;
+import org.jline.utils.AttributedString;
+import org.jline.utils.AttributedStringBuilder;
+import org.jline.utils.AttributedStyle;
+
+@SuppressWarnings("VisibilityModifier")
+public class EncryptContext extends LookupContext {
+    protected EncryptContext(InvokerRequest invokerRequest) {
+        super(invokerRequest);
+    }
+
+    public Map<String, Goal> goals;
+
+    public List<AttributedString> header;
+    public AttributedStyle style;
+    public LineReader reader;
+    public ConsolePrompt prompt;
+
+    public void addInHeader(String text) {
+        addInHeader(AttributedStyle.DEFAULT, text);
+    }
+
+    public void addInHeader(AttributedStyle style, String text) {
+        AttributedStringBuilder asb = new AttributedStringBuilder();
+        asb.style(style).append(text);
+        header.add(asb.toAttributedString());
+    }
+}
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/DefaultEncryptInvoker.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/EncryptInvoker.java
similarity index 70%
rename from impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/DefaultEncryptInvoker.java
rename to impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/EncryptInvoker.java
index cbe3cd3ecd..87e5425495 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/DefaultEncryptInvoker.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/EncryptInvoker.java
@@ -20,75 +20,41 @@ package org.apache.maven.cling.invoker.mvnenc;
 
 import java.io.InterruptedIOException;
 import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
 
-import org.apache.maven.api.cli.mvnenc.EncryptInvoker;
-import org.apache.maven.api.cli.mvnenc.EncryptInvokerRequest;
+import org.apache.maven.api.cli.InvokerRequest;
 import org.apache.maven.api.cli.mvnenc.EncryptOptions;
 import org.apache.maven.cling.invoker.LookupInvoker;
 import org.apache.maven.cling.invoker.ProtoLookup;
 import org.apache.maven.cling.utils.CLIReportingUtils;
 import org.jline.consoleui.prompt.ConsolePrompt;
-import org.jline.reader.LineReader;
 import org.jline.reader.LineReaderBuilder;
 import org.jline.reader.UserInterruptException;
 import org.jline.terminal.Terminal;
-import org.jline.utils.AttributedString;
-import org.jline.utils.AttributedStringBuilder;
 import org.jline.utils.AttributedStyle;
 import org.jline.utils.Colors;
 import org.jline.utils.OSUtils;
 
 /**
- * Encrypt invoker implementation, when Encrypt CLI is being run. System uses ClassWorld launcher, and class world
- * instance is passed in via "enhanced" main method. Hence, this class expects fully setup ClassWorld via constructor.
+ * mvnenc invoker implementation.
  */
-public class DefaultEncryptInvoker
-        extends LookupInvoker<EncryptOptions, EncryptInvokerRequest, DefaultEncryptInvoker.LocalContext>
-        implements EncryptInvoker {
-
-    @SuppressWarnings("VisibilityModifier")
-    public static class LocalContext
-            extends LookupInvokerContext<EncryptOptions, EncryptInvokerRequest, DefaultEncryptInvoker.LocalContext> {
-        protected LocalContext(DefaultEncryptInvoker invoker, EncryptInvokerRequest invokerRequest) {
-            super(invoker, invokerRequest);
-        }
-
-        public Map<String, Goal> goals;
-
-        public List<AttributedString> header;
-        public AttributedStyle style;
-        public LineReader reader;
-        public ConsolePrompt prompt;
-
-        public void addInHeader(String text) {
-            addInHeader(AttributedStyle.DEFAULT, text);
-        }
-
-        public void addInHeader(AttributedStyle style, String text) {
-            AttributedStringBuilder asb = new AttributedStringBuilder();
-            asb.style(style).append(text);
-            header.add(asb.toAttributedString());
-        }
-    }
+public class EncryptInvoker extends LookupInvoker<EncryptContext> {
 
-    public DefaultEncryptInvoker(ProtoLookup protoLookup) {
+    public EncryptInvoker(ProtoLookup protoLookup) {
         super(protoLookup);
     }
 
     @Override
-    protected int execute(LocalContext context) throws Exception {
+    protected int execute(EncryptContext context) throws Exception {
         return doExecute(context);
     }
 
     @Override
-    protected LocalContext createContext(EncryptInvokerRequest invokerRequest) {
-        return new LocalContext(this, invokerRequest);
+    protected EncryptContext createContext(InvokerRequest invokerRequest) {
+        return new EncryptContext(invokerRequest);
     }
 
     @Override
-    protected void lookup(LocalContext context) {
+    protected void lookup(EncryptContext context) {
         context.goals = context.lookup.lookupMap(Goal.class);
     }
 
@@ -97,7 +63,7 @@ public class DefaultEncryptInvoker
     public static final int BAD_OPERATION = 2; // bad user input or alike
     public static final int CANCELED = 3; // user canceled
 
-    protected int doExecute(LocalContext context) throws Exception {
+    protected int doExecute(EncryptContext context) throws Exception {
         try {
             if (!context.interactive) {
                 context.terminal.writer().println("This tool works only in interactive mode!");
@@ -139,12 +105,12 @@ public class DefaultEncryptInvoker
                     LineReaderBuilder.builder().terminal(context.terminal).build();
             context.prompt = new ConsolePrompt(context.reader, context.terminal, config);
 
-            if (context.invokerRequest.options().goals().isEmpty()
-                    || context.invokerRequest.options().goals().get().size() != 1) {
+            EncryptOptions options = (EncryptOptions) context.invokerRequest.options();
+            if (options.goals().isEmpty() || options.goals().get().size() != 1) {
                 return badGoalsErrorMessage("No goal or multiple goals specified, specify only one goal.", context);
             }
 
-            String goalName = context.invokerRequest.options().goals().get().get(0);
+            String goalName = options.goals().get().get(0);
             Goal goal = context.goals.get(goalName);
 
             if (goal == null) {
@@ -168,7 +134,7 @@ public class DefaultEncryptInvoker
         }
     }
 
-    protected int badGoalsErrorMessage(String message, LocalContext context) {
+    protected int badGoalsErrorMessage(String message, EncryptContext context) {
         context.terminal.writer().println(message);
         context.terminal.writer().println("Supported goals are: " + String.join(", ", context.goals.keySet()));
         context.terminal.writer().println("Use -h to display help.");
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/DefaultEncryptInvokerRequest.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/EncryptInvokerRequest.java
similarity index 85%
rename from impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/DefaultEncryptInvokerRequest.java
rename to impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/EncryptInvokerRequest.java
index de8b52eb6f..004e64e428 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/DefaultEncryptInvokerRequest.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/EncryptInvokerRequest.java
@@ -25,20 +25,18 @@ import java.util.List;
 import java.util.Map;
 
 import org.apache.maven.api.annotations.Nonnull;
-import org.apache.maven.api.cli.Options;
 import org.apache.maven.api.cli.ParserRequest;
 import org.apache.maven.api.cli.extensions.CoreExtension;
-import org.apache.maven.api.cli.mvnenc.EncryptInvokerRequest;
 import org.apache.maven.api.cli.mvnenc.EncryptOptions;
 import org.apache.maven.cling.invoker.BaseInvokerRequest;
 
 import static java.util.Objects.requireNonNull;
 
-public class DefaultEncryptInvokerRequest extends BaseInvokerRequest<EncryptOptions> implements EncryptInvokerRequest {
+public class EncryptInvokerRequest extends BaseInvokerRequest {
     private final EncryptOptions options;
 
     @SuppressWarnings("ParameterNumber")
-    public DefaultEncryptInvokerRequest(
+    public EncryptInvokerRequest(
             ParserRequest parserRequest,
             Path cwd,
             Path installationDirectory,
@@ -51,7 +49,8 @@ public class DefaultEncryptInvokerRequest extends BaseInvokerRequest<EncryptOpti
             OutputStream out,
             OutputStream err,
             List<CoreExtension> coreExtensions,
-            Options options) {
+            List<String> jvmArguments,
+            EncryptOptions options) {
         super(
                 parserRequest,
                 cwd,
@@ -64,8 +63,9 @@ public class DefaultEncryptInvokerRequest extends BaseInvokerRequest<EncryptOpti
                 in,
                 out,
                 err,
-                coreExtensions);
-        this.options = (EncryptOptions) requireNonNull(options);
+                coreExtensions,
+                jvmArguments);
+        this.options = requireNonNull(options);
     }
 
     /**
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/DefaultEncryptParser.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/EncryptParser.java
similarity index 82%
rename from impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/DefaultEncryptParser.java
rename to impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/EncryptParser.java
index 601563c0dd..f49fa2e852 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/DefaultEncryptParser.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/EncryptParser.java
@@ -22,16 +22,15 @@ import java.util.Collections;
 import java.util.List;
 
 import org.apache.commons.cli.ParseException;
+import org.apache.maven.api.cli.Options;
 import org.apache.maven.api.cli.ParserException;
-import org.apache.maven.api.cli.mvnenc.EncryptInvokerRequest;
 import org.apache.maven.api.cli.mvnenc.EncryptOptions;
-import org.apache.maven.api.cli.mvnenc.EncryptParser;
 import org.apache.maven.cling.invoker.BaseParser;
 
-public class DefaultEncryptParser extends BaseParser<EncryptOptions, EncryptInvokerRequest> implements EncryptParser {
+public class EncryptParser extends BaseParser {
     @Override
     protected EncryptInvokerRequest getInvokerRequest(LocalContext context) {
-        return new DefaultEncryptInvokerRequest(
+        return new EncryptInvokerRequest(
                 context.parserRequest,
                 context.cwd,
                 context.installationDirectory,
@@ -44,11 +43,12 @@ public class DefaultEncryptParser extends BaseParser<EncryptOptions, EncryptInvo
                 context.parserRequest.out(),
                 context.parserRequest.err(),
                 context.extensions,
-                context.options);
+                getJvmArguments(context.rootDirectory),
+                (EncryptOptions) context.options);
     }
 
     @Override
-    protected List<EncryptOptions> parseCliOptions(LocalContext context) throws ParserException {
+    protected List<Options> parseCliOptions(LocalContext context) throws ParserException {
         return Collections.singletonList(parseEncryptCliOptions(context.parserRequest.args()));
     }
 
@@ -61,7 +61,7 @@ public class DefaultEncryptParser extends BaseParser<EncryptOptions, EncryptInvo
     }
 
     @Override
-    protected EncryptOptions assembleOptions(List<EncryptOptions> parsedOptions) {
+    protected Options assembleOptions(List<Options> parsedOptions) {
         // nothing to assemble, we deal with CLI only
         return parsedOptions.get(0);
     }
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/Goal.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/Goal.java
index d493b289e3..43d2dc8b29 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/Goal.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/Goal.java
@@ -22,5 +22,5 @@ package org.apache.maven.cling.invoker.mvnenc;
  * The mvnenc tool goal.
  */
 public interface Goal {
-    int execute(DefaultEncryptInvoker.LocalContext context) throws Exception;
+    int execute(EncryptContext context) throws Exception;
 }
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/ConfiguredGoalSupport.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/ConfiguredGoalSupport.java
index efd4792f5c..fc9a51ad84 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/ConfiguredGoalSupport.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/ConfiguredGoalSupport.java
@@ -23,10 +23,10 @@ import java.util.Map;
 import java.util.function.Consumer;
 
 import org.apache.maven.api.services.MessageBuilderFactory;
-import org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker;
+import org.apache.maven.cling.invoker.mvnenc.EncryptContext;
 import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
 
-import static org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker.ERROR;
+import static org.apache.maven.cling.invoker.mvnenc.EncryptInvoker.ERROR;
 
 /**
  * The support class for goal implementations that requires valid/workable config.
@@ -37,7 +37,7 @@ public abstract class ConfiguredGoalSupport extends GoalSupport {
     }
 
     @Override
-    public int execute(DefaultEncryptInvoker.LocalContext context) throws Exception {
+    public int execute(EncryptContext context) throws Exception {
         if (!validateConfiguration(context)) {
             context.terminal
                     .writer()
@@ -50,7 +50,7 @@ public abstract class ConfiguredGoalSupport extends GoalSupport {
         return doExecute(context);
     }
 
-    protected boolean validateConfiguration(DefaultEncryptInvoker.LocalContext context) {
+    protected boolean validateConfiguration(EncryptContext context) {
         SecDispatcher.ValidationResponse response = secDispatcher.validateConfiguration();
         if (!response.isValid() || context.invokerRequest.options().verbose().orElse(false)) {
             dumpResponse(context, "", response);
@@ -58,8 +58,7 @@ public abstract class ConfiguredGoalSupport extends GoalSupport {
         return response.isValid();
     }
 
-    protected void dumpResponse(
-            DefaultEncryptInvoker.LocalContext context, String indent, SecDispatcher.ValidationResponse response) {
+    protected void dumpResponse(EncryptContext context, String indent, SecDispatcher.ValidationResponse response) {
         context.terminal
                 .writer()
                 .println(messageBuilderFactory
@@ -100,5 +99,5 @@ public abstract class ConfiguredGoalSupport extends GoalSupport {
         }
     }
 
-    protected abstract int doExecute(DefaultEncryptInvoker.LocalContext context) throws Exception;
+    protected abstract int doExecute(EncryptContext context) throws Exception;
 }
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Decrypt.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Decrypt.java
index 43be1949e5..e58ac80bcd 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Decrypt.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Decrypt.java
@@ -23,11 +23,11 @@ import javax.inject.Named;
 import javax.inject.Singleton;
 
 import org.apache.maven.api.services.MessageBuilderFactory;
-import org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker;
+import org.apache.maven.cling.invoker.mvnenc.EncryptContext;
 import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
 
-import static org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker.BAD_OPERATION;
-import static org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker.OK;
+import static org.apache.maven.cling.invoker.mvnenc.EncryptInvoker.BAD_OPERATION;
+import static org.apache.maven.cling.invoker.mvnenc.EncryptInvoker.OK;
 
 /**
  * The "decrypt" goal.
@@ -41,7 +41,7 @@ public class Decrypt extends ConfiguredGoalSupport {
     }
 
     @Override
-    protected int doExecute(DefaultEncryptInvoker.LocalContext context) throws Exception {
+    protected int doExecute(EncryptContext context) throws Exception {
         String encrypted = context.reader.readLine("Enter the password to decrypt: ");
         if (secDispatcher.isAnyEncryptedString(encrypted)) {
             context.terminal.writer().println(secDispatcher.decrypt(encrypted));
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Diag.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Diag.java
index 1f85bbed48..700ed47bc3 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Diag.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Diag.java
@@ -23,10 +23,10 @@ import javax.inject.Named;
 import javax.inject.Singleton;
 
 import org.apache.maven.api.services.MessageBuilderFactory;
-import org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker;
+import org.apache.maven.cling.invoker.mvnenc.EncryptContext;
 import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
 
-import static org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker.OK;
+import static org.apache.maven.cling.invoker.mvnenc.EncryptInvoker.OK;
 
 /**
  * The "diag" goal.
@@ -40,7 +40,7 @@ public class Diag extends ConfiguredGoalSupport {
     }
 
     @Override
-    protected int doExecute(DefaultEncryptInvoker.LocalContext context) {
+    protected int doExecute(EncryptContext context) {
         dumpResponse(context, "", secDispatcher.validateConfiguration());
         return OK;
     }
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Encrypt.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Encrypt.java
index a3fce98d61..bbe77f54a0 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Encrypt.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Encrypt.java
@@ -23,10 +23,10 @@ import javax.inject.Named;
 import javax.inject.Singleton;
 
 import org.apache.maven.api.services.MessageBuilderFactory;
-import org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker;
+import org.apache.maven.cling.invoker.mvnenc.EncryptContext;
 import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
 
-import static org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker.OK;
+import static org.apache.maven.cling.invoker.mvnenc.EncryptInvoker.OK;
 
 /**
  * The "encrypt" goal.
@@ -40,7 +40,7 @@ public class Encrypt extends ConfiguredGoalSupport {
     }
 
     @Override
-    protected int doExecute(DefaultEncryptInvoker.LocalContext context) throws Exception {
+    protected int doExecute(EncryptContext context) throws Exception {
         String cleartext = context.reader.readLine("Enter the password to encrypt: ", '*');
         context.terminal.writer().println(secDispatcher.encrypt(cleartext, null));
         return OK;
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Init.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Init.java
index 0a98d6c0b1..3c78188376 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Init.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/goals/Init.java
@@ -26,8 +26,9 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 
+import org.apache.maven.api.cli.mvnenc.EncryptOptions;
 import org.apache.maven.api.services.MessageBuilderFactory;
-import org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker;
+import org.apache.maven.cling.invoker.mvnenc.EncryptContext;
 import org.codehaus.plexus.components.secdispatcher.DispatcherMeta;
 import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
 import org.codehaus.plexus.components.secdispatcher.model.Config;
@@ -45,8 +46,8 @@ import org.jline.reader.LineReader;
 import org.jline.reader.ParsedLine;
 import org.jline.utils.Colors;
 
-import static org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker.BAD_OPERATION;
-import static org.apache.maven.cling.invoker.mvnenc.DefaultEncryptInvoker.OK;
+import static org.apache.maven.cling.invoker.mvnenc.EncryptInvoker.BAD_OPERATION;
+import static org.apache.maven.cling.invoker.mvnenc.EncryptInvoker.OK;
 
 /**
  * The "init" goal.
@@ -62,13 +63,15 @@ public class Init extends GoalSupport {
     }
 
     @Override
-    public int execute(DefaultEncryptInvoker.LocalContext context) throws Exception {
+    public int execute(EncryptContext context) throws Exception {
         context.addInHeader(context.style.italic().bold().foreground(Colors.rgbColor("yellow")), "goal: init");
         context.addInHeader("");
 
         ConsolePrompt prompt = context.prompt;
-        boolean force = context.invokerRequest.options().force().orElse(false);
-        boolean yes = context.invokerRequest.options().yes().orElse(false);
+
+        EncryptOptions options = (EncryptOptions) context.invokerRequest.options();
+        boolean force = options.force().orElse(false);
+        boolean yes = options.yes().orElse(false);
 
         if (configExists() && !force) {
             context.terminal
@@ -238,8 +241,7 @@ public class Init extends GoalSupport {
     }
 
     private PromptBuilder configureDispatcher(
-            DefaultEncryptInvoker.LocalContext context, DispatcherMeta dispatcherMeta, PromptBuilder promptBuilder)
-            throws Exception {
+            EncryptContext context, DispatcherMeta dispatcherMeta, PromptBuilder promptBuilder) throws Exception {
         context.addInHeader(
                 context.style.italic().bold().foreground(Colors.rgbColor("yellow")),
                 "Configure " + dispatcherMeta.displayName());
diff --git a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/forked/ForkedMavenInvoker.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/package-info.java
similarity index 73%
rename from api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/forked/ForkedMavenInvoker.java
rename to impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/package-info.java
index ee75b2400b..69376e7872 100644
--- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/forked/ForkedMavenInvoker.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/package-info.java
@@ -16,15 +16,8 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.maven.api.cli.mvn.forked;
-
-import org.apache.maven.api.annotations.Experimental;
-import org.apache.maven.api.cli.mvn.MavenInvoker;
 
 /**
- * Forked Maven invoker.
- *
- * @since 4.0.0
+ * This package contains the {@code mvnenc} tool implementation.
  */
-@Experimental
-public interface ForkedMavenInvoker extends MavenInvoker<ForkedMavenInvokerRequest> {}
+package org.apache.maven.cling.invoker.mvnenc;
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/Goal.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/package-info.java
similarity index 81%
copy from impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/Goal.java
copy to impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/package-info.java
index d493b289e3..b169eba8db 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/Goal.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/package-info.java
@@ -16,11 +16,9 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.maven.cling.invoker.mvnenc;
 
 /**
- * The mvnenc tool goal.
+ * This package contain support (mostly abstract) classes, that implement "base" of CLIng.
+ * In packages below you find actual implementations.
  */
-public interface Goal {
-    int execute(DefaultEncryptInvoker.LocalContext context) throws Exception;
-}
+package org.apache.maven.cling.invoker;
diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenExecutorTestSupport.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenExecutorTestSupport.java
new file mode 100644
index 0000000000..c3c487dcc1
--- /dev/null
+++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenExecutorTestSupport.java
@@ -0,0 +1,64 @@
+/*
+ * 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.maven.cling.invoker.mvn;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.maven.api.cli.Executor;
+import org.apache.maven.api.cli.Parser;
+import org.apache.maven.api.cli.ParserRequest;
+import org.apache.maven.cling.invoker.ProtoLogger;
+import org.apache.maven.jline.JLineMessageBuilderFactory;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public abstract class MavenExecutorTestSupport {
+
+    protected void execute(Path cwd, Collection<String> goals) throws Exception {
+        Files.createDirectory(cwd.resolve(".mvn"));
+        Path pom = cwd.resolve("pom.xml").toAbsolutePath();
+        Files.writeString(pom, MavenTestSupport.POM_STRING);
+        Path appJava = cwd.resolve("src/main/java/org/apache/maven/samples/sample/App.java");
+        Files.createDirectories(appJava.getParent());
+        Files.writeString(appJava, MavenTestSupport.APP_JAVA_STRING);
+
+        Parser parser = createParser();
+        try (Executor invoker = createExecutor()) {
+            for (String goal : goals) {
+                Path logFile = cwd.resolve(goal + "-build.log").toAbsolutePath();
+                int exitCode = invoker.execute(parser.parseExecution(ParserRequest.mvn(
+                                List.of("-l", logFile.toString(), goal),
+                                new ProtoLogger(),
+                                new JLineMessageBuilderFactory())
+                        .cwd(cwd)
+                        .build()));
+                String log = Files.readString(logFile);
+                System.out.println(log);
+                assertEquals(0, exitCode, log);
+            }
+        }
+    }
+
+    protected abstract Executor createExecutor();
+
+    protected abstract Parser createParser();
+}
diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenInvokerTestSupport.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenInvokerTestSupport.java
index e2dd847053..d3dcf3fc9a 100644
--- a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenInvokerTestSupport.java
+++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenInvokerTestSupport.java
@@ -27,15 +27,13 @@ import java.util.List;
 import org.apache.maven.api.cli.Invoker;
 import org.apache.maven.api.cli.Parser;
 import org.apache.maven.api.cli.ParserRequest;
-import org.apache.maven.api.cli.mvn.MavenInvokerRequest;
-import org.apache.maven.api.cli.mvn.MavenOptions;
 import org.apache.maven.cling.invoker.ProtoLogger;
 import org.apache.maven.jline.JLineMessageBuilderFactory;
 import org.junit.jupiter.api.Assumptions;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
-public abstract class MavenInvokerTestSupport<O extends MavenOptions, R extends MavenInvokerRequest<O>> {
+public abstract class MavenInvokerTestSupport {
 
     protected void invoke(Path cwd, Collection<String> goals) throws Exception {
         // works only in recent Maven4
@@ -46,75 +44,30 @@ public abstract class MavenInvokerTestSupport<O extends MavenOptions, R extends
                 "${maven.home}/conf/maven.properties must be a file");
 
         Files.createDirectory(cwd.resolve(".mvn"));
-
-        String pomString =
-                """
-                    <?xml version="1.0" encoding="UTF-8"?>
-                    <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 https://maven.apache.org/maven-v4_0_0.xsd">
-
-                        <modelVersion>4.0.0</modelVersion>
-
-                        <groupId>org.apache.maven.samples</groupId>
-                        <artifactId>sample</artifactId>
-                        <version>1.0.0</version>
-
-                        <dependencyManagement>
-                          <dependencies>
-                            <dependency>
-                              <groupId>org.junit</groupId>
-                              <artifactId>junit-bom</artifactId>
-                              <version>5.11.1</version>
-                              <type>pom</type>
-                              <scope>import</scope>
-                            </dependency>
-                          </dependencies>
-                        </dependencyManagement>
-
-                        <dependencies>
-                          <dependency>
-                            <groupId>org.junit.jupiter</groupId>
-                            <artifactId>junit-jupiter-api</artifactId>
-                            <scope>test</scope>
-                          </dependency>
-                        </dependencies>
-
-                    </project>
-                    """;
         Path pom = cwd.resolve("pom.xml").toAbsolutePath();
-        Files.writeString(pom, pomString);
-
-        String appJavaString =
-                """
-                package org.apache.maven.samples.sample;
-
-                public class App {
-                    public static void main(String... args) {
-                        System.out.println("Hello World!");
-                    }
-                }
-                """;
+        Files.writeString(pom, MavenTestSupport.POM_STRING);
         Path appJava = cwd.resolve("src/main/java/org/apache/maven/samples/sample/App.java");
         Files.createDirectories(appJava.getParent());
-        Files.writeString(appJava, appJavaString);
+        Files.writeString(appJava, MavenTestSupport.APP_JAVA_STRING);
 
-        Parser<R> parser = createParser();
-        try (Invoker<R> invoker = createInvoker()) {
+        Parser parser = createParser();
+        try (Invoker invoker = createInvoker()) {
             for (String goal : goals) {
                 Path logFile = cwd.resolve(goal + "-build.log").toAbsolutePath();
-                int exitCode = invoker.invoke(parser.parse(ParserRequest.mvn(
+                int exitCode = invoker.invoke(parser.parseInvocation(ParserRequest.mvn(
                                 List.of("-l", logFile.toString(), goal),
                                 new ProtoLogger(),
                                 new JLineMessageBuilderFactory())
                         .cwd(cwd)
                         .build()));
                 String log = Files.readString(logFile);
+                System.out.println(log);
                 assertEquals(0, exitCode, log);
             }
         }
     }
 
-    protected abstract Invoker<R> createInvoker();
+    protected abstract Invoker createInvoker();
 
-    protected abstract Parser<R> createParser();
+    protected abstract Parser createParser();
 }
diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenTestSupport.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenTestSupport.java
new file mode 100644
index 0000000000..9c1ebeff36
--- /dev/null
+++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenTestSupport.java
@@ -0,0 +1,69 @@
+/*
+ * 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.maven.cling.invoker.mvn;
+
+public final class MavenTestSupport {
+    private MavenTestSupport() {}
+
+    public static final String POM_STRING =
+            """
+                <?xml version="1.0" encoding="UTF-8"?>
+                <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 https://maven.apache.org/maven-v4_0_0.xsd">
+
+                    <modelVersion>4.0.0</modelVersion>
+
+                    <groupId>org.apache.maven.samples</groupId>
+                    <artifactId>sample</artifactId>
+                    <version>1.0.0</version>
+
+                    <dependencyManagement>
+                      <dependencies>
+                        <dependency>
+                          <groupId>org.junit</groupId>
+                          <artifactId>junit-bom</artifactId>
+                          <version>5.11.1</version>
+                          <type>pom</type>
+                          <scope>import</scope>
+                        </dependency>
+                      </dependencies>
+                    </dependencyManagement>
+
+                    <dependencies>
+                      <dependency>
+                        <groupId>org.junit.jupiter</groupId>
+                        <artifactId>junit-jupiter-api</artifactId>
+                        <scope>test</scope>
+                      </dependency>
+                    </dependencies>
+
+                </project>
+                """;
+
+    public static final String APP_JAVA_STRING =
+            """
+            package org.apache.maven.samples.sample;
+
+            public class App {
+                public static void main(String... args) {
+                    System.out.println("Hello World!");
+                }
+            }
+            """;
+}
diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/forked/DefaultForkedMavenInvokerTest.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/embedded/EmbeddedMavenExecutorTest.java
similarity index 53%
copy from impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/forked/DefaultForkedMavenInvokerTest.java
copy to impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/embedded/EmbeddedMavenExecutorTest.java
index f03c161d4c..7199d43fdd 100644
--- a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/forked/DefaultForkedMavenInvokerTest.java
+++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/embedded/EmbeddedMavenExecutorTest.java
@@ -16,16 +16,16 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.maven.cling.invoker.mvn.forked;
+package org.apache.maven.cling.invoker.mvn.embedded;
 
 import java.nio.file.Path;
-import java.util.Arrays;
+import java.util.List;
 
-import org.apache.maven.api.cli.Invoker;
+import org.apache.maven.api.cli.Executor;
 import org.apache.maven.api.cli.Parser;
-import org.apache.maven.api.cli.mvn.MavenOptions;
-import org.apache.maven.api.cli.mvn.forked.ForkedMavenInvokerRequest;
-import org.apache.maven.cling.invoker.mvn.MavenInvokerTestSupport;
+import org.apache.maven.cling.invoker.mvn.MavenExecutorTestSupport;
+import org.apache.maven.cling.invoker.mvn.MavenParser;
+import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.CleanupMode;
 import org.junit.jupiter.api.io.TempDir;
@@ -33,20 +33,29 @@ import org.junit.jupiter.api.io.TempDir;
 /**
  * Forked UT: it cannot use jimFS as it runs in child process.
  */
-public class DefaultForkedMavenInvokerTest extends MavenInvokerTestSupport<MavenOptions, ForkedMavenInvokerRequest> {
+@Disabled(
+        "The tests reuse properties from the JVM being launched, thus may lead to failures depending on which options are used")
+public class EmbeddedMavenExecutorTest extends MavenExecutorTestSupport {
 
     @Override
-    protected Invoker<ForkedMavenInvokerRequest> createInvoker() {
-        return new DefaultForkedMavenInvoker();
+    protected Executor createExecutor() {
+        return new EmbeddedMavenExecutor();
     }
 
     @Override
-    protected Parser<ForkedMavenInvokerRequest> createParser() {
-        return new DefaultForkedMavenParser();
+    protected Parser createParser() {
+        return new MavenParser();
     }
 
     @Test
     void defaultFs(@TempDir(cleanup = CleanupMode.ON_SUCCESS) Path tempDir) throws Exception {
-        invoke(tempDir, Arrays.asList("clean", "verify"));
+        System.setProperty("maven.home", "/home/cstamas/Tools/maven/apache-maven-4.0.0-beta-6-SNAPSHOT");
+        execute(tempDir, List.of("verify"));
+    }
+
+    @Test
+    void defaultFs3x(@TempDir(cleanup = CleanupMode.ON_SUCCESS) Path tempDir) throws Exception {
+        System.setProperty("maven.home", "/home/cstamas/.sdkman/candidates/maven/3.9.9");
+        execute(tempDir, List.of("verify"));
     }
 }
diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/forked/DefaultForkedMavenInvokerTest.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/forked/ForkedMavenInvokerTest.java
similarity index 76%
rename from impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/forked/DefaultForkedMavenInvokerTest.java
rename to impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/forked/ForkedMavenInvokerTest.java
index f03c161d4c..01ddfe3267 100644
--- a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/forked/DefaultForkedMavenInvokerTest.java
+++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/forked/ForkedMavenInvokerTest.java
@@ -23,9 +23,9 @@ import java.util.Arrays;
 
 import org.apache.maven.api.cli.Invoker;
 import org.apache.maven.api.cli.Parser;
-import org.apache.maven.api.cli.mvn.MavenOptions;
-import org.apache.maven.api.cli.mvn.forked.ForkedMavenInvokerRequest;
 import org.apache.maven.cling.invoker.mvn.MavenInvokerTestSupport;
+import org.apache.maven.cling.invoker.mvn.MavenParser;
+import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.CleanupMode;
 import org.junit.jupiter.api.io.TempDir;
@@ -33,16 +33,18 @@ import org.junit.jupiter.api.io.TempDir;
 /**
  * Forked UT: it cannot use jimFS as it runs in child process.
  */
-public class DefaultForkedMavenInvokerTest extends MavenInvokerTestSupport<MavenOptions, ForkedMavenInvokerRequest> {
+@Disabled(
+        "The tests reuse properties from the JVM being launched, thus may lead to failures depending on which options are used")
+public class ForkedMavenInvokerTest extends MavenInvokerTestSupport {
 
     @Override
-    protected Invoker<ForkedMavenInvokerRequest> createInvoker() {
-        return new DefaultForkedMavenInvoker();
+    protected Invoker createInvoker() {
+        return new ForkedMavenInvoker();
     }
 
     @Override
-    protected Parser<ForkedMavenInvokerRequest> createParser() {
-        return new DefaultForkedMavenParser();
+    protected Parser createParser() {
+        return new MavenParser();
     }
 
     @Test
diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/local/DefaultLocalMavenInvokerTest.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/local/DefaultLocalMavenInvokerTest.java
index 904ac1cff0..d4389788fa 100644
--- a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/local/DefaultLocalMavenInvokerTest.java
+++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/local/DefaultLocalMavenInvokerTest.java
@@ -26,11 +26,9 @@ import com.google.common.jimfs.Configuration;
 import com.google.common.jimfs.Jimfs;
 import org.apache.maven.api.cli.Invoker;
 import org.apache.maven.api.cli.Parser;
-import org.apache.maven.api.cli.mvn.MavenInvokerRequest;
-import org.apache.maven.api.cli.mvn.MavenOptions;
 import org.apache.maven.cling.invoker.ProtoLookup;
-import org.apache.maven.cling.invoker.mvn.DefaultMavenParser;
 import org.apache.maven.cling.invoker.mvn.MavenInvokerTestSupport;
+import org.apache.maven.cling.invoker.mvn.MavenParser;
 import org.codehaus.plexus.classworlds.ClassWorld;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
@@ -42,18 +40,17 @@ import org.junit.jupiter.api.io.TempDir;
  */
 @Disabled(
         "The tests reuse properties from the JVM being launched, thus may lead to failures depending on which options are used")
-public class DefaultLocalMavenInvokerTest
-        extends MavenInvokerTestSupport<MavenOptions, MavenInvokerRequest<MavenOptions>> {
+public class DefaultLocalMavenInvokerTest extends MavenInvokerTestSupport {
     @Override
-    protected Invoker<MavenInvokerRequest<MavenOptions>> createInvoker() {
-        return new DefaultLocalMavenInvoker(ProtoLookup.builder()
+    protected Invoker createInvoker() {
+        return new LocalMavenInvoker(ProtoLookup.builder()
                 .addMapping(ClassWorld.class, new ClassWorld("plexus.core", ClassLoader.getSystemClassLoader()))
                 .build());
     }
 
     @Override
-    protected Parser<MavenInvokerRequest<MavenOptions>> createParser() {
-        return new DefaultMavenParser();
+    protected Parser createParser() {
+        return new MavenParser();
     }
 
     @Test
diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/resident/DefaultResidentMavenInvokerTest.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/resident/DefaultResidentMavenInvokerTest.java
index 73485b9797..de65db9503 100644
--- a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/resident/DefaultResidentMavenInvokerTest.java
+++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/resident/DefaultResidentMavenInvokerTest.java
@@ -26,11 +26,9 @@ import com.google.common.jimfs.Configuration;
 import com.google.common.jimfs.Jimfs;
 import org.apache.maven.api.cli.Invoker;
 import org.apache.maven.api.cli.Parser;
-import org.apache.maven.api.cli.mvn.MavenInvokerRequest;
-import org.apache.maven.api.cli.mvn.MavenOptions;
 import org.apache.maven.cling.invoker.ProtoLookup;
-import org.apache.maven.cling.invoker.mvn.DefaultMavenParser;
 import org.apache.maven.cling.invoker.mvn.MavenInvokerTestSupport;
+import org.apache.maven.cling.invoker.mvn.MavenParser;
 import org.codehaus.plexus.classworlds.ClassWorld;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
@@ -42,19 +40,18 @@ import org.junit.jupiter.api.io.TempDir;
  */
 @Disabled(
         "The tests reuse properties from the JVM being launched, thus may lead to failures depending on which options are used")
-public class DefaultResidentMavenInvokerTest
-        extends MavenInvokerTestSupport<MavenOptions, MavenInvokerRequest<MavenOptions>> {
+public class DefaultResidentMavenInvokerTest extends MavenInvokerTestSupport {
 
     @Override
-    protected Invoker<MavenInvokerRequest<MavenOptions>> createInvoker() {
-        return new DefaultResidentMavenInvoker(ProtoLookup.builder()
+    protected Invoker createInvoker() {
+        return new ResidentMavenInvoker(ProtoLookup.builder()
                 .addMapping(ClassWorld.class, new ClassWorld("plexus.core", ClassLoader.getSystemClassLoader()))
                 .build());
     }
 
     @Override
-    protected Parser<MavenInvokerRequest<MavenOptions>> createParser() {
-        return new DefaultMavenParser();
+    protected Parser createParser() {
+        return new MavenParser();
     }
 
     @Test