Fixed OSGI Shell broken in 00bbae07f2

- optional packages, not provided - they serve as an alternative
- cleanup
- without previous commit it was impossible to find the bug - in fact there
  were two:
  - enhanceForTarget originally returned it's parameter, not null
  - inaccessible imports

Signed-off-by: David Matějček <dmatej@seznam.cz>
diff --git a/nucleus/common/common-util/osgi.bundle b/nucleus/common/common-util/osgi.bundle
index cda35f5..3f03166 100644
--- a/nucleus/common/common-util/osgi.bundle
+++ b/nucleus/common/common-util/osgi.bundle
@@ -46,10 +46,11 @@
                         com.sun.enterprise.util.zip; \
                         com.sun.logging; \
                         org.glassfish.admin.payload; \
-                        org.glassfish.common.util.admin; \
                         org.glassfish.common.util; \
-                        org.glassfish.quality; \
+                        org.glassfish.common.util.admin; \
+                        org.glassfish.common.util.io; \
                         org.glassfish.common.util.timer; \
+                        org.glassfish.quality; \
                         org.glassfish.security.common; \
                         com.sun.logging.enterprise.system.core; \
                         org.glassfish.server; version=${project.osgi.version}
diff --git a/nucleus/common/common-util/src/main/java/org/glassfish/common/util/io/EmptyInputStream.java b/nucleus/common/common-util/src/main/java/org/glassfish/common/util/io/EmptyInputStream.java
new file mode 100644
index 0000000..7289af9
--- /dev/null
+++ b/nucleus/common/common-util/src/main/java/org/glassfish/common/util/io/EmptyInputStream.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2022 Eclipse Foundation and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.common.util.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Input stream which doesn't contain anything.
+ *
+ * @author David Matejcek
+ */
+public class EmptyInputStream extends InputStream {
+
+    @Override
+    public int read() throws IOException {
+        return -1;
+    }
+
+
+    @Override
+    public int available() throws IOException {
+        return 0;
+    }
+
+
+    @Override
+    public int read(final byte[] b) throws IOException {
+        return -1;
+    }
+
+
+    @Override
+    public int read(final byte[] b, final int off, final int len) throws IOException {
+        return -1;
+    }
+}
diff --git a/nucleus/common/common-util/src/main/java/org/glassfish/common/util/io/EmptyOutputStream.java b/nucleus/common/common-util/src/main/java/org/glassfish/common/util/io/EmptyOutputStream.java
new file mode 100644
index 0000000..5f3e07e
--- /dev/null
+++ b/nucleus/common/common-util/src/main/java/org/glassfish/common/util/io/EmptyOutputStream.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2022 Eclipse Foundation and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.common.util.io;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+
+/**
+ * Ignores all input, so is always empty.
+ *
+ * @author David Matejcek
+ */
+public class EmptyOutputStream extends OutputStream {
+
+    @Override
+    public void write(int b) throws IOException {
+        return;
+    }
+
+
+    @Override
+    public void write(byte[] b) throws IOException {
+        return;
+    }
+
+
+    @Override
+    public void write(byte[] b, int off, int len) throws IOException {
+        return;
+    }
+}
diff --git a/nucleus/osgi-platforms/osgi-cli-interactive/src/main/java/org/glassfish/osgi/cli/interactive/LocalOSGiShellCommand.java b/nucleus/osgi-platforms/osgi-cli-interactive/src/main/java/org/glassfish/osgi/cli/interactive/LocalOSGiShellCommand.java
index 2830c98..15491a7 100644
--- a/nucleus/osgi-platforms/osgi-cli-interactive/src/main/java/org/glassfish/osgi/cli/interactive/LocalOSGiShellCommand.java
+++ b/nucleus/osgi-platforms/osgi-cli-interactive/src/main/java/org/glassfish/osgi/cli/interactive/LocalOSGiShellCommand.java
@@ -18,13 +18,14 @@
 package org.glassfish.osgi.cli.interactive;
 
 import com.sun.enterprise.admin.cli.ArgumentTokenizer;
+import com.sun.enterprise.admin.cli.ArgumentTokenizer.ArgumentException;
 import com.sun.enterprise.admin.cli.CLICommand;
 import com.sun.enterprise.admin.cli.CLIUtil;
 import com.sun.enterprise.admin.cli.Environment;
 import com.sun.enterprise.admin.cli.MultimodeCommand;
 import com.sun.enterprise.admin.cli.ProgramOptions;
 import com.sun.enterprise.admin.cli.remote.RemoteCLICommand;
-import com.sun.enterprise.admin.util.CommandModelData;
+import com.sun.enterprise.admin.util.CommandModelData.ParamModelData;
 import com.sun.enterprise.universal.i18n.LocalStringsImpl;
 
 import jakarta.inject.Inject;
@@ -33,26 +34,28 @@
 import java.io.FileDescriptor;
 import java.io.FileInputStream;
 import java.io.IOException;
-import java.io.OutputStream;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.logging.Level;
 
 import org.glassfish.api.I18n;
 import org.glassfish.api.Param;
 import org.glassfish.api.admin.CommandException;
 import org.glassfish.api.admin.CommandModel.ParamModel;
 import org.glassfish.api.admin.CommandValidationException;
-import org.glassfish.api.admin.InvalidCommandException;
+import org.glassfish.common.util.io.EmptyOutputStream;
 import org.glassfish.hk2.api.ActiveDescriptor;
 import org.glassfish.hk2.api.DynamicConfiguration;
 import org.glassfish.hk2.api.DynamicConfigurationService;
+import org.glassfish.hk2.api.MultiException;
 import org.glassfish.hk2.api.PerLookup;
 import org.glassfish.hk2.api.ServiceLocator;
-import org.glassfish.hk2.utilities.BuilderHelper;
 import org.jline.reader.Completer;
+import org.jline.reader.EndOfFileException;
 import org.jline.reader.LineReader;
 import org.jline.reader.LineReaderBuilder;
 import org.jline.reader.impl.completer.NullCompleter;
@@ -61,6 +64,9 @@
 import org.jline.terminal.TerminalBuilder;
 import org.jvnet.hk2.annotations.Service;
 
