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 49825e6dba [MNG-8437] mvnsh (#1982)
49825e6dba is described below

commit 49825e6dbad98a8a9ed037b61e9d324658a7994a
Author: Tamas Cservenak <ta...@cservenak.net>
AuthorDate: Tue Dec 17 10:50:45 2024 +0100

    [MNG-8437] mvnsh (#1982)
    
    Maven shell
    
    Changes:
    * (unrelated) contains fix for property handling in embedded executor and 
in lookup invoker
    * pulled `-o` (offline) option to "generic" Options from MavenOptions
    * introduce mvnsh (scripts, options, CLIng main class)
    * simplified invokers (only one context needed for maven now)
    * invokers made "reentrant", it all depends HOW context is created
    
    Related PRs:
    * mvnd changes https://github.com/apache/maven-mvnd/pull/1228
    
    ---
    
    https://issues.apache.org/jira/browse/MNG-8437
---
 apache-maven/src/assembly/component.xml            |   1 +
 apache-maven/src/assembly/maven/bin/mvn            |   3 +
 apache-maven/src/assembly/maven/bin/mvn.cmd        |   2 +
 apache-maven/src/assembly/maven/bin/mvnsh          |  30 +++
 apache-maven/src/assembly/maven/bin/mvnsh.cmd      |  39 ++++
 .../java/org/apache/maven/api/cli/Options.java     |   8 +
 .../org/apache/maven/api/cli/ParserRequest.java    |  28 +++
 .../main/java/org/apache/maven/api/cli/Tools.java  |   3 +
 .../org/apache/maven/api/cli/mvn/MavenOptions.java |   8 -
 .../cli/{Tools.java => mvnsh/ShellOptions.java}    |  29 +--
 .../java/org/apache/maven/cling/MavenCling.java    |   4 +-
 .../{MavenCling.java => MavenShellCling.java}      |  22 +--
 .../extensions/BootstrapCoreExtensionManager.java  |   7 +-
 .../maven/cling/invoker/CommonsCliOptions.java     |  18 ++
 .../apache/maven/cling/invoker/LayeredOptions.java |   5 +
 .../apache/maven/cling/invoker/LookupContext.java  |  21 +-
 .../apache/maven/cling/invoker/LookupInvoker.java  | 120 ++++++-----
 .../cling/invoker/mvn/CommonsCliMavenOptions.java  |  13 --
 .../cling/invoker/mvn/LayeredMavenOptions.java     |   5 -
 .../maven/cling/invoker/mvn/MavenContext.java      |  18 +-
 .../maven/cling/invoker/mvn/MavenInvoker.java      |  98 ++++-----
 .../invoker/mvn/forked/ForkedMavenInvoker.java     | 220 ---------------------
 .../invoker/mvn/resident/ResidentMavenContext.java |  54 -----
 .../invoker/mvn/resident/ResidentMavenInvoker.java |  55 +++---
 .../maven/cling/invoker/mvnenc/EncryptContext.java |   8 +-
 .../maven/cling/invoker/mvnenc/EncryptInvoker.java |  34 ++--
 .../maven/cling/invoker/mvnenc/goals/Decrypt.java  |   7 +-
 .../maven/cling/invoker/mvnenc/goals/Diag.java     |   7 +-
 .../maven/cling/invoker/mvnenc/goals/Encrypt.java  |   7 +-
 .../maven/cling/invoker/mvnenc/goals/Init.java     | 218 ++++++++++----------
 .../invoker/mvnsh/CommonsCliShellOptions.java      |  87 ++++++++
 .../ShellCommandRegistryFactory.java}              |  12 +-
 .../invoker/mvnsh/ShellCommandRegistryHolder.java  |  62 ++++++
 .../maven/cling/invoker/mvnsh/ShellInvoker.java    | 181 +++++++++++++++++
 .../cling/invoker/mvnsh/ShellInvokerRequest.java   |  78 ++++++++
 .../maven/cling/invoker/mvnsh/ShellParser.java     |  68 +++++++
 .../BuiltinShellCommandRegistryFactory.java        | 186 +++++++++++++++++
 .../package-info.java}                             |  20 +-
 .../apache/maven/cling/invoker/package-info.java   |  12 ++
 ...MavenInvokerTest.java => MavenInvokerTest.java} |   8 +-
 .../invoker/mvn/forked/ForkedMavenInvokerTest.java |  53 -----
 .../executor/embedded/EmbeddedMavenExecutor.java   |   9 +-
 impl/maven-jline/pom.xml                           |   4 +
 pom.xml                                            |   5 +
 44 files changed, 1186 insertions(+), 691 deletions(-)

diff --git a/apache-maven/src/assembly/component.xml 
b/apache-maven/src/assembly/component.xml
index 94ae1f86ee..fefc8b49bf 100644
--- a/apache-maven/src/assembly/component.xml
+++ b/apache-maven/src/assembly/component.xml
@@ -86,6 +86,7 @@ under the License.
       <includes>
         <include>mvn</include>
         <include>mvnenc</include>
+        <include>mvnsh</include>
         <include>mvnDebug</include>
         <include>mvnencDebug</include>
         <!-- This is so that CI systems can periodically run the profiler -->
diff --git a/apache-maven/src/assembly/maven/bin/mvn 
b/apache-maven/src/assembly/maven/bin/mvn
index 5888451f5f..6fd203348f 100755
--- a/apache-maven/src/assembly/maven/bin/mvn
+++ b/apache-maven/src/assembly/maven/bin/mvn
@@ -207,6 +207,9 @@ handle_args() {
       --enc)
         MAVEN_MAIN_CLASS="org.apache.maven.cling.MavenEncCling"
         ;;
+      --shell)
+        MAVEN_MAIN_CLASS="org.apache.maven.cling.MavenShellCling"
+        ;;
       *)
         ;;
     esac
diff --git a/apache-maven/src/assembly/maven/bin/mvn.cmd 
b/apache-maven/src/assembly/maven/bin/mvn.cmd
index dcc413ce4b..d64073c400 100644
--- a/apache-maven/src/assembly/maven/bin/mvn.cmd
+++ b/apache-maven/src/assembly/maven/bin/mvn.cmd
@@ -200,6 +200,8 @@ if "%~1"=="--debug" (
     set 
"MAVEN_OPTS=-agentpath:%YJPLIB%=onexit=snapshot,onexit=memory,tracing,onlylocal 
%MAVEN_OPTS%"
 ) else if "%~1"=="--enc" (
     set "MAVEN_MAIN_CLASS=org.apache.maven.cling.MavenEncCling"
+) else if "%~1"=="--shell" (
+      set "MAVEN_MAIN_CLASS=org.apache.maven.cling.MavenShellCling"
 )
 exit /b 0
 