+import static org.glassfish.hk2.utilities.BuilderHelper.createConstantDescriptor;
+import static org.glassfish.hk2.utilities.BuilderHelper.createContractFilter;
+
 /**
  * A simple local asadmin sub-command to establish an interactive osgi shell.
  *
@@ -76,12 +82,12 @@
 @PerLookup
 public class LocalOSGiShellCommand extends CLICommand {
 
-    protected static final String REMOTE_COMMAND = "osgi";
-    protected static final String SESSIONID_OPTION = "--session-id";
-    protected static final String SESSION_OPTION = "--session";
-    protected static final String SESSION_OPTION_EXECUTE = "execute";
-    protected static final String SESSION_OPTION_START = "new";
-    protected static final String SESSION_OPTION_STOP = "stop";
+    private static final String REMOTE_COMMAND = "osgi";
+    private static final String SESSIONID_OPTION = "--session-id";
+    private static final String SESSION_OPTION = "--session";
+    private static final String SESSION_OPTION_EXECUTE = "execute";
+    private static final String SESSION_OPTION_START = "new";
+    private static final String SESSION_OPTION_STOP = "stop";
     private static final LocalStringsImpl STRINGS = new LocalStringsImpl(MultimodeCommand.class);
 
     @Inject
@@ -104,58 +110,22 @@
     private String shellType;
 
 
-    protected String[] enhanceForTarget(String[] args) {
-        if (instance == null) {
-            return null;
+    @Override
+    public void postConstruct() {
+        super.postConstruct();
+        try {
+            cmd = new RemoteCLICommand(REMOTE_COMMAND, locator.<ProgramOptions> getService(ProgramOptions.class),
+                locator.<Environment> getService(Environment.class));
+        } catch (MultiException | CommandException e) {
+            logger.log(Level.SEVERE, "postConstruct failed!", e);
         }
-        String[] targetArgs = new String[args.length + 2];
-        targetArgs[1] = "--instance";
-        targetArgs[2] = instance;
-        System.arraycopy(args, 0, targetArgs, 0, 1);
-        System.arraycopy(args, 1, targetArgs, 3, args.length - 1);
-        return targetArgs;
     }
 
-    protected String[] prepareArguments(String sessionId, String[] args) {
-        if (sessionId == null) {
-            String[] osgiArgs = new String[args.length + 1];
-            osgiArgs[0] = REMOTE_COMMAND;
-            System.arraycopy(args, 0, osgiArgs, 1, args.length);
-            args = osgiArgs;
-        } else {
-            // attach command to remote session...
-            String[] sessionArgs = new String[args.length + 5];
-            sessionArgs[0] = REMOTE_COMMAND;
-            sessionArgs[1] = SESSION_OPTION;
-            sessionArgs[2] = SESSION_OPTION_EXECUTE;
-            sessionArgs[3] = SESSIONID_OPTION;
-            sessionArgs[4] = sessionId;
-            System.arraycopy(args, 0, sessionArgs, 5, args.length);
-            args = sessionArgs;
-        }
-        return args;
-    }
-
-    protected String startSession() throws CommandException {
-        if (!"gogo".equals(shellType)) {
-            return null;
-        }
-        String[] args = {REMOTE_COMMAND, SESSION_OPTION, SESSION_OPTION_START};
-        return cmd.executeAndReturnOutput(enhanceForTarget(args)).trim();
-    }
-
-    protected int stopSession(String sessionId) throws CommandException {
-        if (sessionId == null) {
-            return 0;
-        }
-        String[] args = {REMOTE_COMMAND, SESSION_OPTION, SESSION_OPTION_STOP, SESSIONID_OPTION, sessionId};
-        return cmd.execute(enhanceForTarget(args));
-    }
 
     /**
      * The validate method validates that the type and quantity of
      * parameters and operands matches the requirements for this
-     * command.  The validate method supplies missing options from
+     * command. The validate method supplies missing options from
      * the environment.
      */
     @Override