diff --git a/apache-maven/src/assembly/maven/bin/mvnsh 
b/apache-maven/src/assembly/maven/bin/mvnsh
new file mode 100644
index 0000000000..e3f847cdf5
--- /dev/null
+++ b/apache-maven/src/assembly/maven/bin/mvnsh
@@ -0,0 +1,30 @@
+#!/bin/sh
+
+# 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.
+
+# -----------------------------------------------------------------------------
+# Apache Maven Encrypt Script
+#
+# Environment Variable Prerequisites
+#
+#   JAVA_HOME           (Optional) Points to a Java installation.
+#   MAVEN_OPTS          (Optional) Java runtime options used when Maven is 
executed.
+#   MAVEN_SKIP_RC       (Optional) Flag to disable loading of mavenrc files.
+# -----------------------------------------------------------------------------
+
+"`dirname "$0"`/mvn" --shell "$@"
diff --git a/apache-maven/src/assembly/maven/bin/mvnsh.cmd 
b/apache-maven/src/assembly/maven/bin/mvnsh.cmd
new file mode 100644
index 0000000000..1b8ac48f3d
--- /dev/null
+++ b/apache-maven/src/assembly/maven/bin/mvnsh.cmd
@@ -0,0 +1,39 @@
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements.  See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership.  The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License.  You may obtain a copy of the License at
+@REM
+@REM    http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied.  See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+
+@REM 
-----------------------------------------------------------------------------
+@REM Apache Maven Encrypt Script
+@REM
+@REM Environment Variable Prerequisites
+@REM
+@REM   JAVA_HOME           (Optional) Points to a Java installation.
+@REM   MAVEN_BATCH_ECHO    (Optional) Set to 'on' to enable the echoing of the 
batch commands.
+@REM   MAVEN_BATCH_PAUSE   (Optional) set to 'on' to wait for a key stroke 
before ending.
+@REM   MAVEN_OPTS          (Optional) Java runtime options used when Maven is 
executed.
+@REM   MAVEN_SKIP_RC       (Optional) Flag to disable loading of mavenrc files.
+@REM 
-----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%"=="on" echo %MAVEN_BATCH_ECHO%
+
+@setlocal
+
+@call "%~dp0"mvn.cmd --shell %*
diff --git 
a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Options.java 
b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Options.java
index fe5629d0d7..3b5a355d32 100644
--- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Options.java
+++ b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Options.java
@@ -182,6 +182,14 @@ public interface Options {
     @Nonnull
     Optional<String> color();
 
+    /**
+     * Indicates whether Maven should operate in offline mode.
+     *
+     * @return an {@link Optional} containing true if offline mode is enabled, 
false if disabled, or empty if not specified
+     */
+    @Nonnull
+    Optional<Boolean> offline();
+
     /**
      * Indicates whether to show help information.
      *
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 a218f23eb9..e462b9842e 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
@@ -205,6 +205,34 @@ public interface ParserRequest {
         return builder(Tools.MVNENC_CMD, Tools.MVNENC_NAME, args, logger, 
messageBuilderFactory);
     }
 
+    /**
+     * Creates a new Builder instance for constructing a Maven Shell Tool 
ParserRequest.
+     *
+     * @param args the command-line arguments
+     * @param logger the logger to be used during parsing
+     * @param messageBuilderFactory the factory for creating message builders
+     * @return a new Builder instance
+     */
+    @Nonnull
+    static Builder mvnsh(
+            @Nonnull String[] args, @Nonnull Logger logger, @Nonnull 
MessageBuilderFactory messageBuilderFactory) {
+        return mvnsh(Arrays.asList(args), logger, messageBuilderFactory);
+    }
+
+    /**
+     * Creates a new Builder instance for constructing a Maven Shell Tool 
ParserRequest.
+     *
+     * @param args the command-line arguments
+     * @param logger the logger to be used during parsing
+     * @param messageBuilderFactory the factory for creating message builders
+     * @return a new Builder instance
+     */
+    @Nonnull
+    static Builder mvnsh(
+            @Nonnull List<String> args, @Nonnull Logger logger, @Nonnull 
MessageBuilderFactory messageBuilderFactory) {
+        return builder(Tools.MVNSHELL_CMD, Tools.MVNSHELL_NAME, args, logger, 
messageBuilderFactory);
+    }
+
     /**
      * Creates a new Builder instance for constructing a ParserRequest.
      *
diff --git 
a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Tools.java 
b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Tools.java
index b237dc5414..ce566d2710 100644
--- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Tools.java
+++ b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Tools.java
@@ -36,4 +36,7 @@ public final class Tools {
 
     public static final String MVNENC_CMD = "mvnenc";
     public static final String MVNENC_NAME = "Maven Password Encrypting Tool";
+
+    public static final String MVNSHELL_CMD = "mvnsh";
+    public static final String MVNSHELL_NAME = "Maven Shell Tool";
 }
diff --git 
a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/MavenOptions.java
 
b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/MavenOptions.java
index 4ce75036b8..1603e2747f 100644
--- 
a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/MavenOptions.java
+++ 
b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvn/MavenOptions.java
@@ -48,14 +48,6 @@ public interface MavenOptions extends Options {
     @Nonnull
     Optional<String> alternatePomFile();
 
-    /**
-     * Indicates whether Maven should operate in offline mode.
-     *
-     * @return an {@link Optional} containing true if offline mode is enabled, 
false if disabled, or empty if not specified
-     */
-    @Nonnull
-    Optional<Boolean> offline();
-
     /**
      * Indicates whether Maven should operate in non-recursive mode (i.e., not 
build child modules).
      *
diff --git 
a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Tools.java 
b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnsh/ShellOptions.java
similarity index 54%
copy from api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Tools.java
copy to 
api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnsh/ShellOptions.java
index b237dc5414..a55d4fd333 100644
--- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Tools.java
+++ 
b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnsh/ShellOptions.java
@@ -16,24 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.maven.api.cli;
+package org.apache.maven.api.cli.mvnsh;
+
+import java.util.Collection;
+import java.util.Map;
 
 import org.apache.maven.api.annotations.Experimental;
-import org.apache.maven.api.annotations.Immutable;
+import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.cli.Options;
 
 /**
- * Represents most common tools supported by CLIng.
+ * Defines the options specific to the Maven Shell tool.
+ * This interface extends the general {@link Options} interface, adding 
shell-specific configuration options.
  *
  * @since 4.0.0
  */
-@Immutable
 @Experimental
-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";
+public interface ShellOptions extends Options {
+    /**
+     * Returns a new instance of ShellOptions with values interpolated using 
the given properties.
+     *
+     * @param properties a collection of property maps to use for interpolation
+     * @return a new EncryptOptions instance with interpolated values
+     */
+    @Nonnull
+    ShellOptions interpolate(Collection<Map<String, String>> properties);
 }
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 d4869f2186..4bc245b558 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
@@ -26,8 +26,8 @@ import org.apache.maven.api.cli.ParserException;
 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.MavenInvoker;
 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;
 
@@ -61,7 +61,7 @@ public class MavenCling extends ClingSupport {
 
     @Override
     protected Invoker createInvoker() {
-        return new LocalMavenInvoker(
+        return new MavenInvoker(
                 ProtoLookup.builder().addMapping(ClassWorld.class, 
classWorld).build());
     }
 
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/MavenShellCling.java
similarity index 79%
copy from impl/maven-cli/src/main/java/org/apache/maven/cling/MavenCling.java
copy to impl/maven-cli/src/main/java/org/apache/maven/cling/MavenShellCling.java
index d4869f2186..b88f6173ef 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/MavenShellCling.java
@@ -26,21 +26,21 @@ import org.apache.maven.api.cli.ParserException;
 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.MavenParser;
-import org.apache.maven.cling.invoker.mvn.local.LocalMavenInvoker;
+import org.apache.maven.cling.invoker.mvnsh.ShellInvoker;
+import org.apache.maven.cling.invoker.mvnsh.ShellParser;
 import org.apache.maven.jline.JLineMessageBuilderFactory;
 import org.codehaus.plexus.classworlds.ClassWorld;
 
 /**
- * Maven CLI "new-gen".
+ * Maven shell.
  */
-public class MavenCling extends ClingSupport {
+public class MavenShellCling extends ClingSupport {
     /**
      * "Normal" Java entry point. Note: Maven uses ClassWorld Launcher and 
this entry point is NOT used under normal
      * circumstances.
      */
     public static void main(String[] args) throws IOException {
-        int exitCode = new MavenCling().run(args);
+        int exitCode = new MavenShellCling().run(args);
         System.exit(exitCode);
     }
 
@@ -48,27 +48,27 @@ public class MavenCling extends ClingSupport {
      * ClassWorld Launcher "enhanced" entry point: returning exitCode and 
accepts Class World.
      */
     public static int main(String[] args, ClassWorld world) throws IOException 
{
-        return new MavenCling(world).run(args);
+        return new MavenShellCling(world).run(args);
     }
 
-    public MavenCling() {
+    public MavenShellCling() {
         super();
     }
 
-    public MavenCling(ClassWorld classWorld) {
+    public MavenShellCling(ClassWorld classWorld) {
         super(classWorld);
     }
 
     @Override
     protected Invoker createInvoker() {
-        return new LocalMavenInvoker(
+        return new ShellInvoker(
                 ProtoLookup.builder().addMapping(ClassWorld.class, 
classWorld).build());
     }
 
     @Override
     protected InvokerRequest parseArguments(String[] args) throws 
ParserException, IOException {
-        return new MavenParser()
-                .parseInvocation(ParserRequest.mvn(args, new ProtoLogger(), 
new JLineMessageBuilderFactory())
+        return new ShellParser()
+                .parseInvocation(ParserRequest.mvnsh(args, new ProtoLogger(), 
new JLineMessageBuilderFactory())
                         .build());
     }
 }
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/extensions/BootstrapCoreExtensionManager.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/extensions/BootstrapCoreExtensionManager.java
index 064c325ebc..b5eb37166b 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/extensions/BootstrapCoreExtensionManager.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/extensions/BootstrapCoreExtensionManager.java
@@ -18,9 +18,6 @@
  */
 package org.apache.maven.cling.extensions;
 
-import javax.inject.Inject;
-import javax.inject.Named;
-
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -33,7 +30,10 @@ import java.util.stream.Collectors;
 import org.apache.maven.RepositoryUtils;
 import org.apache.maven.api.Service;
 import org.apache.maven.api.Session;
+import org.apache.maven.api.annotations.Nullable;
 import org.apache.maven.api.cli.extensions.CoreExtension;
+import org.apache.maven.api.di.Inject;
+import org.apache.maven.api.di.Named;
 import org.apache.maven.api.model.Plugin;
 import org.apache.maven.api.services.ArtifactCoordinatesFactory;
 import org.apache.maven.api.services.ArtifactManager;
@@ -80,7 +80,6 @@ import org.eclipse.aether.resolution.ArtifactResult;
 import org.eclipse.aether.resolution.DependencyResult;
 import org.eclipse.aether.util.filter.ExclusionsDependencyFilter;
 import org.eclipse.aether.util.version.GenericVersionScheme;
-import org.eclipse.sisu.Nullable;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/CommonsCliOptions.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/CommonsCliOptions.java
index 190b7d5862..0f8464d61b 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/CommonsCliOptions.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/CommonsCliOptions.java
@@ -202,6 +202,14 @@ public abstract class CommonsCliOptions implements Options 
{
         return Optional.empty();
     }
 
+    @Override
+    public Optional<Boolean> offline() {
+        if (commandLine.hasOption(CLIManager.OFFLINE)) {
+            return Optional.of(Boolean.TRUE);
+        }
+        return Optional.empty();
+    }
+
     @Override
     public Optional<Boolean> help() {
         if (commandLine.hasOption(CLIManager.HELP)) {
@@ -267,11 +275,13 @@ public abstract class CommonsCliOptions implements 
Options {
         public static final String LOG_FILE = "l";
         public static final String RAW_STREAMS = "raw-streams";
         public static final String COLOR = "color";
+        public static final String OFFLINE = "o";
         public static final String HELP = "h";
 
         // parameters handled by script
         public static final String DEBUG = "debug";
         public static final String ENC = "enc";
+        public static final String SHELL = "shell";
         public static final String YJP = "yjp";
 
         // deprecated ones
@@ -378,6 +388,10 @@ public abstract class CommonsCliOptions implements Options 
{
                     .optionalArg(true)
                     .desc("Defines the color mode of the output. Supported are 
'auto', 'always', 'never'.")
                     .build());
+            options.addOption(Option.builder(OFFLINE)
+                    .longOpt("offline")
+                    .desc("Work offline")
+                    .build());
 
             // Parameters handled by script
             options.addOption(Option.builder()
@@ -388,6 +402,10 @@ public abstract class CommonsCliOptions implements Options 
{
                     .longOpt(ENC)
                     .desc("Launch the Maven Encryption tool (script option).")
                     .build());
+            options.addOption(Option.builder()
+                    .longOpt(SHELL)
+                    .desc("Launch the Maven Shell tool (script option).")
+                    .build());
             options.addOption(Option.builder()
                     .longOpt(YJP)
                     .desc("Launch the JVM with Yourkit profiler (script 
option).")
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LayeredOptions.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LayeredOptions.java
index c5b9a50ffa..4d9d7b6c09 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LayeredOptions.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LayeredOptions.java
@@ -132,6 +132,11 @@ public abstract class LayeredOptions<O extends Options> 
implements Options {
         return returnFirstPresentOrEmpty(Options::color);
     }
 
+    @Override
+    public Optional<Boolean> offline() {
+        return returnFirstPresentOrEmpty(Options::offline);
+    }
+
     @Override
     public Optional<Boolean> help() {
         return returnFirstPresentOrEmpty(Options::help);
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
index 8c7fd1df5f..d487b72f66 100644
--- 
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
@@ -35,6 +35,7 @@ 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.logging.Slf4jConfiguration;
+import org.apache.maven.eventspy.internal.EventSpyDispatcher;
 import org.apache.maven.logging.BuildEventListener;
 import org.jline.terminal.Terminal;
 import org.slf4j.ILoggerFactory;
@@ -47,14 +48,20 @@ public class LookupContext implements AutoCloseable {
     public final Function<String, Path> cwdResolver;
     public final Function<String, Path> installationResolver;
     public final Function<String, Path> userResolver;
+    public final boolean containerCapsuleManaged;
 
-    protected LookupContext(InvokerRequest invokerRequest) {
+    public LookupContext(InvokerRequest invokerRequest) {
+        this(invokerRequest, true);
+    }
+
+    public LookupContext(InvokerRequest invokerRequest, boolean 
containerCapsuleManaged) {
         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.containerCapsuleManaged = containerCapsuleManaged;
         this.logger = invokerRequest.parserRequest().logger();
 
         Map<String, String> user = new 
HashMap<>(invokerRequest.userProperties());
@@ -84,8 +91,10 @@ public class LookupContext implements AutoCloseable {
     public Boolean coloredOutput;
     public Terminal terminal;
     public Consumer<String> writer;
+
     public ContainerCapsule containerCapsule;
     public Lookup lookup;
+    public EventSpyDispatcher eventSpyDispatcher;
 
     public BuildEventListener buildEventListener;
 
@@ -93,7 +102,6 @@ public class LookupContext implements AutoCloseable {
     public Path installationSettingsPath;
     public Path projectSettingsPath;
     public Path userSettingsPath;
-
     public boolean interactive;
     public Path localRepositoryPath;
     public Settings effectiveSettings;
@@ -124,11 +132,18 @@ public class LookupContext implements AutoCloseable {
         }
     }
 
-    protected void closeContainer() {
+    public final void closeContainer() {
+        if (containerCapsuleManaged) {
+            doCloseContainer();
+        }
+    }
+
+    public void doCloseContainer() {
         if (containerCapsule != null) {
             try {
                 containerCapsule.close();
             } finally {
+                eventSpyDispatcher = null;
                 lookup = null;
                 containerCapsule = null;
             }
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 348492f069..95d1ce9cc4 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,6 +24,7 @@ import java.io.PrintWriter;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
@@ -34,6 +35,7 @@ import java.util.function.Function;
 
 import org.apache.maven.api.Constants;
 import org.apache.maven.api.ProtoSession;
+import org.apache.maven.api.annotations.Nullable;
 import org.apache.maven.api.cli.Invoker;
 import org.apache.maven.api.cli.InvokerException;
 import org.apache.maven.api.cli.InvokerRequest;
@@ -65,6 +67,7 @@ import 
org.apache.maven.cling.invoker.spi.PropertyContributorsHolder;
 import org.apache.maven.cling.logging.Slf4jConfiguration;
 import org.apache.maven.cling.logging.Slf4jConfigurationFactory;
 import org.apache.maven.cling.utils.CLIReportingUtils;
+import org.apache.maven.eventspy.internal.EventSpyDispatcher;
 import org.apache.maven.execution.MavenExecutionRequest;
 import org.apache.maven.internal.impl.SettingsUtilsV4;
 import org.apache.maven.jline.FastTerminal;
@@ -75,6 +78,7 @@ import org.apache.maven.logging.ProjectBuildLogAppender;
 import org.apache.maven.logging.SimpleBuildEventListener;
 import org.apache.maven.logging.api.LogLevelRecorder;
 import org.apache.maven.slf4j.MavenSimpleLogger;
+import org.codehaus.plexus.PlexusContainer;
 import org.jline.terminal.Terminal;
 import org.jline.terminal.TerminalBuilder;
 import org.jline.terminal.impl.AbstractPosixTerminal;
@@ -92,19 +96,27 @@ import static 
org.apache.maven.cling.invoker.Utils.toProperties;
  * @param <C> The context type.
  */
 public abstract class LookupInvoker<C extends LookupContext> implements 
Invoker {
-    protected final ProtoLookup protoLookup;
+    protected final Lookup protoLookup;
 
-    public LookupInvoker(ProtoLookup protoLookup) {
+    @Nullable
+    protected final Consumer<LookupContext> contextConsumer;
+
+    public LookupInvoker(Lookup protoLookup, @Nullable Consumer<LookupContext> 
contextConsumer) {
         this.protoLookup = requireNonNull(protoLookup);
+        this.contextConsumer = contextConsumer;
     }
 
     @Override
     public int invoke(InvokerRequest invokerRequest) throws InvokerException {
         requireNonNull(invokerRequest);
 
-        Properties oldProps = (Properties) System.getProperties().clone();
+        Properties oldProps = new Properties();
+        oldProps.putAll(System.getProperties());
         ClassLoader oldCL = Thread.currentThread().getContextClassLoader();
         try (C context = createContext(invokerRequest)) {
+            if (contextConsumer != null) {
+                contextConsumer.accept(context);
+            }
             try {
                 if (context.containerCapsule != null
                         && 
context.containerCapsule.currentThreadClassLoader().isPresent()) {
@@ -128,8 +140,6 @@ public abstract class LookupInvoker<C extends 
LookupContext> implements Invoker
     protected int doInvoke(C context) throws Exception {
         pushCoreProperties(context);
         pushUserProperties(context);
-        validate(context);
-        prepare(context);
         configureLogging(context);
         createTerminal(context);
         activateLogging(context);
@@ -192,10 +202,6 @@ public abstract class LookupInvoker<C extends 
LookupContext> implements Invoker
         }
     }
 
-    protected void validate(C context) throws Exception {}
-
-    protected void prepare(C context) throws Exception {}
-
     protected void configureLogging(C context) throws Exception {
         // LOG COLOR
         Options mavenOptions = context.invokerRequest.options();
@@ -251,33 +257,40 @@ public abstract class LookupInvoker<C extends 
LookupContext> implements Invoker
     }
 
     protected void createTerminal(C context) {
-        MessageUtils.systemInstall(
-                builder -> {
-                    builder.streams(
-                            context.invokerRequest.in().orElse(null),
-                            context.invokerRequest.out().orElse(null));
-                    
builder.systemOutput(TerminalBuilder.SystemOutput.ForcedSysOut);
-                    // The exec builder suffers from 
https://github.com/jline/jline3/issues/1098
-                    // We could re-enable it when fixed to provide support for 
non-standard architectures,
-                    // for which JLine does not provide any native library.
-                    builder.exec(false);
-                    if (context.coloredOutput != null) {
-                        builder.color(context.coloredOutput);
-                    }
-                },
-                terminal -> doConfigureWithTerminal(context, terminal));
-
-        context.terminal = MessageUtils.getTerminal();
-        // JLine is quite slow to start due to the native library unpacking 
and loading
-        // so boot it asynchronously
-        context.closeables.add(MessageUtils::systemUninstall);
-        MessageUtils.registerShutdownHook(); // safety belt
-        if (context.coloredOutput != null) {
-            MessageUtils.setColorEnabled(context.coloredOutput);
+        if (context.terminal == null) {
+            MessageUtils.systemInstall(
+                    builder -> {
+                        builder.streams(
+                                context.invokerRequest.in().orElse(null),
+                                context.invokerRequest.out().orElse(null));
+                        
builder.systemOutput(TerminalBuilder.SystemOutput.ForcedSysOut);
+                        // The exec builder suffers from 
https://github.com/jline/jline3/issues/1098
+                        // We could re-enable it when fixed to provide support 
for non-standard architectures,
+                        // for which JLine does not provide any native library.
+                        builder.exec(false);
+                        if (context.coloredOutput != null) {
+                            builder.color(context.coloredOutput);
+                        }
+                    },
+                    terminal -> doConfigureWithTerminal(context, terminal));
+
+            context.terminal = MessageUtils.getTerminal();
+            // JLine is quite slow to start due to the native library 
unpacking and loading
+            // so boot it asynchronously
+            context.closeables.add(MessageUtils::systemUninstall);
+            MessageUtils.registerShutdownHook(); // safety belt
+            if (context.coloredOutput != null) {
+                MessageUtils.setColorEnabled(context.coloredOutput);
+            }
+        } else {
+            if (context.coloredOutput != null) {
+                MessageUtils.setColorEnabled(context.coloredOutput);
+            }
         }
     }
 
     protected void doConfigureWithTerminal(C context, Terminal terminal) {
+        context.terminal = terminal;
         Options options = context.invokerRequest.options();
         if (options.rawStreams().isEmpty() || !options.rawStreams().get()) {
             MavenSimpleLogger stdout = (MavenSimpleLogger) 
context.loggerFactory.getLogger("stdout");
@@ -406,15 +419,13 @@ public abstract class LookupInvoker<C extends 
LookupContext> implements Invoker
     }
 
     protected void container(C context) throws Exception {
-        context.containerCapsule = 
createContainerCapsuleFactory().createContainerCapsule(this, context);
-        context.closeables.add(context::closeContainer);
-        context.lookup = context.containerCapsule.getLookup();
-
-        // refresh logger in case container got customized by spy
-        org.slf4j.Logger l = 
context.loggerFactory.getLogger(this.getClass().getName());
-        context.logger = (level, message, error) -> 
l.atLevel(org.slf4j.event.Level.valueOf(level.name()))
-                .setCause(error)
-                .log(message);
+        if (context.lookup == null) {
+            context.containerCapsule = 
createContainerCapsuleFactory().createContainerCapsule(this, context);
+            context.closeables.add(context::closeContainer);
+            context.lookup = context.containerCapsule.getLookup();
+        } else {
+            context.containerCapsule.updateLogging(context);
+        }
     }
 
     protected ContainerCapsuleFactory<C> createContainerCapsuleFactory() {
@@ -434,9 +445,22 @@ public abstract class LookupInvoker<C extends 
LookupContext> implements Invoker
         context.protoSession = protoSession;
     }
 
-    protected void lookup(C context) throws Exception {}
+    protected void lookup(C context) throws Exception {
+        if (context.eventSpyDispatcher == null) {
+            context.eventSpyDispatcher = 
context.lookup.lookup(EventSpyDispatcher.class);
+        }
+    }
 
-    protected void init(C context) throws Exception {}
+    protected void init(C context) throws Exception {
+        InvokerRequest invokerRequest = context.invokerRequest;
+        Map<String, Object> data = new HashMap<>();
+        data.put("plexus", context.lookup.lookup(PlexusContainer.class));
+        data.put("workingDirectory", invokerRequest.cwd().toString());
+        data.put("systemProperties", 
toProperties(context.protoSession.getSystemProperties()));
+        data.put("userProperties", 
toProperties(context.protoSession.getUserProperties()));
+        data.put("versionProperties", CLIReportingUtils.getBuildProperties());
+        context.eventSpyDispatcher.init(() -> data);
+    }
 
     protected void postCommands(C context) throws Exception {
         InvokerRequest invokerRequest = context.invokerRequest;
@@ -465,7 +489,9 @@ public abstract class LookupInvoker<C extends 
LookupContext> implements Invoker
     }
 
     protected void settings(C context) throws Exception {
-        settings(context, true, context.lookup.lookup(SettingsBuilder.class));
+        if (context.effectiveSettings == null) {
+            settings(context, true, 
context.lookup.lookup(SettingsBuilder.class));
+        }
     }
 
     /**
@@ -553,6 +579,9 @@ public abstract class LookupInvoker<C extends 
LookupContext> implements Invoker
                 .build();
 
         customizeSettingsRequest(context, settingsRequest);
+        if (context.eventSpyDispatcher != null) {
+            context.eventSpyDispatcher.onEvent(settingsRequest);
+        }
 
         context.logger.debug("Reading installation settings from '" + 
installationSettingsFile + "'");
         context.logger.debug("Reading project settings from '" + 
projectSettingsFile + "'");
@@ -560,6 +589,9 @@ public abstract class LookupInvoker<C extends 
LookupContext> implements Invoker
 
         SettingsBuilderResult settingsResult = 
settingsBuilder.build(settingsRequest);
         customizeSettingsResult(context, settingsResult);
+        if (context.eventSpyDispatcher != null) {
+            context.eventSpyDispatcher.onEvent(settingsResult);
+        }
 
         context.effectiveSettings = settingsResult.getEffectiveSettings();
         context.interactive = mayDisableInteractiveMode(context, 
context.effectiveSettings.isInteractiveMode());
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/CommonsCliMavenOptions.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/CommonsCliMavenOptions.java
index 6104507156..33ff742d6b 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/CommonsCliMavenOptions.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/CommonsCliMavenOptions.java
@@ -79,14 +79,6 @@ public class CommonsCliMavenOptions extends 
CommonsCliOptions implements MavenOp
         return Optional.empty();
     }
 
-    @Override
-    public Optional<Boolean> offline() {
-        if (commandLine.hasOption(CLIManager.OFFLINE)) {
-            return Optional.of(Boolean.TRUE);
-        }
-        return Optional.empty();
-    }
-
     @Override
     public Optional<Boolean> nonRecursive() {
         if (commandLine.hasOption(CLIManager.NON_RECURSIVE)) {
@@ -263,7 +255,6 @@ public class CommonsCliMavenOptions extends 
CommonsCliOptions implements MavenOp
 
     protected static class CLIManager extends CommonsCliOptions.CLIManager {
         public static final String ALTERNATE_POM_FILE = "f";
-        public static final String OFFLINE = "o";
         public static final String NON_RECURSIVE = "N";
         public static final String UPDATE_SNAPSHOTS = "U";
         public static final String ACTIVATE_PROFILES = "P";
@@ -293,10 +284,6 @@ public class CommonsCliMavenOptions extends 
CommonsCliOptions implements MavenOp
                     .hasArg()
                     .desc("Force the use of an alternate POM file (or 
directory with pom.xml)")
                     .build());
-            options.addOption(Option.builder(OFFLINE)
-                    .longOpt("offline")
-                    .desc("Work offline")
-                    .build());
             options.addOption(Option.builder(NON_RECURSIVE)
                     .longOpt("non-recursive")
                     .desc(
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/LayeredMavenOptions.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/LayeredMavenOptions.java
index b06675780e..79c25b8fd4 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/LayeredMavenOptions.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/LayeredMavenOptions.java
@@ -54,11 +54,6 @@ public class LayeredMavenOptions<O extends MavenOptions> 
extends LayeredOptions<
         return returnFirstPresentOrEmpty(MavenOptions::alternatePomFile);
     }
 
-    @Override
-    public Optional<Boolean> offline() {
-        return returnFirstPresentOrEmpty(MavenOptions::offline);
-    }
-
     @Override
     public Optional<Boolean> nonRecursive() {
         return returnFirstPresentOrEmpty(MavenOptions::nonRecursive);
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
index 18f61aea36..9f364ba593 100644
--- 
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
@@ -21,21 +21,25 @@ package org.apache.maven.cling.invoker.mvn;
 import org.apache.maven.Maven;
 import org.apache.maven.api.cli.InvokerRequest;
 import org.apache.maven.cling.invoker.LookupContext;
-import org.apache.maven.eventspy.internal.EventSpyDispatcher;
 
 @SuppressWarnings("VisibilityModifier")
 public class MavenContext extends LookupContext {
     public MavenContext(InvokerRequest invokerRequest) {
-        super(invokerRequest);
+        this(invokerRequest, true);
+    }
+
+    public MavenContext(InvokerRequest invokerRequest, boolean 
containerCapsuleManaged) {
+        super(invokerRequest, containerCapsuleManaged);
     }
 
-    public EventSpyDispatcher eventSpyDispatcher;
     public Maven maven;
 
     @Override
-    protected void closeContainer() {
-        eventSpyDispatcher = null;
-        maven = null;
-        super.closeContainer();
+    public void doCloseContainer() {
+        try {
+            super.doCloseContainer();
+        } finally {
+            maven = null;
+        }
     }
 }
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java
index 6a6c2a9d27..5029132170 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java
@@ -22,10 +22,10 @@ import java.io.FileNotFoundException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Consumer;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -33,28 +33,26 @@ import org.apache.maven.InternalErrorException;
 import org.apache.maven.Maven;
 import org.apache.maven.api.Constants;
 import org.apache.maven.api.MonotonicClock;
+import org.apache.maven.api.annotations.Nullable;
+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.mvn.MavenOptions;
 import org.apache.maven.api.services.BuilderProblem;
 import org.apache.maven.api.services.Lookup;
-import org.apache.maven.api.services.SettingsBuilderRequest;
-import org.apache.maven.api.services.SettingsBuilderResult;
 import org.apache.maven.api.services.Source;
 import org.apache.maven.api.services.ToolchainsBuilder;
 import org.apache.maven.api.services.ToolchainsBuilderRequest;
 import org.apache.maven.api.services.ToolchainsBuilderResult;
 import org.apache.maven.api.services.model.ModelProcessor;
 import org.apache.maven.cling.event.ExecutionEventLogger;
+import org.apache.maven.cling.invoker.LookupContext;
 import org.apache.maven.cling.invoker.LookupInvoker;
-import org.apache.maven.cling.invoker.ProtoLookup;
 import org.apache.maven.cling.invoker.Utils;
 import org.apache.maven.cling.transfer.ConsoleMavenTransferListener;
 import org.apache.maven.cling.transfer.QuietMavenTransferListener;
 import org.apache.maven.cling.transfer.SimplexTransferListener;
 import org.apache.maven.cling.transfer.Slf4jMavenTransferListener;
-import org.apache.maven.cling.utils.CLIReportingUtils;
-import org.apache.maven.eventspy.internal.EventSpyDispatcher;
 import org.apache.maven.exception.DefaultExceptionHandler;
 import org.apache.maven.exception.ExceptionHandler;
 import org.apache.maven.exception.ExceptionSummary;
@@ -70,25 +68,30 @@ import 
org.apache.maven.lifecycle.LifecycleExecutionException;
 import org.apache.maven.logging.LoggingExecutionListener;
 import org.apache.maven.logging.MavenTransferListener;
 import org.apache.maven.project.MavenProject;
-import org.codehaus.plexus.PlexusContainer;
 import org.eclipse.aether.DefaultRepositoryCache;
 import org.eclipse.aether.transfer.TransferListener;
 
 import static java.util.Comparator.comparing;
-import static org.apache.maven.cling.invoker.Utils.toProperties;
 
 /**
- * The "local" Maven invoker, that expects whole Maven on classpath and 
invokes it.
- *
- * @param <C> The context type.
+ * The Maven invoker, that expects whole Maven on classpath and invokes it.
  */
-public abstract class MavenInvoker<C extends MavenContext> extends 
LookupInvoker<C> {
-    public MavenInvoker(ProtoLookup protoLookup) {
-        super(protoLookup);
+public class MavenInvoker extends LookupInvoker<MavenContext> {
+    public MavenInvoker(Lookup protoLookup) {
+        this(protoLookup, null);
+    }
+
+    public MavenInvoker(Lookup protoLookup, @Nullable Consumer<LookupContext> 
contextConsumer) {
+        super(protoLookup, contextConsumer);
+    }
+
+    @Override
+    protected MavenContext createContext(InvokerRequest invokerRequest) throws 
InvokerException {
+        return new MavenContext(invokerRequest);
     }
 
     @Override
-    protected int execute(C context) throws Exception {
+    protected int execute(MavenContext context) throws Exception {
         MavenExecutionRequest request = prepareMavenExecutionRequest();
         toolchains(context, request);
         populateRequest(context, context.lookup, request);
@@ -113,26 +116,15 @@ public abstract class MavenInvoker<C extends 
MavenContext> extends LookupInvoker
     }
 
     @Override
-    protected void lookup(C context) throws Exception {
-        context.eventSpyDispatcher = 
context.lookup.lookup(EventSpyDispatcher.class);
-        context.maven = context.lookup.lookup(Maven.class);
-    }
-
-    @Override
-    protected void init(C context) throws Exception {
-        super.init(context);
-        InvokerRequest invokerRequest = context.invokerRequest;
-        Map<String, Object> data = new HashMap<>();
-        data.put("plexus", context.lookup.lookup(PlexusContainer.class));
-        data.put("workingDirectory", invokerRequest.cwd().toString());
-        data.put("systemProperties", 
toProperties(context.protoSession.getSystemProperties()));
-        data.put("userProperties", 
toProperties(context.protoSession.getUserProperties()));
-        data.put("versionProperties", CLIReportingUtils.getBuildProperties());
-        context.eventSpyDispatcher.init(() -> data);
+    protected void lookup(MavenContext context) throws Exception {
+        if (context.maven == null) {
+            super.lookup(context);
+            context.maven = context.lookup.lookup(Maven.class);
+        }
     }
 
     @Override
-    protected void postCommands(C context) throws Exception {
+    protected void postCommands(MavenContext context) throws Exception {
         super.postCommands(context);
 
         InvokerRequest invokerRequest = context.invokerRequest;
@@ -145,21 +137,7 @@ public abstract class MavenInvoker<C extends MavenContext> 
extends LookupInvoker
         }
     }
 
-    @Override
-    protected void customizeSettingsRequest(C context, SettingsBuilderRequest 
settingsBuilderRequest) throws Exception {
-        if (context.eventSpyDispatcher != null) {
-            context.eventSpyDispatcher.onEvent(settingsBuilderRequest);
-        }
-    }
-
-    @Override
-    protected void customizeSettingsResult(C context, SettingsBuilderResult 
settingsBuilderResult) throws Exception {
-        if (context.eventSpyDispatcher != null) {
-            context.eventSpyDispatcher.onEvent(settingsBuilderResult);
-        }
-    }
-
-    protected void toolchains(C context, MavenExecutionRequest request) throws 
Exception {
+    protected void toolchains(MavenContext context, MavenExecutionRequest 
request) throws Exception {
         Path userToolchainsFile = null;
         if (context.invokerRequest.options().altUserToolchains().isPresent()) {
             userToolchainsFile = context.cwdResolver.apply(
@@ -240,7 +218,8 @@ public abstract class MavenInvoker<C extends MavenContext> 
extends LookupInvoker
     }
 
     @Override
-    protected void populateRequest(C context, Lookup lookup, 
MavenExecutionRequest request) throws Exception {
+    protected void populateRequest(MavenContext context, Lookup lookup, 
MavenExecutionRequest request)
+            throws Exception {
         super.populateRequest(context, lookup, request);
         if (context.invokerRequest.rootDirectory().isEmpty()) {
             // maven requires this to be set; so default it (and see below at 
POM)
@@ -322,7 +301,7 @@ public abstract class MavenInvoker<C extends MavenContext> 
extends LookupInvoker
         }
     }
 
-    protected Path determinePom(C context, Lookup lookup) {
+    protected Path determinePom(MavenContext context, Lookup lookup) {
         Path current = context.invokerRequest.cwd();
         MavenOptions options = (MavenOptions) context.invokerRequest.options();
         if (options.alternatePomFile().isPresent()) {
@@ -337,7 +316,7 @@ public abstract class MavenInvoker<C extends MavenContext> 
extends LookupInvoker
         }
     }
 
-    protected String determineReactorFailureBehaviour(C context) {
+    protected String determineReactorFailureBehaviour(MavenContext context) {
         MavenOptions mavenOptions = (MavenOptions) 
context.invokerRequest.options();
         if (mavenOptions.failFast().isPresent()) {
             return MavenExecutionRequest.REACTOR_FAIL_FAST;
@@ -350,7 +329,7 @@ public abstract class MavenInvoker<C extends MavenContext> 
extends LookupInvoker
         }
     }
 
-    protected String determineGlobalChecksumPolicy(C context) {
+    protected String determineGlobalChecksumPolicy(MavenContext context) {
         MavenOptions mavenOptions = (MavenOptions) 
context.invokerRequest.options();
         if (mavenOptions.strictChecksums().orElse(false)) {
             return MavenExecutionRequest.CHECKSUM_POLICY_FAIL;
@@ -361,7 +340,7 @@ public abstract class MavenInvoker<C extends MavenContext> 
extends LookupInvoker
         }
     }
 
-    protected ExecutionListener determineExecutionListener(C context) {
+    protected ExecutionListener determineExecutionListener(MavenContext 
context) {
         ExecutionListener listener = new 
ExecutionEventLogger(context.invokerRequest.messageBuilderFactory());
         if (context.eventSpyDispatcher != null) {
             listener = context.eventSpyDispatcher.chainListener(listener);
@@ -369,7 +348,7 @@ public abstract class MavenInvoker<C extends MavenContext> 
extends LookupInvoker
         return new LoggingExecutionListener(listener, 
determineBuildEventListener(context));
     }
 
-    protected TransferListener determineTransferListener(C context, boolean 
noTransferProgress) {
+    protected TransferListener determineTransferListener(MavenContext context, 
boolean noTransferProgress) {
         boolean quiet = context.invokerRequest.options().quiet().orElse(false);
         boolean logFile = 
context.invokerRequest.options().logFile().isPresent();
         boolean runningOnCI = isRunningOnCI(context);
@@ -390,7 +369,7 @@ public abstract class MavenInvoker<C extends MavenContext> 
extends LookupInvoker
         return new MavenTransferListener(delegate, 
determineBuildEventListener(context));
     }
 
-    protected String determineMakeBehavior(C context) {
+    protected String determineMakeBehavior(MavenContext context) {
         MavenOptions mavenOptions = (MavenOptions) 
context.invokerRequest.options();
         if (mavenOptions.alsoMake().isPresent()
                 && mavenOptions.alsoMakeDependents().isEmpty()) {
@@ -406,7 +385,7 @@ public abstract class MavenInvoker<C extends MavenContext> 
extends LookupInvoker
         }
     }
 
-    protected void performProjectActivation(C context, ProjectActivation 
projectActivation) {
+    protected void performProjectActivation(MavenContext context, 
ProjectActivation projectActivation) {
         MavenOptions mavenOptions = (MavenOptions) 
context.invokerRequest.options();
         if (mavenOptions.projects().isPresent()
                 && !mavenOptions.projects().get().isEmpty()) {
@@ -434,7 +413,7 @@ public abstract class MavenInvoker<C extends MavenContext> 
extends LookupInvoker
         }
     }
 
-    protected void performProfileActivation(C context, ProfileActivation 
profileActivation) {
+    protected void performProfileActivation(MavenContext context, 
ProfileActivation profileActivation) {
         MavenOptions mavenOptions = (MavenOptions) 
context.invokerRequest.options();
         if (mavenOptions.activatedProfiles().isPresent()
                 && !mavenOptions.activatedProfiles().get().isEmpty()) {
@@ -462,7 +441,7 @@ public abstract class MavenInvoker<C extends MavenContext> 
extends LookupInvoker
         }
     }
 
-    protected int doExecute(C context, MavenExecutionRequest request) throws 
Exception {
+    protected int doExecute(MavenContext context, MavenExecutionRequest 
request) throws Exception {
         context.eventSpyDispatcher.onEvent(request);
 
         MavenExecutionResult result;
@@ -534,7 +513,7 @@ public abstract class MavenInvoker<C extends MavenContext> 
extends LookupInvoker
         }
     }
 
-    protected void logBuildResumeHint(C context, String resumeBuildHint) {
+    protected void logBuildResumeHint(MavenContext context, String 
resumeBuildHint) {
         context.logger.error("");
         context.logger.error("After correcting the problems, you can resume 
the build with the command");
         context.logger.error(
@@ -577,7 +556,8 @@ public abstract class MavenInvoker<C extends MavenContext> 
extends LookupInvoker
 
     protected static final String ANSI_RESET = "\u001B\u005Bm";
 
-    protected void logSummary(C context, ExceptionSummary summary, Map<String, 
String> references, String indent) {
+    protected void logSummary(
+            MavenContext context, ExceptionSummary summary, Map<String, 
String> references, String indent) {
         String referenceKey = "";
 
         if (summary.getReference() != null && 
!summary.getReference().isEmpty()) {
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/forked/ForkedMavenInvoker.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/forked/ForkedMavenInvoker.java
deleted file mode 100644
index 29ad880a2f..0000000000
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/forked/ForkedMavenInvoker.java
+++ /dev/null
@@ -1,220 +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.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.internal.impl.model.profile.Os;
-
-import static java.util.Objects.requireNonNull;
-
-/**
- * Forked invoker implementation, that spawns a subprocess with Maven from the 
installation directory.
- */
-public class ForkedMavenInvoker implements Invoker {
-    @SuppressWarnings("MethodLength")
-    @Override
-    public int invoke(InvokerRequest invokerRequest) throws InvokerException {
-        requireNonNull(invokerRequest);
-        validate(invokerRequest);
-
-        ArrayList<String> cmdAndArguments = new ArrayList<>();
-        cmdAndArguments.add(invokerRequest
-                .installationDirectory()
-                .resolve("bin")
-                .resolve(
-                        Os.IS_WINDOWS
-                                ? invokerRequest.parserRequest().command() + 
".cmd"
-                                : invokerRequest.parserRequest().command())
-                .toString());
-
-        MavenOptions mavenOptions = (MavenOptions) invokerRequest.options();
-        if (mavenOptions.userProperties().isPresent()) {
-            for (Map.Entry<String, String> entry :
-                    mavenOptions.userProperties().get().entrySet()) {
-                cmdAndArguments.add("-D" + entry.getKey() + "=" + 
entry.getValue());
-            }
-        }
-        if (mavenOptions.showVersionAndExit().orElse(false)) {
-            cmdAndArguments.add("--version");
-        }
-        if (mavenOptions.showVersion().orElse(false)) {
-            cmdAndArguments.add("--show-version");
-        }
-        if (mavenOptions.quiet().orElse(false)) {
-            cmdAndArguments.add("--quiet");
-        }
-        if (mavenOptions.verbose().orElse(false)) {
-            cmdAndArguments.add("--verbose");
-        }
-        if (mavenOptions.showErrors().orElse(false)) {
-            cmdAndArguments.add("--errors");
-        }
-        if (mavenOptions.failOnSeverity().isPresent()) {
-            cmdAndArguments.add("--fail-on-severity");
-            cmdAndArguments.add(mavenOptions.failOnSeverity().get());
-        }
-        if (mavenOptions.nonInteractive().orElse(false)) {
-            cmdAndArguments.add("--non-interactive");
-        }
-        if (mavenOptions.forceInteractive().orElse(false)) {
-            cmdAndArguments.add("--force-interactive");
-        }
-        if (mavenOptions.altUserSettings().isPresent()) {
-            cmdAndArguments.add("--settings");
-            cmdAndArguments.add(mavenOptions.altUserSettings().get());
-        }
-        if (mavenOptions.altProjectSettings().isPresent()) {
-            cmdAndArguments.add("--project-settings");
-            cmdAndArguments.add(mavenOptions.altProjectSettings().get());
-        }
-        if (mavenOptions.altInstallationSettings().isPresent()) {
-            cmdAndArguments.add("--install-settings");
-            cmdAndArguments.add(mavenOptions.altInstallationSettings().get());
-        }
-        if (mavenOptions.altUserToolchains().isPresent()) {
-            cmdAndArguments.add("--toolchains");
-            cmdAndArguments.add(mavenOptions.altUserToolchains().get());
-        }
-        if (mavenOptions.altInstallationToolchains().isPresent()) {
-            cmdAndArguments.add("--install-toolchains");
-            
cmdAndArguments.add(mavenOptions.altInstallationToolchains().get());
-        }
-        if (mavenOptions.logFile().isPresent()) {
-            cmdAndArguments.add("--log-file");
-            cmdAndArguments.add(mavenOptions.logFile().get());
-        }
-        if (mavenOptions.color().isPresent()) {
-            cmdAndArguments.add("--color");
-            cmdAndArguments.add(mavenOptions.color().get());
-        }
-        if (mavenOptions.help().orElse(false)) {
-            cmdAndArguments.add("--help");
-        }
-        if (mavenOptions.alternatePomFile().isPresent()) {
-            cmdAndArguments.add("--file");
-            cmdAndArguments.add(mavenOptions.alternatePomFile().get());
-        }
-        if (mavenOptions.offline().orElse(false)) {
-            cmdAndArguments.add("--offline");
-        }
-        if (mavenOptions.nonRecursive().orElse(false)) {
-            cmdAndArguments.add("--non-recursive");
-        }
-        if (mavenOptions.updateSnapshots().orElse(false)) {
-            cmdAndArguments.add("--update-snapshots");
-        }
-        if (mavenOptions.activatedProfiles().isPresent()) {
-            cmdAndArguments.add("--activate-profiles");
-            cmdAndArguments.add(
-                    String.join(",", mavenOptions.activatedProfiles().get()));
-        }
-        if (mavenOptions.suppressSnapshotUpdates().orElse(false)) {
-            cmdAndArguments.add("--no-snapshot-updates");
-        }
-        if (mavenOptions.strictChecksums().orElse(false)) {
-            cmdAndArguments.add("--strict-checksums");
-        }
-        if (mavenOptions.relaxedChecksums().orElse(false)) {
-            cmdAndArguments.add("--lax-checksums");
-        }
-        if (mavenOptions.failFast().orElse(false)) {
-            cmdAndArguments.add("--fail-fast");
-        }
-        if (mavenOptions.failAtEnd().orElse(false)) {
-            cmdAndArguments.add("--fail-at-end");
-        }
-        if (mavenOptions.failNever().orElse(false)) {
-            cmdAndArguments.add("--fail-never");
-        }
-        if (mavenOptions.resume().orElse(false)) {
-            cmdAndArguments.add("--resume");
-        }
-        if (mavenOptions.resumeFrom().isPresent()) {
-            cmdAndArguments.add("--resume-from");
-            cmdAndArguments.add(mavenOptions.resumeFrom().get());
-        }
-        if (mavenOptions.projects().isPresent()) {
-            cmdAndArguments.add("--projects");
-            cmdAndArguments.add(String.join(",", 
mavenOptions.projects().get()));
-        }
-        if (mavenOptions.alsoMake().orElse(false)) {
-            cmdAndArguments.add("--also-make");
-        }
-        if (mavenOptions.alsoMakeDependents().orElse(false)) {
-            cmdAndArguments.add("--also-make-dependents");
-        }
-        if (mavenOptions.threads().isPresent()) {
-            cmdAndArguments.add("--threads");
-            cmdAndArguments.add(mavenOptions.threads().get());
-        }
-        if (mavenOptions.builder().isPresent()) {
-            cmdAndArguments.add("--builder");
-            cmdAndArguments.add(mavenOptions.builder().get());
-        }
-        if (mavenOptions.noTransferProgress().orElse(false)) {
-            cmdAndArguments.add("--no-transfer-progress");
-        }
-        if (mavenOptions.cacheArtifactNotFound().isPresent()) {
-            cmdAndArguments.add("--cache-artifact-not-found");
-            
cmdAndArguments.add(mavenOptions.cacheArtifactNotFound().get().toString());
-        }
-        if (mavenOptions.strictArtifactDescriptorPolicy().isPresent()) {
-            cmdAndArguments.add("--strict-artifact-descriptor-policy");
-            cmdAndArguments.add(
-                    
mavenOptions.strictArtifactDescriptorPolicy().get().toString());
-        }
-        if (mavenOptions.ignoreTransitiveRepositories().isPresent()) {
-            cmdAndArguments.add("--ignore-transitive-repositories");
-        }
-
-        // last the goals
-        
cmdAndArguments.addAll(mavenOptions.goals().orElse(Collections.emptyList()));
-
-        try {
-            ProcessBuilder pb = new ProcessBuilder()
-                    .directory(invokerRequest.cwd().toFile())
-                    .command(cmdAndArguments);
-
-            if (invokerRequest.jvmArguments().isPresent()) {
-                pb.environment()
-                        .put(
-                                "MAVEN_OPTS",
-                                String.join(" ", 
invokerRequest.jvmArguments().get()));
-            }
-
-            return pb.start().waitFor();
-        } catch (IOException e) {
-            invokerRequest.logger().error("IO problem while executing command: 
" + cmdAndArguments, e);
-            return 127;
-        } catch (InterruptedException e) {
-            invokerRequest.logger().error("Interrupted while executing 
command: " + cmdAndArguments, e);
-            return 127;
-        }
-    }
-
-    protected void validate(InvokerRequest invokerRequest) throws 
InvokerException {}
-}
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
deleted file mode 100644
index fb1e2ed7ea..0000000000
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/resident/ResidentMavenContext.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.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
-    protected void closeContainer() {
-        // we are resident; we do not shut down here
-    }
-
-    public void shutDown() throws InvokerException {
-        super.closeContainer();
-    }
-
-    public ResidentMavenContext copy(InvokerRequest invokerRequest) {
-        if (invokerRequest == this.invokerRequest) {
-            return this;
-        }
-        ResidentMavenContext shadow = new ResidentMavenContext(invokerRequest);
-
-        // we carry over only "resident" things
-        shadow.containerCapsule = containerCapsule;
-        shadow.lookup = lookup;
-        shadow.eventSpyDispatcher = eventSpyDispatcher;
-        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
index 41d57395e7..fe4c9cb9ed 100644
--- 
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
@@ -23,29 +23,32 @@ 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.api.services.Lookup;
+import org.apache.maven.cling.invoker.mvn.MavenContext;
 import org.apache.maven.cling.invoker.mvn.MavenInvoker;
 
 /**
- * Resident invoker implementation, similar to "local", but keeps Maven 
instance resident. This implies, that
+ * Resident invoker implementation, specialization of Maven Invoker, 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).
+ * This implementation "pre-populates" MavenContext with pre-existing stuff 
(except for very first call)
+ * and does not let DI container to be closed.
  */
-public class ResidentMavenInvoker extends MavenInvoker<ResidentMavenContext> {
+public class ResidentMavenInvoker extends MavenInvoker {
 
-    private final ConcurrentHashMap<String, ResidentMavenContext> 
residentContext;
+    private final ConcurrentHashMap<String, MavenContext> residentContext;
 
-    public ResidentMavenInvoker(ProtoLookup protoLookup) {
-        super(protoLookup);
+    public ResidentMavenInvoker(Lookup protoLookup) {
+        super(protoLookup, null);
         this.residentContext = new ConcurrentHashMap<>();
     }
 
     @Override
     public void close() throws InvokerException {
         ArrayList<InvokerException> exceptions = new ArrayList<>();
-        for (ResidentMavenContext context : residentContext.values()) {
+        for (MavenContext context : residentContext.values()) {
             try {
-                context.shutDown();
+                context.doCloseContainer();
             } catch (InvokerException e) {
                 exceptions.add(e);
             }
@@ -58,31 +61,25 @@ public class ResidentMavenInvoker extends 
MavenInvoker<ResidentMavenContext> {
     }
 
     @Override
-    protected ResidentMavenContext createContext(InvokerRequest 
invokerRequest) {
-        return residentContext
-                .computeIfAbsent(getContextId(invokerRequest), k -> new 
ResidentMavenContext(invokerRequest))
-                .copy(invokerRequest);
-    }
-
-    protected String getContextId(InvokerRequest invokerRequest) {
+    protected MavenContext createContext(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";
+        // and allow multiple instances per JVM, this may become a pool? 
derive key based in invokerRequest?
+        MavenContext result = residentContext.computeIfAbsent("resident", k -> 
new MavenContext(invokerRequest, false));
+        return copyIfDifferent(result, invokerRequest);
     }
 
-    @Override
-    protected void container(ResidentMavenContext context) throws Exception {
-        if (context.containerCapsule == null) {
-            super.container(context);
-        } else {
-            context.containerCapsule.updateLogging(context);
+    protected MavenContext copyIfDifferent(MavenContext mavenContext, 
InvokerRequest invokerRequest) {
+        if (invokerRequest == mavenContext.invokerRequest) {
+            return mavenContext;
         }
-    }
+        MavenContext shadow = new MavenContext(invokerRequest, false);
 
-    @Override
-    protected void lookup(ResidentMavenContext context) throws Exception {
-        if (context.maven == null) {
-            super.lookup(context);
-        }
+        // we carry over only "resident" things
+        shadow.containerCapsule = mavenContext.containerCapsule;
+        shadow.lookup = mavenContext.lookup;
+        shadow.eventSpyDispatcher = mavenContext.eventSpyDispatcher;
+        shadow.maven = mavenContext.maven;
+
+        return shadow;
     }
 }
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
index 7f1e97abcb..1e3f225148 100644
--- 
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
@@ -30,8 +30,12 @@ import org.jline.utils.AttributedStyle;
 
 @SuppressWarnings("VisibilityModifier")
 public class EncryptContext extends LookupContext {
-    protected EncryptContext(InvokerRequest invokerRequest) {
-        super(invokerRequest);
+    public EncryptContext(InvokerRequest invokerRequest) {
+        this(invokerRequest, true);
+    }
+
+    public EncryptContext(InvokerRequest invokerRequest, boolean 
containerCapsuleManaged) {
+        super(invokerRequest, containerCapsuleManaged);
     }
 
     public Map<String, Goal> goals;
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/EncryptInvoker.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/EncryptInvoker.java
index 1c52752377..ac55d48ecc 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/EncryptInvoker.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/EncryptInvoker.java
@@ -20,11 +20,14 @@ package org.apache.maven.cling.invoker.mvnenc;
 
 import java.io.InterruptedIOException;
 import java.util.ArrayList;
+import java.util.function.Consumer;
 
+import org.apache.maven.api.annotations.Nullable;
 import org.apache.maven.api.cli.InvokerRequest;
 import org.apache.maven.api.cli.mvnenc.EncryptOptions;
+import org.apache.maven.api.services.Lookup;
+import org.apache.maven.cling.invoker.LookupContext;
 import org.apache.maven.cling.invoker.LookupInvoker;
-import org.apache.maven.cling.invoker.ProtoLookup;
 import org.apache.maven.cling.utils.CLIReportingUtils;
 import org.jline.reader.LineReaderBuilder;
 import org.jline.reader.UserInterruptException;
@@ -37,13 +40,17 @@ import org.jline.utils.Colors;
  */
 public class EncryptInvoker extends LookupInvoker<EncryptContext> {
 
-    public EncryptInvoker(ProtoLookup protoLookup) {
-        super(protoLookup);
+    public static final int OK = 0; // OK
+    public static final int ERROR = 1; // "generic" error
+    public static final int BAD_OPERATION = 2; // bad user input or alike
+    public static final int CANCELED = 3; // user canceled
+
+    public EncryptInvoker(Lookup protoLookup) {
+        this(protoLookup, null);
     }
 
-    @Override
-    protected int execute(EncryptContext context) throws Exception {
-        return doExecute(context);
+    public EncryptInvoker(Lookup protoLookup, @Nullable 
Consumer<LookupContext> contextConsumer) {
+        super(protoLookup, contextConsumer);
     }
 
     @Override
@@ -52,16 +59,15 @@ public class EncryptInvoker extends 
LookupInvoker<EncryptContext> {
     }
 
     @Override
-    protected void lookup(EncryptContext context) {
-        context.goals = context.lookup.lookupMap(Goal.class);
+    protected void lookup(EncryptContext context) throws Exception {
+        if (context.goals == null) {
+            super.lookup(context);
+            context.goals = context.lookup.lookupMap(Goal.class);
+        }
     }
 
-    public static final int OK = 0; // OK
-    public static final int ERROR = 1; // "generic" error
-    public static final int BAD_OPERATION = 2; // bad user input or alike
-    public static final int CANCELED = 3; // user canceled
-
-    protected int doExecute(EncryptContext context) throws Exception {
+    @Override
+    protected int execute(EncryptContext context) throws Exception {
         try {
             context.header = new ArrayList<>();
             context.style = new AttributedStyle();
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 e58ac80bcd..c5e42699e2 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
@@ -18,10 +18,9 @@
  */
 package org.apache.maven.cling.invoker.mvnenc.goals;
 
-import javax.inject.Inject;
-import javax.inject.Named;
-import javax.inject.Singleton;
-
+import org.apache.maven.api.di.Inject;
+import org.apache.maven.api.di.Named;
+import org.apache.maven.api.di.Singleton;
 import org.apache.maven.api.services.MessageBuilderFactory;
 import org.apache.maven.cling.invoker.mvnenc.EncryptContext;
 import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
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 f6271d15cb..ab845de223 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
@@ -18,10 +18,9 @@
  */
 package org.apache.maven.cling.invoker.mvnenc.goals;
 
-import javax.inject.Inject;
-import javax.inject.Named;
-import javax.inject.Singleton;
-
+import org.apache.maven.api.di.Inject;
+import org.apache.maven.api.di.Named;
+import org.apache.maven.api.di.Singleton;
 import org.apache.maven.api.services.MessageBuilderFactory;
 import org.apache.maven.cling.invoker.mvnenc.EncryptContext;
 import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
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 bbe77f54a0..2bfe2c01a5 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
@@ -18,10 +18,9 @@
  */
 package org.apache.maven.cling.invoker.mvnenc.goals;
 
-import javax.inject.Inject;
-import javax.inject.Named;
-import javax.inject.Singleton;
-
+import org.apache.maven.api.di.Inject;
+import org.apache.maven.api.di.Named;
+import org.apache.maven.api.di.Singleton;
 import org.apache.maven.api.services.MessageBuilderFactory;
 import org.apache.maven.cling.invoker.mvnenc.EncryptContext;
 import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
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 ddaeed2846..18f15ca923 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
@@ -18,15 +18,17 @@
  */
 package org.apache.maven.cling.invoker.mvnenc.goals;
 
-import javax.inject.Inject;
-import javax.inject.Named;
-import javax.inject.Singleton;
-
+import java.io.IOError;
+import java.io.InterruptedIOException;
+import java.util.HashMap;
 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.di.Inject;
+import org.apache.maven.api.di.Named;
+import org.apache.maven.api.di.Singleton;
 import org.apache.maven.api.services.MessageBuilderFactory;
 import org.apache.maven.cling.invoker.mvnenc.EncryptContext;
 import org.codehaus.plexus.components.secdispatcher.DispatcherMeta;
@@ -40,14 +42,12 @@ import org.jline.consoleui.prompt.ConsolePrompt;
 import org.jline.consoleui.prompt.PromptResultItemIF;
 import org.jline.consoleui.prompt.builder.ListPromptBuilder;
 import org.jline.consoleui.prompt.builder.PromptBuilder;
-import org.jline.reader.Candidate;
-import org.jline.reader.Completer;
-import org.jline.reader.LineReader;
-import org.jline.reader.ParsedLine;
+import org.jline.reader.UserInterruptException;
 import org.jline.utils.Colors;
 import org.jline.utils.OSUtils;
 
 import static 
org.apache.maven.cling.invoker.mvnenc.EncryptInvoker.BAD_OPERATION;
+import static org.apache.maven.cling.invoker.mvnenc.EncryptInvoker.CANCELED;
 import static org.apache.maven.cling.invoker.mvnenc.EncryptInvoker.OK;
 
 /**
@@ -63,6 +63,7 @@ public class Init extends InteractiveGoalSupport {
         super(messageBuilderFactory, secDispatcher);
     }
 
+    @SuppressWarnings("MethodLength")
     @Override
     public int doExecute(EncryptContext context) throws Exception {
         EncryptOptions options = (EncryptOptions) 
context.invokerRequest.options();
@@ -87,125 +88,128 @@ public class Init extends InteractiveGoalSupport {
             promptConfig = new ConsolePrompt.UiConfig("❯", "◯ ", "◉ ", "◯ ");
         }
         promptConfig.setCancellableFirstPrompt(true);
-        ConsolePrompt prompt = new ConsolePrompt(context.reader, 
context.terminal, promptConfig);
 
         SettingsSecurity config = secDispatcher.readConfiguration(true);
-
         // reset config
         config.setDefaultDispatcher(null);
         config.getConfigurations().clear();
 
-        Map<String, PromptResultItemIF> result = prompt.prompt(
-                context.header, 
dispatcherPrompt(prompt.getPromptBuilder()).build());
-        if (result == null) {
-            throw new InterruptedException();
-        }
-        if (NONE.equals(result.get("defaultDispatcher").getResult())) {
-            context.terminal
-                    .writer()
-                    .println(messageBuilderFactory
-                            .builder()
-                            .warning(
-                                    "Maven4 SecDispatcher disabled; Maven3 
fallback may still work, use `mvnenc diag` to check")
-                            .build());
-            secDispatcher.writeConfiguration(config);
-            return OK;
-        }
-        
config.setDefaultDispatcher(result.get("defaultDispatcher").getResult());
+        try (ConsolePrompt prompt = new ConsolePrompt(context.reader, 
context.terminal, promptConfig)) {
+            Map<String, PromptResultItemIF> dispatcherResult = new HashMap<>();
+            Map<String, PromptResultItemIF> dispatcherConfigResult = new 
HashMap<>();
+            Map<String, PromptResultItemIF> confirmChoice = new HashMap<>();
 
-        DispatcherMeta meta = secDispatcher.availableDispatchers().stream()
-                .filter(d -> Objects.equals(config.getDefaultDispatcher(), 
d.name()))
-                .findFirst()
-                .orElseThrow();
-        if (!meta.fields().isEmpty()) {
-            result = prompt.prompt(
-                    context.header,
-                    configureDispatcher(context, meta, 
prompt.getPromptBuilder())
-                            .build());
-            if (result == null) {
+            prompt.prompt(
+                    context.header, 
dispatcherPrompt(prompt.getPromptBuilder()).build(), dispatcherResult);
+            if (dispatcherResult.isEmpty()) {
                 throw new InterruptedException();
             }
+            if 
(NONE.equals(dispatcherResult.get("defaultDispatcher").getResult())) {
+                context.terminal
+                        .writer()
+                        .println(messageBuilderFactory
+                                .builder()
+                                .warning(
+                                        "Maven4 SecDispatcher disabled; Maven3 
fallback may still work, use `mvnenc diag` to check")
+                                .build());
+            } else {
+                config.setDefaultDispatcher(
+                        dispatcherResult.get("defaultDispatcher").getResult());
 
-            List<Map.Entry<String, PromptResultItemIF>> editables = 
result.entrySet().stream()
-                    .filter(e -> e.getValue().getResult().contains("$"))
-                    .toList();
-            if (!editables.isEmpty()) {
-                context.addInHeader("");
-                context.addInHeader("Please customize the editable value:");
-                Map<String, PromptResultItemIF> editMap;
-                for (Map.Entry<String, PromptResultItemIF> editable : 
editables) {
-                    String template = editable.getValue().getResult();
-                    String prefix = template.substring(0, 
template.indexOf("$"));
-                    editMap = prompt.prompt(
+                DispatcherMeta meta = 
secDispatcher.availableDispatchers().stream()
+                        .filter(d -> 
Objects.equals(config.getDefaultDispatcher(), d.name()))
+                        .findFirst()
+                        .orElseThrow();
+                if (!meta.fields().isEmpty()) {
+                    prompt.prompt(
                             context.header,
-                            prompt.getPromptBuilder()
-                                    .createInputPrompt()
-                                    .name("edit")
-                                    .message(template)
-                                    .addCompleter(new Completer() {
-                                        @Override
-                                        public void complete(
-                                                LineReader reader, ParsedLine 
line, List<Candidate> candidates) {
-                                            if 
(!line.line().startsWith(prefix)) {
-                                                candidates.add(
-                                                        new Candidate(prefix, 
prefix, null, null, null, null, false));
-                                            }
-                                        }
-                                    })
-                                    .addPrompt()
-                                    .build());
-                    if (editMap == null) {
+                            configureDispatcher(context, meta, 
prompt.getPromptBuilder())
+                                    .build(),
+                            dispatcherConfigResult);
+                    if (dispatcherConfigResult.isEmpty()) {
                         throw new InterruptedException();
                     }
-                    result.put(editable.getKey(), editMap.get("edit"));
-                }
-            }
 
-            Config dispatcherConfig = new Config();
-            dispatcherConfig.setName(meta.name());
-            for (DispatcherMeta.Field field : meta.fields()) {
-                ConfigProperty property = new ConfigProperty();
-                property.setName(field.getKey());
-                property.setValue(result.get(field.getKey()).getResult());
-                dispatcherConfig.addProperty(property);
-            }
-            if (!dispatcherConfig.getProperties().isEmpty()) {
-                config.addConfiguration(dispatcherConfig);
-            }
-        }
+                    List<Map.Entry<String, PromptResultItemIF>> editables = 
dispatcherConfigResult.entrySet().stream()
+                            .filter(e -> 
e.getValue().getResult().contains("$"))
+                            .toList();
+                    if (!editables.isEmpty()) {
+                        context.addInHeader("");
+                        context.addInHeader("Please customize the editable 
value:");
+                        Map<String, PromptResultItemIF> editMap = new 
HashMap<>(editables.size());
+                        for (Map.Entry<String, PromptResultItemIF> editable : 
editables) {
+                            String template = editable.getValue().getResult();
+                            prompt.prompt(
+                                    context.header,
+                                    prompt.getPromptBuilder()
+                                            .createInputPrompt()
+                                            .name("edit")
+                                            .message(template)
+                                            .addPrompt()
+                                            .build(),
+                                    editMap);
+                            if (editMap.isEmpty()) {
+                                throw new InterruptedException();
+                            }
+                            dispatcherConfigResult.put(editable.getKey(), 
editMap.get("edit"));
+                        }
+                    }
 
-        if (yes) {
-            secDispatcher.writeConfiguration(config);
-        } else {
-            context.addInHeader("");
-            context.addInHeader("Values set:");
-            context.addInHeader("defaultDispatcher=" + 
config.getDefaultDispatcher());
-            for (Config c : config.getConfigurations()) {
-                context.addInHeader("  dispatcherName=" + c.getName());
-                for (ConfigProperty cp : c.getProperties()) {
-                    context.addInHeader("    " + cp.getName() + "=" + 
cp.getValue());
+                    Config dispatcherConfig = new Config();
+                    dispatcherConfig.setName(meta.name());
+                    for (DispatcherMeta.Field field : meta.fields()) {
+                        ConfigProperty property = new ConfigProperty();
+                        property.setName(field.getKey());
+                        property.setValue(
+                                
dispatcherConfigResult.get(field.getKey()).getResult());
+                        dispatcherConfig.addProperty(property);
+                    }
+                    if (!dispatcherConfig.getProperties().isEmpty()) {
+                        config.addConfiguration(dispatcherConfig);
+                    }
                 }
             }
 
-            result = prompt.prompt(
-                    context.header, 
confirmPrompt(prompt.getPromptBuilder()).build());
-            ConfirmResult confirm = (ConfirmResult) result.get("confirm");
-            if (confirm.getConfirmed() == ConfirmChoice.ConfirmationValue.YES) 
{
-                context.terminal
-                        .writer()
-                        .println(messageBuilderFactory
-                                .builder()
-                                .info("Writing out the configuration...")
-                                .build());
+            if (yes) {
                 secDispatcher.writeConfiguration(config);
             } else {
-                context.terminal
-                        .writer()
-                        .println(messageBuilderFactory
-                                .builder()
-                                .warning("Values not accepted; not saving 
configuration.")
-                                .build());
-                return BAD_OPERATION;
+                context.addInHeader("");
+                context.addInHeader("Values set:");
+                context.addInHeader("defaultDispatcher=" + 
config.getDefaultDispatcher());
+                for (Config c : config.getConfigurations()) {
+                    context.addInHeader("  dispatcherName=" + c.getName());
+                    for (ConfigProperty cp : c.getProperties()) {
+                        context.addInHeader("    " + cp.getName() + "=" + 
cp.getValue());
+                    }
+                }
+
+                prompt.prompt(
+                        context.header, 
confirmPrompt(prompt.getPromptBuilder()).build(), confirmChoice);
+                ConfirmResult confirm = (ConfirmResult) 
confirmChoice.get("confirm");
+                if (confirm.getConfirmed() == 
ConfirmChoice.ConfirmationValue.YES) {
+                    context.terminal
+                            .writer()
+                            .println(messageBuilderFactory
+                                    .builder()
+                                    .info("Writing out the configuration...")
+                                    .build());
+                    secDispatcher.writeConfiguration(config);
+                } else {
+                    context.terminal
+                            .writer()
+                            .println(messageBuilderFactory
+                                    .builder()
+                                    .warning("Values not accepted; not saving 
configuration.")
+                                    .build());
+                    return CANCELED;
+                }
+            }
+        } catch (IOError e) {
+            // TODO: this should be handled properly in jline3!
+            if (e.getCause() instanceof InterruptedIOException) {
+                throw new UserInterruptException(e.getCause());
+            } else {
+                throw e;
             }
         }
 
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/CommonsCliShellOptions.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/CommonsCliShellOptions.java
new file mode 100644
index 0000000000..04b1c36755
--- /dev/null
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/CommonsCliShellOptions.java
@@ -0,0 +1,87 @@
+/*
+ * 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.mvnsh;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.ParseException;
+import org.apache.maven.api.cli.Options;
+import org.apache.maven.api.cli.mvnsh.ShellOptions;
+import org.apache.maven.cling.invoker.CommonsCliOptions;
+import org.codehaus.plexus.interpolation.BasicInterpolator;
+import org.codehaus.plexus.interpolation.InterpolationException;
+
+import static org.apache.maven.cling.invoker.Utils.createInterpolator;
+
+/**
+ * Implementation of {@link ShellOptions} (base + shell).
+ */
+public class CommonsCliShellOptions extends CommonsCliOptions implements 
ShellOptions {
+    public static CommonsCliShellOptions parse(String[] args) throws 
ParseException {
+        CLIManager cliManager = new CLIManager();
+        return new CommonsCliShellOptions(Options.SOURCE_CLI, cliManager, 
cliManager.parse(args));
+    }
+
+    protected CommonsCliShellOptions(String source, CLIManager cliManager, 
CommandLine commandLine) {
+        super(source, cliManager, commandLine);
+    }
+
+    private static CommonsCliShellOptions interpolate(
+            CommonsCliShellOptions options, Collection<Map<String, String>> 
properties) {
+        try {
+            // now that we have properties, interpolate all arguments
+            BasicInterpolator interpolator = createInterpolator(properties);
+            CommandLine.Builder commandLineBuilder = new CommandLine.Builder();
+            commandLineBuilder.setDeprecatedHandler(o -> {});
+            for (Option option : options.commandLine.getOptions()) {
+                if (!CLIManager.USER_PROPERTY.equals(option.getOpt())) {
+                    List<String> values = option.getValuesList();
+                    for (ListIterator<String> it = values.listIterator(); 
it.hasNext(); ) {
+                        it.set(interpolator.interpolate(it.next()));
+                    }
+                }
+                commandLineBuilder.addOption(option);
+            }
+            for (String arg : options.commandLine.getArgList()) {
+                commandLineBuilder.addArg(interpolator.interpolate(arg));
+            }
+            return new CommonsCliShellOptions(
+                    options.source, (CLIManager) options.cliManager, 
commandLineBuilder.build());
+        } catch (InterpolationException e) {
+            throw new IllegalArgumentException("Could not interpolate 
CommonsCliOptions", e);
+        }
+    }
+
+    @Override
+    public ShellOptions interpolate(Collection<Map<String, String>> 
properties) {
+        return interpolate(this, properties);
+    }
+
+    protected static class CLIManager extends CommonsCliOptions.CLIManager {
+        @Override
+        protected String commandLineSyntax(String command) {
+            return command + " [options]";
+        }
+    }
+}
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/package-info.java 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/ShellCommandRegistryFactory.java
similarity index 75%
copy from 
impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/package-info.java
copy to 
impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/ShellCommandRegistryFactory.java
index b169eba8db..0f8fae9948 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/package-info.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/ShellCommandRegistryFactory.java
@@ -16,9 +16,11 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+package org.apache.maven.cling.invoker.mvnsh;
 
-/**
- * This package contain support (mostly abstract) classes, that implement 
"base" of CLIng.
- * In packages below you find actual implementations.
- */
-package org.apache.maven.cling.invoker;
+import org.apache.maven.cling.invoker.LookupContext;
+import org.jline.console.CommandRegistry;
+
+public interface ShellCommandRegistryFactory {
+    CommandRegistry createShellCommandRegistry(LookupContext context);
+}
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/ShellCommandRegistryHolder.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/ShellCommandRegistryHolder.java
new file mode 100644
index 0000000000..20251943b1
--- /dev/null
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/ShellCommandRegistryHolder.java
@@ -0,0 +1,62 @@
+/*
+ * 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.mvnsh;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jline.console.CommandRegistry;
+
+import static java.util.Objects.requireNonNull;
+
+public class ShellCommandRegistryHolder implements AutoCloseable {
+    private final List<CommandRegistry> commandRegistries;
+
+    public ShellCommandRegistryHolder() {
+        this.commandRegistries = new ArrayList<>();
+    }
+
+    public void addCommandRegistry(CommandRegistry commandRegistry) {
+        requireNonNull(commandRegistry, "commandRegistry");
+        this.commandRegistries.add(commandRegistry);
+    }
+
+    public CommandRegistry[] getCommandRegistries() {
+        return commandRegistries.toArray(new CommandRegistry[0]);
+    }
+
+    @Override
+    public void close() throws Exception {
+        ArrayList<Exception> exceptions = new ArrayList<>();
+        for (CommandRegistry commandRegistry : commandRegistries) {
+            if (commandRegistry instanceof AutoCloseable closeable) {
+                try {
+                    closeable.close();
+                } catch (Exception e) {
+                    exceptions.add(e);
+                }
+            }
+        }
+        if (!exceptions.isEmpty()) {
+            IllegalStateException ex = new IllegalStateException("Could not 
close commandRegistries");
+            exceptions.forEach(ex::addSuppressed);
+            throw ex;
+        }
+    }
+}
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/ShellInvoker.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/ShellInvoker.java
new file mode 100644
index 0000000000..12abb2eb19
--- /dev/null
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/ShellInvoker.java
@@ -0,0 +1,181 @@
+/*
+ * 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.mvnsh;
+
+import java.nio.file.Path;
+import java.util.Map;
+
+import org.apache.maven.api.cli.InvokerRequest;
+import org.apache.maven.api.services.Lookup;
+import org.apache.maven.cling.invoker.LookupContext;
+import org.apache.maven.cling.invoker.LookupInvoker;
+import org.apache.maven.cling.utils.CLIReportingUtils;
+import org.jline.builtins.ConfigurationPath;
+import org.jline.console.impl.Builtins;
+import org.jline.console.impl.SimpleSystemRegistryImpl;
+import org.jline.console.impl.SystemRegistryImpl;
+import org.jline.keymap.KeyMap;
+import org.jline.reader.Binding;
+import org.jline.reader.EndOfFileException;
+import org.jline.reader.LineReader;
+import org.jline.reader.LineReaderBuilder;
+import org.jline.reader.MaskingCallback;
+import org.jline.reader.Parser;
+import org.jline.reader.Reference;
+import org.jline.reader.UserInterruptException;
+import org.jline.reader.impl.DefaultHighlighter;
+import org.jline.reader.impl.DefaultParser;
+import org.jline.reader.impl.history.DefaultHistory;
+import org.jline.utils.AttributedStringBuilder;
+import org.jline.utils.AttributedStyle;
+import org.jline.utils.InfoCmp;
+import org.jline.widget.TailTipWidgets;
+
+/**
+ * mvnsh invoker implementation.
+ */
+public class ShellInvoker extends LookupInvoker<LookupContext> {
+
+    public ShellInvoker(Lookup protoLookup) {
+        super(protoLookup, null);
+    }
+
+    @Override
+    protected LookupContext createContext(InvokerRequest invokerRequest) {
+        return new LookupContext(invokerRequest);
+    }
+
+    public static final int OK = 0; // OK
+    public static final int ERROR = 1; // "generic" error
+
+    @Override
+    protected int execute(LookupContext context) throws Exception {
+        // set up JLine built-in commands
+        ConfigurationPath configPath =
+                new ConfigurationPath(context.invokerRequest.cwd(), 
context.invokerRequest.cwd());
+        Builtins builtins = new Builtins(context.invokerRequest::cwd, 
configPath, null);
+        builtins.rename(Builtins.Command.TTOP, "top");
+        builtins.alias("zle", "widget");
+        builtins.alias("bindkey", "keymap");
+
+        ShellCommandRegistryHolder holder = new ShellCommandRegistryHolder();
+        holder.addCommandRegistry(builtins);
+
+        // gather commands
+        Map<String, ShellCommandRegistryFactory> factories =
+                context.lookup.lookupMap(ShellCommandRegistryFactory.class);
+        for (Map.Entry<String, ShellCommandRegistryFactory> entry : 
factories.entrySet()) {
+            
holder.addCommandRegistry(entry.getValue().createShellCommandRegistry(context));
+        }
+
+        Parser parser = new DefaultParser();
+
+        String banner =
+                """
+
+                ░▒▓██████████████▓▒░ ░▒▓█▓▒░░▒▓█▓▒░░▒▓███████▓▒░  
░▒▓███████▓▒░░▒▓█▓▒░░▒▓█▓▒░\s
+                ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░       
░▒▓█▓▒░░▒▓█▓▒░\s
+                ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░       
░▒▓█▓▒░░▒▓█▓▒░\s
+                ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓██████▓▒░ 
░▒▓████████▓▒░\s
+                ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░  ░▒▓█▓▓█▓▒░  ░▒▓█▓▒░░▒▓█▓▒░       
░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░\s
+                ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░  ░▒▓█▓▓█▓▒░  ░▒▓█▓▒░░▒▓█▓▒░       
░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░\s
+                ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░   ░▒▓██▓▒░   ░▒▓█▓▒░░▒▓█▓▒░░▒▓███████▓▒░ 
░▒▓█▓▒░░▒▓█▓▒░""";
+        context.writer.accept(banner);
+        if (!context.invokerRequest.options().showVersion().orElse(false)) {
+            context.writer.accept(CLIReportingUtils.showVersionMinimal());
+        }
+        context.writer.accept("");
+
+        try (holder) {
+            SimpleSystemRegistryImpl systemRegistry =
+                    new SimpleSystemRegistryImpl(parser, context.terminal, 
context.invokerRequest::cwd, configPath);
+            systemRegistry.setCommandRegistries(holder.getCommandRegistries());
+
+            Path history = context.userResolver.apply(".mvnsh_history");
+            LineReader reader = LineReaderBuilder.builder()
+                    .terminal(context.terminal)
+                    .history(new DefaultHistory())
+                    .highlighter(new ReplHighlighter())
+                    .completer(systemRegistry.completer())
+                    .parser(parser)
+                    .variable(LineReader.LIST_MAX, 50) // max tab completion 
candidates
+                    .variable(LineReader.HISTORY_FILE, history)
+                    .variable(LineReader.OTHERS_GROUP_NAME, "Others")
+                    .variable(LineReader.COMPLETION_STYLE_GROUP, 
"fg:blue,bold")
+                    .variable("HELP_COLORS", "ti=1;34:co=38:ar=3:op=33:de=90")
+                    .option(LineReader.Option.GROUP_PERSIST, true)
+                    .build();
+            builtins.setLineReader(reader);
+            systemRegistry.setLineReader(reader);
+            new TailTipWidgets(reader, systemRegistry::commandDescription, 5, 
TailTipWidgets.TipType.COMPLETER);
+            KeyMap<Binding> keyMap = reader.getKeyMaps().get("main");
+            keyMap.bind(new Reference("tailtip-toggle"), KeyMap.alt("s"));
+
+            String prompt = "mvnsh> ";
+            String rightPrompt = null;
+
+            // start the shell and process input until the user quits with 
Ctrl-D
+            String line;
+            while (true) {
+                try {
+                    systemRegistry.cleanUp();
+                    line = reader.readLine(prompt, rightPrompt, 
(MaskingCallback) null, null);
+                    systemRegistry.execute(line);
+                } catch (UserInterruptException e) {
+                    // Ignore
+                    // return CANCELED;
+                } catch (EndOfFileException e) {
+                    return OK;
+                } catch (SystemRegistryImpl.UnknownCommandException e) {
+                    context.writer.accept(context.invokerRequest
+                            .messageBuilderFactory()
+                            .builder()
+                            .error(e.getMessage())
+                            .build());
+                } catch (Exception e) {
+                    systemRegistry.trace(e);
+                    context.writer.accept(context.invokerRequest
+                            .messageBuilderFactory()
+                            .builder()
+                            .error("Error:" + e.getMessage())
+                            .build());
+                    if 
(context.invokerRequest.options().showErrors().orElse(false)) {
+                        e.printStackTrace(context.terminal.writer());
+                    }
+                    return ERROR;
+                }
+            }
+        }
+    }
+
+    private static class ReplHighlighter extends DefaultHighlighter {
+        @Override
+        protected void commandStyle(LineReader reader, AttributedStringBuilder 
sb, boolean enable) {
+            if (enable) {
+                if 
(reader.getTerminal().getNumericCapability(InfoCmp.Capability.max_colors) >= 
256) {
+                    sb.style(AttributedStyle.DEFAULT.bold().foreground(69));
+                } else {
+                    
sb.style(AttributedStyle.DEFAULT.foreground(AttributedStyle.CYAN));
+                }
+            } else {
+                sb.style(AttributedStyle.DEFAULT.boldOff().foregroundOff());
+            }
+        }
+    }
+}
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/ShellInvokerRequest.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/ShellInvokerRequest.java
new file mode 100644
index 0000000000..9866170144
--- /dev/null
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/ShellInvokerRequest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.mvnsh;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Path;
+import java.util.List;
+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.mvnsh.ShellOptions;
+import org.apache.maven.cling.invoker.BaseInvokerRequest;
+
+import static java.util.Objects.requireNonNull;
+
+public class ShellInvokerRequest extends BaseInvokerRequest {
+    private final ShellOptions options;
+
+    @SuppressWarnings("ParameterNumber")
+    public ShellInvokerRequest(
+            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,
+            ShellOptions options) {
+        super(
+                parserRequest,
+                cwd,
+                installationDirectory,
+                userHomeDirectory,
+                userProperties,
+                systemProperties,
+                topDirectory,
+                rootDirectory,
+                in,
+                out,
+                err,
+                coreExtensions,
+                jvmArguments);
+        this.options = requireNonNull(options);
+    }
+
+    /**
+     * The mandatory Shell options.
+     */
+    @Nonnull
+    public ShellOptions options() {
+        return options;
+    }
+}
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/ShellParser.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/ShellParser.java
new file mode 100644
index 0000000000..105e6b018a
--- /dev/null
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/ShellParser.java
@@ -0,0 +1,68 @@
+/*
+ * 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.mvnsh;
+
+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.mvnsh.ShellOptions;
+import org.apache.maven.cling.invoker.BaseParser;
+
+public class ShellParser extends BaseParser {
+    @Override
+    protected ShellInvokerRequest getInvokerRequest(LocalContext context) {
+        return new ShellInvokerRequest(
+                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),
+                (ShellOptions) context.options);
+    }
+
+    @Override
+    protected List<Options> parseCliOptions(LocalContext context) throws 
ParserException {
+        return 
Collections.singletonList(parseShellCliOptions(context.parserRequest.args()));
+    }
+
+    protected CommonsCliShellOptions parseShellCliOptions(List<String> args) 
throws ParserException {
+        try {
+            return CommonsCliShellOptions.parse(args.toArray(new String[0]));
+        } catch (ParseException e) {
+            throw new ParserException("Failed to parse command line options: " 
+ e.getMessage(), e);
+        }
+    }
+
+    @Override
+    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/mvnsh/builtin/BuiltinShellCommandRegistryFactory.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/builtin/BuiltinShellCommandRegistryFactory.java
new file mode 100644
index 0000000000..03b82380e2
--- /dev/null
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/builtin/BuiltinShellCommandRegistryFactory.java
@@ -0,0 +1,186 @@
+/*
+ * 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.mvnsh.builtin;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+
+import org.apache.maven.api.cli.ParserRequest;
+import org.apache.maven.api.di.Named;
+import org.apache.maven.api.di.Singleton;
+import org.apache.maven.cling.invoker.LookupContext;
+import org.apache.maven.cling.invoker.mvn.MavenInvoker;
+import org.apache.maven.cling.invoker.mvn.MavenParser;
+import org.apache.maven.cling.invoker.mvnenc.EncryptInvoker;
+import org.apache.maven.cling.invoker.mvnenc.EncryptParser;
+import org.apache.maven.cling.invoker.mvnsh.ShellCommandRegistryFactory;
+import org.jline.builtins.Completers;
+import org.jline.builtins.Options;
+import org.jline.console.CmdDesc;
+import org.jline.console.CommandInput;
+import org.jline.console.CommandMethods;
+import org.jline.console.CommandRegistry;
+import org.jline.console.impl.AbstractCommandRegistry;
+import org.jline.reader.Completer;
+import org.jline.reader.impl.completer.ArgumentCompleter;
+import org.jline.reader.impl.completer.NullCompleter;
+
+import static java.util.Objects.requireNonNull;
+import static 
org.jline.console.impl.JlineCommandRegistry.compileCommandOptions;
+
+@Named("builtin")
+@Singleton
+public class BuiltinShellCommandRegistryFactory implements 
ShellCommandRegistryFactory {
+    public CommandRegistry createShellCommandRegistry(LookupContext context) {
+        return new BuiltinShellCommandRegistry(context);
+    }
+
+    private static class BuiltinShellCommandRegistry extends 
AbstractCommandRegistry implements AutoCloseable {
+        public enum Command {
+            MVN,
+            MVNENC
+        }
+
+        private final LookupContext shellContext;
+        private final MavenInvoker shellMavenInvoker;
+        private final MavenParser mavenParser;
+        private final EncryptInvoker shellEncryptInvoker;
+        private final EncryptParser encryptParser;
+
+        private BuiltinShellCommandRegistry(LookupContext shellContext) {
+            this.shellContext = requireNonNull(shellContext, "shellContext");
+            this.shellMavenInvoker = new 
MavenInvoker(shellContext.invokerRequest.lookup(), contextCopier());
+            this.mavenParser = new MavenParser();
+            this.shellEncryptInvoker = new 
EncryptInvoker(shellContext.invokerRequest.lookup(), contextCopier());
+            this.encryptParser = new EncryptParser();
+            Set<Command> commands = new 
HashSet<>(EnumSet.allOf(Command.class));
+            Map<Command, String> commandName = new HashMap<>();
+            Map<Command, CommandMethods> commandExecute = new HashMap<>();
+            for (Command c : commands) {
+                commandName.put(c, c.name().toLowerCase());
+            }
+            commandExecute.put(Command.MVN, new CommandMethods(this::mvn, 
this::mvnCompleter));
+            commandExecute.put(Command.MVNENC, new 
CommandMethods(this::mvnenc, this::mvnencCompleter));
+            registerCommands(commandName, commandExecute);
+        }
+
+        private Consumer<LookupContext> contextCopier() {
+            return result -> {
+                result.logger = shellContext.logger;
+                result.loggerFactory = shellContext.loggerFactory;
+                result.slf4jConfiguration = shellContext.slf4jConfiguration;
+                result.loggerLevel = shellContext.loggerLevel;
+                result.coloredOutput = shellContext.coloredOutput;
+                result.terminal = shellContext.terminal;
+                result.writer = shellContext.writer;
+
+                result.installationSettingsPath = 
shellContext.installationSettingsPath;
+                result.projectSettingsPath = shellContext.projectSettingsPath;
+                result.userSettingsPath = shellContext.userSettingsPath;
+                result.interactive = shellContext.interactive;
+                result.localRepositoryPath = shellContext.localRepositoryPath;
+                result.effectiveSettings = shellContext.effectiveSettings;
+
+                result.containerCapsule = shellContext.containerCapsule;
+                result.lookup = shellContext.lookup;
+                result.eventSpyDispatcher = shellContext.eventSpyDispatcher;
+            };
+        }
+
+        @Override
+        public void close() throws Exception {
+            shellMavenInvoker.close();
+            shellEncryptInvoker.close();
+        }
+
+        @Override
+        public List<String> commandInfo(String command) {
+            return List.of();
+        }
+
+        @Override
+        public CmdDesc commandDescription(List<String> args) {
+            return null;
+        }
+
+        @Override
+        public String name() {
+            return "Builtin Maven Shell commands";
+        }
+
+        private List<Completers.OptDesc> commandOptions(String command) {
+            try {
+                invoke(new CommandSession(), command, "--help");
+            } catch (Options.HelpException e) {
+                return compileCommandOptions(e.getMessage());
+            } catch (Exception e) {
+                // ignore
+            }
+            return null;
+        }
+
+        private void mvn(CommandInput input) {
+            try {
+                
shellMavenInvoker.invoke(mavenParser.parseInvocation(ParserRequest.mvn(
+                                input.args(),
+                                shellContext.invokerRequest.logger(),
+                                
shellContext.invokerRequest.messageBuilderFactory())
+                        .build()));
+            } catch (Exception e) {
+                saveException(e);
+            }
+        }
+
+        private List<Completer> mvnCompleter(String name) {
+            List<Completer> completers = new ArrayList<>();
+            completers.add(new ArgumentCompleter(
+                    NullCompleter.INSTANCE,
+                    new Completers.OptionCompleter(
+                            new 
Completers.FilesCompleter(shellContext.invokerRequest::cwd), 
this::commandOptions, 1)));
+            return completers;
+        }
+
+        private void mvnenc(CommandInput input) {
+            try {
+                
shellEncryptInvoker.invoke(encryptParser.parseInvocation(ParserRequest.mvnenc(
+                                input.args(),
+                                shellContext.invokerRequest.logger(),
+                                
shellContext.invokerRequest.messageBuilderFactory())
+                        .build()));
+            } catch (Exception e) {
+                saveException(e);
+            }
+        }
+
+        private List<Completer> mvnencCompleter(String name) {
+            List<Completer> completers = new ArrayList<>();
+            completers.add(new ArgumentCompleter(
+                    NullCompleter.INSTANCE,
+                    new Completers.OptionCompleter(
+                            new 
Completers.FilesCompleter(shellContext.invokerRequest::cwd), 
this::commandOptions, 1)));
+            return completers;
+        }
+    }
+}
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/local/LocalMavenInvoker.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/package-info.java
similarity index 52%
rename from 
impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/local/LocalMavenInvoker.java
rename to 
impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/package-info.java
index 3ac8cc783b..6e14380c50 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/local/LocalMavenInvoker.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/package-info.java
@@ -16,24 +16,8 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.maven.cling.invoker.mvn.local;
-
-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.MavenContext;
-import org.apache.maven.cling.invoker.mvn.MavenInvoker;
 
 /**
- * Local Maven invoker implementation, that expects all the Maven to be on 
classpath.
+ * This package contains the {@code mvnsh} tool implementation.
  */
-public class LocalMavenInvoker extends MavenInvoker<MavenContext> {
-    public LocalMavenInvoker(ProtoLookup protoLookup) {
-        super(protoLookup);
-    }
-
-    @Override
-    protected MavenContext createContext(InvokerRequest invokerRequest) throws 
InvokerException {
-        return new MavenContext(invokerRequest);
-    }
-}
+package org.apache.maven.cling.invoker.mvnsh;
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/package-info.java 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/package-info.java
index b169eba8db..3156066d14 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/package-info.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/package-info.java
@@ -20,5 +20,17 @@
 /**
  * This package contain support (mostly abstract) classes, that implement 
"base" of CLIng.
  * In packages below you find actual implementations.
+ *
+ * Hierarchy:
+ * <ul>
+ * <li>{@link org.apache.maven.cling.invoker.LookupInvoker} is the "basis", 
the common ground of all Maven Tools</li>
+ * <li>extended by {@link org.apache.maven.cling.invoker.mvn.MavenInvoker} is 
the "mvn Tool"</li>
+ * <li>extended by {@link 
org.apache.maven.cling.invoker.mvnenc.EncryptInvoker} is the "mvnenc Tool"</li>
+ * <li>extended by {@link org.apache.maven.cling.invoker.mvnsh.ShellInvoker} 
is the "mvnsh Tool"</li>
+ * </ul>
+ *
+ * There is one specialization of {@link 
org.apache.maven.cling.invoker.mvn.MavenInvoker}, the "resident"
+ * {@link org.apache.maven.cling.invoker.mvn.resident.ResidentMavenInvoker}. 
The difference is that this invoker
+ * will on close "clean up" (tear down) the instance. All invokers are 
re-entrant.
  */
 package org.apache.maven.cling.invoker;
diff --git 
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/local/LocalMavenInvokerTest.java
 
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenInvokerTest.java
similarity index 87%
rename from 
impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/local/LocalMavenInvokerTest.java
rename to 
impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenInvokerTest.java
index 11b8a4d878..54d6fb147b 100644
--- 
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/local/LocalMavenInvokerTest.java
+++ 
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenInvokerTest.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.maven.cling.invoker.mvn.local;
+package org.apache.maven.cling.invoker.mvn;
 
 import java.nio.file.FileSystem;
 import java.nio.file.Path;
@@ -27,8 +27,6 @@ import com.google.common.jimfs.Jimfs;
 import org.apache.maven.api.cli.Invoker;
 import org.apache.maven.api.cli.Parser;
 import org.apache.maven.cling.invoker.ProtoLookup;
-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.Order;
@@ -40,10 +38,10 @@ import org.junit.jupiter.api.io.TempDir;
  * Local UT.
  */
 @Order(200)
-public class LocalMavenInvokerTest extends MavenInvokerTestSupport {
+public class MavenInvokerTest extends MavenInvokerTestSupport {
     @Override
     protected Invoker createInvoker() {
-        return new LocalMavenInvoker(ProtoLookup.builder()
+        return new MavenInvoker(ProtoLookup.builder()
                 .addMapping(ClassWorld.class, new ClassWorld("plexus.core", 
ClassLoader.getSystemClassLoader()))
                 .build());
     }
diff --git 
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/forked/ForkedMavenInvokerTest.java
 
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/forked/ForkedMavenInvokerTest.java
deleted file mode 100644
index 56c1cd2388..0000000000
--- 
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/forked/ForkedMavenInvokerTest.java
+++ /dev/null
@@ -1,53 +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.nio.file.Path;
-import java.util.Arrays;
-
-import org.apache.maven.api.cli.Invoker;
-import org.apache.maven.api.cli.Parser;
-import org.apache.maven.cling.invoker.mvn.MavenInvokerTestSupport;
-import org.apache.maven.cling.invoker.mvn.MavenParser;
-import org.junit.jupiter.api.Order;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.io.CleanupMode;
-import org.junit.jupiter.api.io.TempDir;
-
-/**
- * Forked UT: it cannot use jimFS as it runs in child process.
- */
-@Order(300)
-public class ForkedMavenInvokerTest extends MavenInvokerTestSupport {
-
-    @Override
-    protected Invoker createInvoker() {
-        return new ForkedMavenInvoker();
-    }
-
-    @Override
-    protected Parser createParser() {
-        return new MavenParser();
-    }
-
-    @Test
-    void defaultFs(@TempDir(cleanup = CleanupMode.ON_SUCCESS) Path tempDir) 
throws Exception {
-        invoke(tempDir, Arrays.asList("clean", "verify"));
-    }
-}
diff --git 
a/impl/maven-executor/src/main/java/org/apache/maven/cling/executor/embedded/EmbeddedMavenExecutor.java
 
b/impl/maven-executor/src/main/java/org/apache/maven/cling/executor/embedded/EmbeddedMavenExecutor.java
index 0bbacf9f66..e713a73a43 100644
--- 
a/impl/maven-executor/src/main/java/org/apache/maven/cling/executor/embedded/EmbeddedMavenExecutor.java
+++ 
b/impl/maven-executor/src/main/java/org/apache/maven/cling/executor/embedded/EmbeddedMavenExecutor.java
@@ -126,7 +126,8 @@ public class EmbeddedMavenExecutor implements Executor {
         this.originalStderr = System.err;
         this.originalClassLoader = 
Thread.currentThread().getContextClassLoader();
         this.contexts = new ConcurrentHashMap<>();
-        this.originalProperties = System.getProperties();
+        this.originalProperties = new Properties();
+        this.originalProperties.putAll(System.getProperties());
     }
 
     @Override
@@ -261,7 +262,6 @@ public class EmbeddedMavenExecutor implements Executor {
                 Class<?>[] parameterTypes = {String[].class, String.class, 
PrintStream.class, PrintStream.class};
                 Method doMain = cliClass.getMethod("doMain", parameterTypes);
                 exec = r -> {
-                    System.setProperties(null);
                     System.setProperties(prepareProperties(r));
                     try {
                         return (int) doMain.invoke(mavenCli, new Object[] {
@@ -278,7 +278,6 @@ public class EmbeddedMavenExecutor implements Executor {
                 Field ansiConsoleInstalled = 
ansiConsole.getDeclaredField("installed");
                 ansiConsoleInstalled.setAccessible(true);
                 exec = r -> {
-                    System.setProperties(null);
                     System.setProperties(prepareProperties(r));
                     try {
                         try {
@@ -310,8 +309,10 @@ public class EmbeddedMavenExecutor implements Executor {
     }
 
     protected Properties prepareProperties(ExecutorRequest request) {
+        System.setProperties(null); // this "inits" them!
+
         Properties properties = new Properties();
-        properties.putAll(System.getProperties());
+        properties.putAll(System.getProperties()); // get mandatory/expected 
init-ed above
 
         properties.setProperty("user.dir", request.cwd().toString());
         properties.setProperty("user.home", 
request.userHomeDirectory().toString());
diff --git a/impl/maven-jline/pom.xml b/impl/maven-jline/pom.xml
index 1dd607d85c..da3a9ec6c8 100644
--- a/impl/maven-jline/pom.xml
+++ b/impl/maven-jline/pom.xml
@@ -52,6 +52,10 @@ under the License.
       <groupId>org.jline</groupId>
       <artifactId>jline-builtins</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.jline</groupId>
+      <artifactId>jline-console</artifactId>
+    </dependency>
     <dependency>
       <groupId>org.jline</groupId>
       <artifactId>jline-console-ui</artifactId>
diff --git a/pom.xml b/pom.xml
index e54f1f3ad1..ffe62c9954 100644
--- a/pom.xml
+++ b/pom.xml
@@ -452,6 +452,11 @@ under the License.
         <artifactId>jline-builtins</artifactId>
         <version>${jlineVersion}</version>
       </dependency>
+      <dependency>
+        <groupId>org.jline</groupId>
+        <artifactId>jline-console</artifactId>
+        <version>${jlineVersion}</version>
+      </dependency>
       <dependency>
         <groupId>org.jline</groupId>
         <artifactId>jline-console-ui</artifactId>

Reply via email to