@@ -174,9 +144,8 @@
     protected Collection<ParamModel> usageOptions() {
         Collection<ParamModel> opts = commandModel.getParameters();
         Set<ParamModel> uopts = new LinkedHashSet<>();
-        ParamModel p = new CommandModelData.ParamModelData("printprompt",
-                boolean.class, true,
-                Boolean.toString(programOpts.isInteractive()));
+        String interactive = Boolean.toString(programOpts.isInteractive());
+        ParamModel p = new ParamModelData("printprompt", boolean.class, true, interactive);
         for (ParamModel pm : opts) {
             if (pm.getName().equals("printprompt")) {
                 uopts.add(p);
@@ -192,49 +161,48 @@
         if (cmd == null) {
             throw new CommandException("Remote command 'osgi' is not available.");
         }
-        programOpts.setEcho(echo);       // restore echo flag, saved in validate
-            if (encoding != null) {
-                // see Configuration.getEncoding()...
-                System.setProperty("input.encoding", encoding);
-            }
-            final String[] args = enhanceForTarget(new String[] {REMOTE_COMMAND, "asadmin-osgi-shell"});
-            shellType = cmd.executeAndReturnOutput(args).trim();
+        programOpts.setEcho(echo);
+        // restore echo flag, saved in validate
+        if (encoding != null) {
+            // see Configuration.getEncoding()...
+            System.setProperty("input.encoding", encoding);
+        }
+        final String[] args = enhanceForTarget(new String[] {REMOTE_COMMAND, "asadmin-osgi-shell"});
+        logger.log(Level.FINEST, "executeCommand: args {0}", Arrays.toString(args));
+        shellType = cmd.executeAndReturnOutput(args).trim();
         try (Terminal terminal = createTerminal()) {
-            LineReader reader = LineReaderBuilder.builder().completer(getCommandCompleter()).appName(REMOTE_COMMAND).terminal(terminal).build();
+            LineReader reader = LineReaderBuilder.builder().completer(getCommandCompleter()).appName(REMOTE_COMMAND)
+                .terminal(terminal).build();
             return executeCommands(reader);
         } catch (IOException e) {
             throw new CommandException(e);
         }
     }
 
+    private String[] enhanceForTarget(String[] args) {
+        if (instance == null) {
+            return args;
+        }
+        String[] targetArgs = new String[args.length + 2];
+        targetArgs[1] = "--instance";
+        targetArgs[2] = instance;
+        System.arraycopy(args, 0, targetArgs, 0, 1);
+        System.arraycopy(args, 1, targetArgs, 3, args.length - 1);
+        return targetArgs;
+    }
+
 
     private Terminal createTerminal() throws IOException, CommandException {
         if (file == null) {
             System.out.println(STRINGS.get("multimodeIntro"));
-            return TerminalBuilder.builder().streams(new FileInputStream(FileDescriptor.in), System.out).build();
+            FileInputStream inputStream = new FileInputStream(FileDescriptor.in);
+            return TerminalBuilder.builder().streams(inputStream, System.out).build();
         }
         if (!file.canRead()) {
             throw new CommandException("File: " + file + " can not be read");
         }
 
-        OutputStream out = new OutputStream() {
-
-            @Override
-            public void write(int b) throws IOException {
-                return;
-            }
-
-            @Override
-            public void write(byte[] b) throws IOException {
-                return;
-            }
-
-            @Override
-            public void write(byte[] b, int off, int len) throws IOException {
-                return;
-            }
-        };
-        return TerminalBuilder.builder().streams(new FileInputStream(file), out).build();
+        return TerminalBuilder.builder().streams(new FileInputStream(file), new EmptyOutputStream()).build();
     }
 
 
@@ -244,7 +212,7 @@
      * @return The command completer
      */
     private Completer getCommandCompleter() {
-        if("gogo".equals(shellType)) {
+        if ("gogo".equals(shellType)) {
             return new StringsCompleter(
                     "bundlelevel",
                     "cd",
@@ -285,7 +253,7 @@
                     "repos",
                     "source"
                     );
-        } else if("felix".equals(shellType)) {
+        } else if ("felix".equals(shellType)) {
             return new StringsCompleter(
                     "exit",
                     "quit",
@@ -336,49 +304,44 @@
         String sessionId = startSession();
 
         try {
-            for (;;) {
-                if (isPromptPrinted()) {
-                    line = reader.readLine(shellType + "$ ");
-                } else {
-                    line = reader.readLine();
+            while (true) {
+                try {
+                    if (isPromptPrinted()) {
+                        line = reader.readLine(shellType + "$ ");
+                    } else {
+                        line = reader.readLine();
+                    }
+                } catch (EndOfFileException e) {
+                    break;
                 }
 
-                if (line == null) {
+                if (line == null || line.isBlank()) {
                     if (isPromptPrinted()) {
                         System.out.println();
                     }
                     break;
                 }
 
-                if (line.trim().startsWith("#")) // ignore comment lines
-                {
+                if (line.trim().startsWith("#")) {
+                    // ignore comment lines
                     continue;
                 }
 
-                String[] args = null;
+                final String[] args;
                 try {
                     args = getArgs(line);
-                } catch (ArgumentTokenizer.ArgumentException ex) {
-                    logger.info(ex.getMessage());
+                } catch (ArgumentException ex) {
+                    logger.severe(ex.getMessage());
                     continue;
                 }
 
-                if (args.length == 0) {
-                    continue;
-                }
-
-                String command = args[0];
-                if (command.trim().length() == 0) {
-                    continue;
-                }
-
+                final String command = args[0];
                 // handle built-in exit and quit commands
-                // XXX - care about their arguments?
-                if (command.equals("exit") || command.equals("quit")) {
+                if ("exit".equals(command) || "quit".equals(command)) {
                     break;
                 }
 
-                ProgramOptions po = null;
+                final String[] arguments = enhanceForTarget(prepareArguments(sessionId, args));
                 try {
                     /*
                      * Every command gets its own copy of program options
@@ -386,28 +349,21 @@
                      * command line options don't effect other commands.
                      * But all commands share the same environment.
                      */
-                    po = new ProgramOptions(env);
+                    final ProgramOptions programOptions = new ProgramOptions(env);
                     // copy over AsadminMain info
-                    po.setClassPath(programOpts.getClassPath());
-                    po.setClassName(programOpts.getClassName());
+                    programOptions.setClassPath(programOpts.getClassPath());
+                    programOptions.setClassName(programOpts.getClassName());
                     // remove the old one and replace it
-                    atomicReplace(locator, po);
+                    atomicReplace(locator, programOptions);
 
-                    args = prepareArguments(sessionId, args);
-
-                    args = enhanceForTarget(args);
-
-                    String output = cmd.executeAndReturnOutput(args).trim();
-                    if(output != null && output.length() > 0) {
+                    String output = cmd.executeAndReturnOutput(arguments).trim();
+                    if (output != null && !output.isEmpty()) {
                         logger.info(output);
                     }
                 } catch (CommandValidationException cve) {
                     logger.severe(cve.getMessage());
                     logger.severe(cmd.getUsage());
                     rc = ERROR;
-                } catch (InvalidCommandException ice) {
-                    // find closest match with local or remote commands
-                    logger.severe(ice.getMessage());
                 } catch (CommandException ce) {
                     logger.severe(ce.getMessage());
                     rc = ERROR;
@@ -416,8 +372,7 @@
                     // XXX - is this necessary?
                     atomicReplace(locator, programOpts);
                 }
-
-                CLIUtil.writeCommandToDebugLog(name, env, args, rc);
+                CLIUtil.writeCommandToDebugLog(name, env, arguments, rc);
             }
         } finally {
             // what if something breaks on the wire?
@@ -426,24 +381,64 @@
         return rc;
     }
 
+
+    private String[] prepareArguments(String sessionId, String[] args) {
+        if (sessionId == null) {
+            String[] osgiArgs = args == null ? new String[1] : new String[args.length + 1];
+            osgiArgs[0] = REMOTE_COMMAND;
+            if (args != null && args.length > 0) {
+                System.arraycopy(args, 0, osgiArgs, 1, args.length);
+            }
+            return osgiArgs;
+        }
+        // attach command to remote session...
+        String[] sessionArgs = args == null ? new String[5] : new String[args.length + 5];
+        sessionArgs[0] = REMOTE_COMMAND;
+        sessionArgs[1] = SESSION_OPTION;
+        sessionArgs[2] = SESSION_OPTION_EXECUTE;
+        sessionArgs[3] = SESSIONID_OPTION;
+        sessionArgs[4] = sessionId;
+        if (args != null && args.length > 0) {
+            System.arraycopy(args, 0, sessionArgs, 5, args.length);
+        }
+        return sessionArgs;
+    }
+
+
+    private String startSession() throws CommandException {
+        if (!"gogo".equals(shellType)) {
+            return null;
+        }
+        String[] args = {REMOTE_COMMAND, SESSION_OPTION, SESSION_OPTION_START};
+        return cmd.executeAndReturnOutput(enhanceForTarget(args)).trim();
+    }
+
+
+    private int stopSession(String sessionId) throws CommandException {
+        if (sessionId == null) {
+            return 0;
+        }
+        String[] args = {REMOTE_COMMAND, SESSION_OPTION, SESSION_OPTION_STOP, SESSIONID_OPTION, sessionId};
+        return cmd.execute(enhanceForTarget(args));
+    }
+
+
     private boolean isPromptPrinted() {
         return file == null;
     }
 
+
     private static void atomicReplace(ServiceLocator locator, ProgramOptions options) {
         DynamicConfigurationService dcs = locator.getService(DynamicConfigurationService.class);
         DynamicConfiguration config = dcs.createDynamicConfiguration();
-
-        config.addUnbindFilter(BuilderHelper.createContractFilter(ProgramOptions.class.getName()));
-        ActiveDescriptor<ProgramOptions> desc = BuilderHelper.createConstantDescriptor(
-                options, null, ProgramOptions.class);
+        config.addUnbindFilter(createContractFilter(ProgramOptions.class.getName()));
+        ActiveDescriptor<ProgramOptions> desc = createConstantDescriptor(options, null, ProgramOptions.class);
         config.addActiveDescriptor(desc);
-
         config.commit();
     }
 
 
-    private String[] getArgs(String line) throws ArgumentTokenizer.ArgumentException {
+    private static String[] getArgs(String line) throws ArgumentException {
         List<String> args = new ArrayList<>();
         ArgumentTokenizer t = new ArgumentTokenizer(line);
         while (t.hasMoreTokens()) {
@@ -451,16 +446,4 @@
         }
         return args.toArray(new String[args.size()]);
     }
-
-    @Override
-    public void postConstruct() {
-        super.postConstruct();
-        try {
-            cmd = new RemoteCLICommand(REMOTE_COMMAND,
-                locator.<ProgramOptions>getService(ProgramOptions.class),
-                locator.<Environment>getService(Environment.class));
-        } catch (CommandException ex) {
-            // ignore - will be handled by execute()
-        }
-    }
 }
diff --git a/nucleus/osgi-platforms/osgi-cli-remote/pom.xml b/nucleus/osgi-platforms/osgi-cli-remote/pom.xml
index ce53c33..0fc1d81 100644
--- a/nucleus/osgi-platforms/osgi-cli-remote/pom.xml
+++ b/nucleus/osgi-platforms/osgi-cli-remote/pom.xml
@@ -53,15 +53,19 @@
             <artifactId>admin-util</artifactId>
             <version>${project.version}</version>
         </dependency>
+
+        <!-- Alternatives -->
         <dependency>
             <groupId>org.apache.felix</groupId>
             <artifactId>org.apache.felix.shell</artifactId>
             <scope>provided</scope>
+            <optional>true</optional>
         </dependency>
         <dependency>
             <groupId>org.apache.felix</groupId>
             <artifactId>org.apache.felix.gogo.runtime</artifactId>
             <scope>provided</scope>
+            <optional>true</optional>
         </dependency>
     </dependencies>
 </project>
diff --git a/nucleus/osgi-platforms/osgi-cli-remote/src/main/java/org/glassfish/osgi/cli/remote/OSGiShellCommand.java b/nucleus/osgi-platforms/osgi-cli-remote/src/main/java/org/glassfish/osgi/cli/remote/OSGiShellCommand.java
index e331bf1..28d2c21 100644
--- a/nucleus/osgi-platforms/osgi-cli-remote/src/main/java/org/glassfish/osgi/cli/remote/OSGiShellCommand.java
+++ b/nucleus/osgi-platforms/osgi-cli-remote/src/main/java/org/glassfish/osgi/cli/remote/OSGiShellCommand.java
@@ -1,4 +1,5 @@
 /*
+ * Copyright (c) 2022 Contributors to the Eclipse Foundation
  * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
@@ -19,21 +20,15 @@
 import com.sun.enterprise.admin.remote.ServerRemoteAdminCommand;
 import com.sun.enterprise.config.serverbeans.Domain;
 import com.sun.enterprise.config.serverbeans.Server;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.PrintStream;
+
+import jakarta.inject.Inject;
+
+import java.util.Arrays;
 import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.logging.Level;
 import java.util.logging.Logger;
-import jakarta.inject.Inject;
-import org.apache.felix.service.command.CommandProcessor;
-import org.apache.felix.service.command.CommandSession;
-import org.apache.felix.service.command.Result;
-import org.apache.felix.shell.ShellService;
+import java.util.stream.Collectors;
+
 import org.glassfish.api.ActionReport;
 import org.glassfish.api.I18n;
 import org.glassfish.api.Param;
@@ -50,11 +45,15 @@
 import org.glassfish.hk2.api.PerLookup;
 import org.glassfish.hk2.api.PostConstruct;
 import org.glassfish.hk2.api.ServiceLocator;
+import org.glassfish.osgi.cli.remote.impl.OsgiShellService;
+import org.glassfish.osgi.cli.remote.impl.SessionOperation;
 import org.jvnet.hk2.annotations.Service;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.BundleReference;
-import org.osgi.framework.ServiceReference;
+
+import static org.glassfish.osgi.cli.remote.impl.OsgiShellService.ASADMIN_OSGI_SHELL;
+import static org.glassfish.osgi.cli.remote.impl.OsgiShellServiceProvider.detectService;
 
 /**
  * A simple AdminCommand that bridges to the Felix Shell Service.
@@ -77,15 +76,12 @@
 @AccessRequired(resource="domain/osgi/shell", action="execute")
 public class OSGiShellCommand implements AdminCommand, PostConstruct {
 
-    private static final Logger log = Logger.getLogger(OSGiShellCommand.class.getPackage().getName());
-
-    private static final Map<String, RemoteCommandSession> sessions =
-            new ConcurrentHashMap<String, RemoteCommandSession>();
+    private static final Logger LOG = Logger.getLogger(OSGiShellCommand.class.getPackage().getName());
 
     @Param(name = "command-line", primary = true, optional = true, multiple = true, defaultValue = "help")
     private Object commandLine;
 
-    @Param(name = "session", optional = true)
+    @Param(name = "session", optional = true, acceptableValues = "new,list,execute,stop")
     private String sessionOp;
 
     @Param(name = "session-id", optional = true)
@@ -102,234 +98,94 @@
     @Inject
     Domain domain;
 
-    @Override
-    public void execute(AdminCommandContext context) {
-        ActionReport report = context.getActionReport();
-
-        if(instance != null) {
-            Server svr = domain.getServerNamed(instance);
-            if(svr == null) {
-                report.setMessage("No server target found for "
-                        + instance);
-                report.setActionExitCode(ActionReport.ExitCode.FAILURE);
-                return;
-            }
-            String host = svr.getAdminHost();
-            int port = svr.getAdminPort();
-
-            try {
-                ServerRemoteAdminCommand remote =
-                        new ServerRemoteAdminCommand(
-                                locator,
-                                "osgi",
-                                host,
-                                port,
-                                false,
-                                "admin",
-                                "",
-                                log);
-
-                ParameterMap params = new ParameterMap();
-
-                if(commandLine == null) {
-                    params.set("DEFAULT".toLowerCase(Locale.US), "asadmin-osgi-shell");
-                } else if(commandLine instanceof String) {
-                    params.set("DEFAULT".toLowerCase(Locale.US), (String) commandLine);
-                } else if(commandLine instanceof List) {
-                    params.set("DEFAULT".toLowerCase(Locale.US), (List<String>) commandLine);
-                }
-
-                if(sessionOp != null) {
-                    params.set("session", sessionOp);
-                }
-
-                if(sessionId != null) {
-                    params.set("session-id", sessionId);
-                }
-
-                report.setMessage(remote.executeCommand(params));
-                report.setActionExitCode(ActionReport.ExitCode.SUCCESS);
-                return;
-            } catch(CommandException x) {
-                report.setMessage("Remote execution failed: "
-                        + x.getMessage());
-                report.setFailureCause(x);
-                report.setActionExitCode(ActionReport.ExitCode.FAILURE);
-                return;
-            }
-        }
-
-        String cmdName = "";
-        String cmd = "";
-        if(commandLine == null) {
-            cmd = "asadmin-osgi-shell";
-            cmdName = cmd;
-        } else if(commandLine instanceof String) {
-            cmd = (String) commandLine;
-            cmdName = cmd;
-        } else if(commandLine instanceof List) {
-            for(Object arg : (List) commandLine) {
-                if(cmd.length() == 0) {
-                    // first arg
-                    cmd = (String) arg;
-                    cmdName = cmd;
-                } else {
-                    cmd += " " + (String) arg;
-                }
-            }
-        } else if(commandLine instanceof String[]) {
-            for(Object arg : (String[]) commandLine) {
-                if(cmd.length() == 0) {
-                    // first arg
-                    cmd = (String) arg;
-                    cmdName = cmd;
-                } else {
-                    cmd += " " + (String) arg;
-                }
-            }
-        } else {
-            // shouldn't happen...
-            report.setMessage("Unable to deal with argument list of type "
-                    + commandLine.getClass().getName());
-            report.setActionExitCode(ActionReport.ExitCode.WARNING);
-            return;
-        }
-
-        // standard output...
-        ByteArrayOutputStream bOut = new ByteArrayOutputStream(512);
-        PrintStream out = new PrintStream(bOut);
-
-        // error output...
-        ByteArrayOutputStream bErr = new ByteArrayOutputStream(512);
-        PrintStream err = new PrintStream(bErr);
-
-        try {
-            Object shell = null;
-
-            ServiceReference sref = ctx.getServiceReference(
-                    "org.apache.felix.service.command.CommandProcessor");
-            if(sref != null) {
-                shell = ctx.getService(sref);
-            }
-
-            if(shell == null) {
-                // try with felix...
-                sref = ctx.getServiceReference("org.apache.felix.shell.ShellService");
-                if(sref != null) {
-                    shell = ctx.getService(sref);
-                }
-
-                if(shell == null) {
-                    report.setMessage("No Shell Service available");
-                    report.setActionExitCode(ActionReport.ExitCode.WARNING);
-                    return;
-                } else if("asadmin-osgi-shell".equals(cmdName)) {
-                    out.println("felix");
-                } else {
-                    ShellService s = (ShellService) shell;
-                    s.executeCommand(cmd, out, err);
-                }
-            } else {
-                // try with gogo...
-
-                // GLASSFISH-19126 - prepare fake input stream...
-                InputStream in = new InputStream() {
-
-                    @Override
-                    public int read() throws IOException {
-                        return -1;
-                    }
-
-                    @Override
-                    public int available() throws IOException {
-                        return 0;
-                    }
-
-                    @Override
-                    public int read(byte[] b) throws IOException {
-                        return -1;
-                    }
-
-                    @Override
-                    public int read(byte[] b, int off, int len) throws IOException {
-                        return -1;
-                    }
-                };
-
-                CommandProcessor cp = (CommandProcessor) shell;
-                if (sessionOp == null) {
-                    if("asadmin-osgi-shell".equals(cmdName)) {
-                        out.println("gogo");
-                    } else {
-                        CommandSession session = cp.createSession(in, out, err);
-                        Object result = session.execute(cmd);
-
-                        if (result instanceof String) {
-                            out.println(result.toString());
-                        }
-
-                        session.close();
-                    }
-                } else if("new".equals(sessionOp)) {
-                    CommandSession session = cp.createSession(in, out, err);
-                    RemoteCommandSession remote = new RemoteCommandSession(session);
-
-                    log.log(Level.FINE, "Remote session established: {0}",
-                            remote.getId());
-
-                    sessions.put(remote.getId(), remote);
-                    out.println(remote.getId());
-                } else if("list".equals(sessionOp)) {
-                    for(String id : sessions.keySet()) {
-                        out.println(id);
-                    }
-                } else if("execute".equals(sessionOp)) {
-                    RemoteCommandSession remote = sessions.get(sessionId);
-                    CommandSession session = remote.attach(in, out, err);
-                    Object result = session.execute(cmd);
-
-                    if (result instanceof String) {
-                        out.println(result.toString());
-                    }
-
-                    remote.detach();
-                } else if("stop".equals(sessionOp)) {
-                    RemoteCommandSession remote = sessions.remove(sessionId);
-                    CommandSession session = remote.attach(in, out, err);
-                    session.close();
-
-                    log.log(Level.FINE, "Remote session closed: {0}",
-                            remote.getId());
-                }
-            }
-
-            out.flush();
-            err.flush();
-
-            String output = bOut.toString("UTF-8");
-            String errors = bErr.toString("UTF-8");
-            report.setMessage(output);
-
-            if(errors.length() > 0) {
-                report.setMessage(errors);
-                report.setActionExitCode(ActionReport.ExitCode.WARNING);
-            } else {
-                report.setActionExitCode(ActionReport.ExitCode.SUCCESS);
-            }
-        } catch (Exception ex) {
-            report.setMessage(ex.getMessage());
-            report.setActionExitCode(ActionReport.ExitCode.WARNING);
-        } finally {
-            out.close();
-            err.close();
-        }
-    }
 
     @Override
     public void postConstruct() {
-        if(ctx == null) {
-            Bundle me = BundleReference.class.cast(getClass().getClassLoader()).getBundle();
+        if (ctx == null) {
+            final Bundle me = BundleReference.class.cast(getClass().getClassLoader()).getBundle();
             ctx = me.getBundleContext();
         }
     }
+
+
+    @Override
+    public void execute(final AdminCommandContext context) {
+        final ActionReport report = context.getActionReport();
+
+        if (instance != null) {
+            final Server server = domain.getServerNamed(instance);
+            if (server == null) {
+                report.setMessage("No server target found for " + instance);
+                report.setActionExitCode(ActionReport.ExitCode.FAILURE);
+                return;
+            }
+            final String host = server.getAdminHost();
+            final int port = server.getAdminPort();
+            try {
+                final ServerRemoteAdminCommand remote
+                    = new ServerRemoteAdminCommand(locator, "osgi", host, port, false, "admin", "", LOG);
+
+                final ParameterMap params = new ParameterMap();
+                if (commandLine == null) {
+                    params.set("default", ASADMIN_OSGI_SHELL);
+                } else if (commandLine instanceof String) {
+                    params.set("default", (String) commandLine);
+                } else if (commandLine instanceof List) {
+                    params.set("default", (List<String>) commandLine);
+                }
+                if (sessionOp != null) {
+                    params.set("session", sessionOp);
+                }
+                if (sessionId != null) {
+                    params.set("session-id", sessionId);
+                }
+                report.setMessage(remote.executeCommand(params));
+                report.setActionExitCode(ActionReport.ExitCode.SUCCESS);
+                return;
+            } catch(final CommandException e) {
+                report.setMessage("Remote execution failed: " + e.getMessage());
+                report.setFailureCause(e);
+                report.setActionExitCode(ActionReport.ExitCode.FAILURE);
+                return;
+            }
+        }
+
+        // must not be null, called services forbid it.
+        final String cmdName;
+        final String cmd;
+        if (commandLine == null) {
+            cmdName = ASADMIN_OSGI_SHELL;
+            cmd = ASADMIN_OSGI_SHELL;
+        } else if (commandLine instanceof String) {
+            cmdName = (String) commandLine;
+            cmd = cmdName;
+        } else if (commandLine instanceof List) {
+            @SuppressWarnings("unchecked")
+            final List<String> list = (List<String>) commandLine;
+            cmdName = list.isEmpty() ? "" : list.get(0);
+            cmd = list.isEmpty() ? "" : list.stream().collect(Collectors.joining(" "));
+        } else if (commandLine instanceof String[]) {
+            final String[] list = (String[]) commandLine;
+            cmdName = list.length == 0 ? "" : list[0];
+            cmd = list.length == 0 ? "" : Arrays.stream(list).collect(Collectors.joining(" "));
+        } else {
+            report.setMessage("Unable to deal with argument list of type " + commandLine.getClass().getName());
+            report.setActionExitCode(ActionReport.ExitCode.FAILURE);
+            return;
+        }
+        try {
+            final SessionOperation sessionOperation = SessionOperation.parse(sessionOp);
+            final OsgiShellService service = detectService(ctx, sessionOperation, sessionId, report);
+            if (service == null) {
+                report.setMessage("No Shell Service available");
+                report.setActionExitCode(ActionReport.ExitCode.FAILURE);
+                return;
+            }
+            service.exec(cmdName, cmd);
+        } catch (final Exception ex) {
+            LOG.log(Level.SEVERE, ex.getMessage());
+            report.setMessage(ex.getMessage());
+            report.setActionExitCode(ActionReport.ExitCode.FAILURE);
+        }
+    }
 }
diff --git a/nucleus/osgi-platforms/osgi-cli-remote/src/main/java/org/glassfish/osgi/cli/remote/impl/FelixOsgiShellService.java b/nucleus/osgi-platforms/osgi-cli-remote/src/main/java/org/glassfish/osgi/cli/remote/impl/FelixOsgiShellService.java
new file mode 100644
index 0000000..a2fba91
--- /dev/null
+++ b/nucleus/osgi-platforms/osgi-cli-remote/src/main/java/org/glassfish/osgi/cli/remote/impl/FelixOsgiShellService.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2022 Eclipse Foundation and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.osgi.cli.remote.impl;
+
+import org.apache.felix.shell.ShellService;
+import org.glassfish.api.ActionReport;
+
+/**
+ * Service using {@link ShellService} and NOT supporting sessions.
+ *
+ * @author David Matejcek
+ */
+class FelixOsgiShellService extends OsgiShellService {
+
+    private final ShellService service;
+
+    FelixOsgiShellService(Object service, ActionReport report) {
+        super(report);
+        this.service = (ShellService) service;
+    }
+
+
+    @Override
+    protected void execCommand(String cmdName, String cmd) throws Exception {
+        if (ASADMIN_OSGI_SHELL.equals(cmdName)) {
+            stdout.println("felix");
+        } else {
+            service.executeCommand(cmd, stdout, stderr);
+        }
+    }
+}
diff --git a/nucleus/osgi-platforms/osgi-cli-remote/src/main/java/org/glassfish/osgi/cli/remote/impl/GogoOsgiShellService.java b/nucleus/osgi-platforms/osgi-cli-remote/src/main/java/org/glassfish/osgi/cli/remote/impl/GogoOsgiShellService.java
new file mode 100644
index 0000000..8183d41
--- /dev/null
+++ b/nucleus/osgi-platforms/osgi-cli-remote/src/main/java/org/glassfish/osgi/cli/remote/impl/GogoOsgiShellService.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2022 Eclipse Foundation and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.osgi.cli.remote.impl;
+
+import java.io.InputStream;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.apache.felix.service.command.CommandProcessor;
+import org.apache.felix.service.command.CommandSession;
+import org.glassfish.api.ActionReport;
+import org.glassfish.common.util.io.EmptyInputStream;
+
+import static org.glassfish.osgi.cli.remote.impl.SessionOperation.EXECUTE;
+import static org.glassfish.osgi.cli.remote.impl.SessionOperation.LIST;
+import static org.glassfish.osgi.cli.remote.impl.SessionOperation.NEW;
+import static org.glassfish.osgi.cli.remote.impl.SessionOperation.STOP;
+
+/**
+ * Service using {@link CommandProcessor} and supporting sessions.
+ *
+ * @author David Matejcek
+ */
+class GogoOsgiShellService extends OsgiShellService {
+    private static final Logger LOG = Logger.getLogger(GogoOsgiShellService.class.getName());
+
+    private static final Map<String, RemoteCommandSession> SESSIONS = new ConcurrentHashMap<>();
+    private final CommandProcessor processor;
+    private final SessionOperation sessionOperation;
+    private final String sessionId;
+
+    GogoOsgiShellService(Object service, SessionOperation sessionOperation, String sessionId, ActionReport report) {
+        super(report);
+        this.processor = (CommandProcessor) service;
+        this.sessionOperation = sessionOperation;
+        this.sessionId = sessionId;
+    }
+
+    @Override
+    protected void execCommand(String cmdName, String cmd) throws Exception {
+        final InputStream in = new EmptyInputStream();
+        if (sessionOperation == null) {
+            if (ASADMIN_OSGI_SHELL.equals(cmdName)) {
+                stdout.println("gogo");
+            } else {
+                try (CommandSession session = processor.createSession(in, stdout, stderr)) {
+                    final Object result = session.execute(cmd);
+                    if (result instanceof String) {
+                        stdout.println(result.toString());
+                    }
+                }
+            }
+        } else if (sessionOperation == NEW) {
+            final CommandSession session = processor.createSession(in, stdout, stderr);
+            final RemoteCommandSession remote = new RemoteCommandSession(session);
+            LOG.log(Level.FINE, "Remote session established: {0}", remote.getId());
+            SESSIONS.put(remote.getId(), remote);
+            stdout.println(remote.getId());
+        } else if (sessionOperation == LIST) {
+            for (final String id : SESSIONS.keySet()) {
+                stdout.println(id);
+            }
+        } else if (sessionOperation == EXECUTE) {
+            final RemoteCommandSession remote = SESSIONS.get(sessionId);
+            final CommandSession session = remote.attach(in, stdout, stderr);
+            final Object result = session.execute(cmd);
+            if (result instanceof String) {
+                stdout.println(result.toString());
+            }
+            remote.detach();
+        } else if (sessionOperation == STOP) {
+            final RemoteCommandSession remote = SESSIONS.remove(sessionId);
+            if (remote != null) {
+                final CommandSession session = remote.attach(in, stdout, stderr);
+                session.close();
+            }
+            LOG.log(Level.FINE, "Remote session closed: {0}", sessionId);
+        }
+    }
+}
diff --git a/nucleus/osgi-platforms/osgi-cli-remote/src/main/java/org/glassfish/osgi/cli/remote/impl/OsgiShellService.java b/nucleus/osgi-platforms/osgi-cli-remote/src/main/java/org/glassfish/osgi/cli/remote/impl/OsgiShellService.java
new file mode 100644
index 0000000..76238f1
--- /dev/null
+++ b/nucleus/osgi-platforms/osgi-cli-remote/src/main/java/org/glassfish/osgi/cli/remote/impl/OsgiShellService.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2022 Eclipse Foundation and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.osgi.cli.remote.impl;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.glassfish.api.ActionReport;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * Common parent of OSGi Shell service wrappers.
+ *
+ * @author David Matejcek
+ */
+public abstract class OsgiShellService {
+    /** Used by LocalOSGiShellCommand */
+    public static final String ASADMIN_OSGI_SHELL = "asadmin-osgi-shell";
+    private static final Logger LOG = Logger.getLogger(OsgiShellService.class.getName());
+
+    private final ActionReport report;
+    private final ByteArrayOutputStream stdoutBytes;
+    private final ByteArrayOutputStream stderrBytes;
+    /** Can be used to send response to user */
+    protected final PrintStream stdout;
+    /** Error output for user */
+    protected final PrintStream stderr;
+
+
+    /**
+     * Initializes streams collecting output,
+     *
+     * @param report
+     */
+    protected OsgiShellService(final ActionReport report) {
+        this.report = report;
+        this.stdoutBytes = new ByteArrayOutputStream(1024);
+        this.stdout = new PrintStream(stdoutBytes);
+        this.stderrBytes = new ByteArrayOutputStream(1024);
+        this.stderr = new PrintStream(stderrBytes);
+    }
+
+
+    /**
+     * Executes the command.
+     *
+     * @param cmdName - command name. Can be {@value #ASADMIN_OSGI_SHELL} or the first command of cmd.
+     * @param cmd - command and arguments.
+     * @return updated {@link ActionReport} given in constructor
+     * @throws Exception
+     */
+    public ActionReport exec(final String cmdName, final String cmd) throws Exception {
+        LOG.log(Level.FINE, "exec: {0}", cmd);
+        execCommand(cmdName, cmd);
+        stdout.flush();
+        stderr.flush();
+        return generateReport();
+    }
+
+
+    /**
+     * Calls the real implementation of the OSGI shell.
+     *
+     * @param cmdName - command name. Can be {@value #ASADMIN_OSGI_SHELL} or the first command of cmd.
+     * @param cmd - command and arguments.
+     * @throws Exception
+     */
+    protected abstract void execCommand(String cmdName, String cmd) throws Exception;
+
+
+    private ActionReport generateReport() {
+        final String output = stdoutBytes.toString(UTF_8);
+        report.setMessage(output);
+
+        final String errors = stderrBytes.toString(UTF_8);
+        if (errors.isEmpty()) {
+            report.setActionExitCode(ActionReport.ExitCode.SUCCESS);
+        } else {
+            report.setMessage(errors);
+            report.setActionExitCode(ActionReport.ExitCode.FAILURE);
+        }
+        return report;
+    }
+}
diff --git a/nucleus/osgi-platforms/osgi-cli-remote/src/main/java/org/glassfish/osgi/cli/remote/impl/OsgiShellServiceProvider.java b/nucleus/osgi-platforms/osgi-cli-remote/src/main/java/org/glassfish/osgi/cli/remote/impl/OsgiShellServiceProvider.java
new file mode 100644
index 0000000..65e3db0
--- /dev/null
+++ b/nucleus/osgi-platforms/osgi-cli-remote/src/main/java/org/glassfish/osgi/cli/remote/impl/OsgiShellServiceProvider.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2022 Eclipse Foundation and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.osgi.cli.remote.impl;
+
+import org.glassfish.api.ActionReport;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+
+/**
+ * Detects OSGI shell impl available on the classpath and provides one of them.
+ *
+ * @author David Matejcek
+ */
+public class OsgiShellServiceProvider {
+
+
+    /**
+     * Effectively masks classes as used dependencies are optional and might not be on the classpath
+     *
+     * @param ctx - used to detect and get the service impl
+     * @param sessionId - used to attach to an existing session with this id.
+     * @param sessionOperation - used to start/attach/stop
+     * @param report
+     * @return {@link OsgiShellService} or null if there is no shell impl available.
+     */
+    public static OsgiShellService detectService(final BundleContext ctx, final SessionOperation sessionOperation,
+        final String sessionId, final ActionReport report) {
+        // warning: don't replace strings by classes, classes might not be on the classpath.
+        final ServiceReference<?> processor = ctx
+            .getServiceReference("org.apache.felix.service.command.CommandProcessor");
+        if (processor != null) {
+            return new GogoOsgiShellService(ctx.getService(processor), sessionOperation, sessionId, report);
+        }
+        final ServiceReference<?> shell = ctx.getServiceReference("org.apache.felix.shell.ShellService");
+        if (shell != null) {
+            return new FelixOsgiShellService(ctx.getService(shell), report);
+        }
+        return null;
+    }
+}
diff --git a/nucleus/osgi-platforms/osgi-cli-remote/src/main/java/org/glassfish/osgi/cli/remote/RemoteCommandSession.java b/nucleus/osgi-platforms/osgi-cli-remote/src/main/java/org/glassfish/osgi/cli/remote/impl/RemoteCommandSession.java
similarity index 80%
rename from nucleus/osgi-platforms/osgi-cli-remote/src/main/java/org/glassfish/osgi/cli/remote/RemoteCommandSession.java
rename to nucleus/osgi-platforms/osgi-cli-remote/src/main/java/org/glassfish/osgi/cli/remote/impl/RemoteCommandSession.java
index a08a4ff..1d685ae 100644
--- a/nucleus/osgi-platforms/osgi-cli-remote/src/main/java/org/glassfish/osgi/cli/remote/RemoteCommandSession.java
+++ b/nucleus/osgi-platforms/osgi-cli-remote/src/main/java/org/glassfish/osgi/cli/remote/impl/RemoteCommandSession.java
@@ -1,4 +1,5 @@
 /*
+ * Copyright (c) 2022 Contributors to the Eclipse Foundation
  * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
@@ -14,7 +15,7 @@
  * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
  */
 
-package org.glassfish.osgi.cli.remote;
+package org.glassfish.osgi.cli.remote.impl;
 
 import java.io.InputStream;
 import java.io.PrintStream;
@@ -26,6 +27,7 @@
 import java.security.AccessController;
 import java.security.PrivilegedAction;
 import java.util.UUID;
+
 import org.apache.felix.service.command.CommandSession;
 
 /**
@@ -39,49 +41,47 @@
  *
  * @author ancoron
  */
-public class RemoteCommandSession {
+class RemoteCommandSession {
 
     private final CommandSession delegate;
     private final String id;
 
-    public RemoteCommandSession(CommandSession delegate)
-    {
+    public RemoteCommandSession(final CommandSession delegate) {
         this.delegate = delegate;
         this.id = UUID.randomUUID().toString();
     }
 
+
     /**
-     * Get the identifier for this session, which is a UUID of type 4.
-     *
-     * @return
+     * @return the identifier for this session, which is a UUID of type 4.
      */
     public String getId() {
         return id;
     }
 
+
     /**
-     * Attached the specified streams to the delegate of this instance and
+     * Attaches the specified streams to the delegate of this instance and
      * returns the modified delegate.
      *
      * @param in The "stdin" stream for the session
      * @param out The "stdout" stream for the session
      * @param err The "stderr" stream for the session
-     *
      * @return The modified {@link CommandSession} delegate
-     *
      * @see #detach()
      */
-    public CommandSession attach(InputStream in, PrintStream out, PrintStream err) {
+    public CommandSession attach(final InputStream in, final PrintStream out, final PrintStream err) {
         set(this.delegate, "in", in);
         set(this.delegate, "out", out);
         set(this.delegate, "err", err);
-        ReadableByteChannel inCh = Channels.newChannel(in);
-        WritableByteChannel outCh = Channels.newChannel(out);
-        WritableByteChannel errCh = out == err ? outCh : Channels.newChannel(err);
+        final ReadableByteChannel inCh = Channels.newChannel(in);
+        final WritableByteChannel outCh = Channels.newChannel(out);
+        final WritableByteChannel errCh = out == err ? outCh : Channels.newChannel(err);
         set(this.delegate, "channels", new Channel[] {inCh, outCh, errCh});
         return this.delegate;
     }
 
+
     /**
      * Detaches all previously attached streams and hence, ensures that there
      * are no stale references left.
@@ -94,11 +94,14 @@
         set(this.delegate, "err", null);
     }
 
+
     private void set(final Object obj, final String field, final Object value) {
         try {
             final Field f = obj.getClass().getDeclaredField(field);
-            final boolean accessible = f.isAccessible();
-            if(!accessible) {
+            final boolean accessible = f.canAccess(obj);
+            if (accessible) {
+                f.set(obj, value);
+            } else {
                 AccessController.doPrivileged(new PrivilegedAction<Void>() {
 
                     @Override
@@ -106,7 +109,7 @@
                         f.setAccessible(true);
                         try {
                             f.set(obj, value);
-                        } catch(Exception x) {
+                        } catch (final Exception x) {
                             throw new RuntimeException(x);
                         }
 
@@ -115,10 +118,8 @@
                         return null;
                     }
                 });
-            } else {
-                f.set(obj, value);
             }
-        } catch(Exception x) {
+        } catch (final Exception x) {
             throw new RuntimeException(x);
         }
     }
diff --git a/nucleus/osgi-platforms/osgi-cli-remote/src/main/java/org/glassfish/osgi/cli/remote/impl/SessionOperation.java b/nucleus/osgi-platforms/osgi-cli-remote/src/main/java/org/glassfish/osgi/cli/remote/impl/SessionOperation.java
new file mode 100644
index 0000000..b07868e
--- /dev/null
+++ b/nucleus/osgi-platforms/osgi-cli-remote/src/main/java/org/glassfish/osgi/cli/remote/impl/SessionOperation.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2022 Eclipse Foundation and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.osgi.cli.remote.impl;
+
+
+/**
+ * Commands for session manipulation.
+ *
+ * @author David Matejcek
+ */
+public enum SessionOperation {
+
+    /** Create session */
+    NEW,
+    /** List sessions */
+    LIST,
+    /** Execute command in session */
+    EXECUTE,
+    /** Stop session */
+    STOP;
+
+    /**
+     * @param operation see {@link SessionOperation} values. Case insensitive.
+     * @return {@link SessionOperation} or null if operation was null.
+     * @throws IllegalArgumentException if the operation is an unknown string.
+     *
+     */
+    public static SessionOperation parse(String operation) throws IllegalArgumentException {
+        if (operation == null) {
+            return null;
+        }
+        for (SessionOperation value : values()) {
+            if (value.name().equalsIgnoreCase(operation)) {
+                return value;
+            }
+        }
+        throw new IllegalArgumentException("Unsupported session operation: " + operation);
+    }
+
+}