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

janhoy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/main by this push:
     new 9becf6c6a15 SOLR-18515 | PackageTool Migration to PicoCLI (#5047)
9becf6c6a15 is described below

commit 9becf6c6a154b73c3445b41393517df1208a811c
Author: Jalaz Kumar <[email protected]>
AuthorDate: Fri Oct 9 00:59:36 2026 +0530

    SOLR-18515 | PackageTool Migration to PicoCLI (#5047)
    
    Co-authored-by: Jan Høydahl <[email protected]>
---
 .../SOLR-17697-picocli-experimental-cli.yml        |   4 +
 .../org/apache/solr/cli/ConnectionOptions.java     |  81 ++++++
 .../java/org/apache/solr/cli/PackageAddKey.java    |  56 ++++
 .../java/org/apache/solr/cli/PackageAddRepo.java   |  62 +++++
 .../java/org/apache/solr/cli/PackageDeploy.java    | 100 ++++++++
 .../java/org/apache/solr/cli/PackageInstall.java   |  60 +++++
 .../org/apache/solr/cli/PackageListAvailable.java  |  47 ++++
 .../org/apache/solr/cli/PackageListDeployed.java   |  77 ++++++
 .../org/apache/solr/cli/PackageListInstalled.java  |  47 ++++
 .../org/apache/solr/cli/PackageSubCommand.java     |  64 +++++
 .../src/java/org/apache/solr/cli/PackageTool.java  | 285 +++++++++++++++------
 .../java/org/apache/solr/cli/PackageUndeploy.java  |  74 ++++++
 .../java/org/apache/solr/cli/PackageUninstall.java |  55 ++++
 .../core/src/java/org/apache/solr/cli/SolrCLI.java |   3 +-
 .../src/java/org/apache/solr/pkg/PackageAPI.java   |   6 +-
 .../org/apache/solr/pkg/SolrPackageLoader.java     |   4 +-
 .../org/apache/solr/cli/ConnectionOptionsTest.java |  46 ++++
 .../apache/solr/cli/PackageToolPicocliTest.java    |  32 +++
 .../test/org/apache/solr/cli/PackageToolTest.java  |  85 +++---
 .../modules/deployment-guide/deployment-nav.adoc   |  10 +
 .../modules/deployment-guide/pages/cli/index.adoc  |  10 +
 .../pages/cli/solr-package-add-key.adoc            |  96 +++++++
 .../pages/cli/solr-package-add-repo.adoc           | 100 ++++++++
 .../pages/cli/solr-package-deploy.adoc             | 116 +++++++++
 .../pages/cli/solr-package-install.adoc            | 100 ++++++++
 .../pages/cli/solr-package-list-available.adoc     |  88 +++++++
 .../pages/cli/solr-package-list-deployed.adoc      | 104 ++++++++
 .../pages/cli/solr-package-list-installed.adoc     |  88 +++++++
 .../pages/cli/solr-package-undeploy.adoc           | 103 ++++++++
 .../pages/cli/solr-package-uninstall.adoc          |  96 +++++++
 .../deployment-guide/pages/cli/solr-package.adoc   | 132 ++++++++++
 31 files changed, 2096 insertions(+), 135 deletions(-)

diff --git a/changelog/unreleased/SOLR-17697-picocli-experimental-cli.yml 
b/changelog/unreleased/SOLR-17697-picocli-experimental-cli.yml
index 0603e293c11..2c102eb62ac 100644
--- a/changelog/unreleased/SOLR-17697-picocli-experimental-cli.yml
+++ b/changelog/unreleased/SOLR-17697-picocli-experimental-cli.yml
@@ -8,6 +8,8 @@ authors:
   - name: Jan Høydahl
     url: https://home.apache.org/phonebook.html?uid=janhoy
   - name: Serhiy Bzhezytskyy
+  - name: Jalaz Kumar
+    nick: jaykay12
 links:
   - name: SOLR-17697
     url: https://issues.apache.org/jira/browse/SOLR-17697
@@ -31,3 +33,5 @@ links:
     url: https://issues.apache.org/jira/browse/SOLR-18518
   - name: SOLR-18517
     url: https://issues.apache.org/jira/browse/SOLR-18517
+  - name: SOLR-18515
+    url: https://issues.apache.org/jira/browse/SOLR-18515
diff --git a/solr/core/src/java/org/apache/solr/cli/ConnectionOptions.java 
b/solr/core/src/java/org/apache/solr/cli/ConnectionOptions.java
index 0a782b5a60f..57ee0532faa 100644
--- a/solr/core/src/java/org/apache/solr/cli/ConnectionOptions.java
+++ b/solr/core/src/java/org/apache/solr/cli/ConnectionOptions.java
@@ -17,7 +17,10 @@
 package org.apache.solr.cli;
 
 import java.io.IOException;
+import java.util.Map;
+import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.impl.CloudSolrClient;
+import org.apache.solr.common.util.EnvUtils;
 import picocli.CommandLine;
 
 /**
@@ -73,4 +76,82 @@ class ConnectionOptions {
     }
     return solrUrl;
   }
+
+  static String resolveSolrUrl(ConnectionOptions connectionOptions, String 
credentials)
+      throws Exception {
+    if (connectionOptions != null) {
+      String solrUrl = connectionOptions.effectiveSolrUrl();
+      if (solrUrl != null) {
+        return CLIUtils.normalizeSolrUrl(solrUrl);
+      }
+      String zkHost = connectionOptions.effectiveZkHost();
+      if (zkHost != null) {
+        return CLIUtils.solrUrlFromConnection(
+            CloudSolrClient.CloudSolrClientConnection.parse(zkHost), 
credentials);
+      }
+    }
+
+    String solrConnectionProp = EnvUtils.getProperty("solr.connection");
+    if (solrConnectionProp != null && !solrConnectionProp.isBlank()) {
+      var connection = 
CloudSolrClient.CloudSolrClientConnection.parse(solrConnectionProp);
+      if (connection.isZookeeper()) {
+        return CLIUtils.solrUrlFromConnection(connection, credentials);
+      }
+      return CLIUtils.normalizeSolrUrl(connection.quorumItems().get(0));
+    }
+
+    String zkHostProp = EnvUtils.getProperty("zkHost");
+    if (zkHostProp != null && !zkHostProp.isBlank()) {
+      return CLIUtils.solrUrlFromConnection(
+          CloudSolrClient.CloudSolrClientConnection.parse(zkHostProp), 
credentials);
+    }
+
+    String defaultUrl = CLIUtils.getDefaultSolrUrl();
+    CLIO.err(
+        "Neither --solr-connection, --zk-host or --solr-url parameters, nor 
SOLR_CONNECTION, ZK_HOST env var provided, so assuming solr url is "
+            + defaultUrl
+            + ".");
+    return defaultUrl;
+  }
+
+  static String resolveZkHost(
+      ConnectionOptions connectionOptions, String solrUrl, String credentials) 
throws Exception {
+    boolean resolveFromSolrUrl = false;
+    if (connectionOptions != null) {
+      String zkHost = connectionOptions.effectiveZkHost();
+      if (zkHost != null) {
+        return zkHost;
+      }
+      resolveFromSolrUrl = connectionOptions.effectiveSolrUrl() != null;
+    }
+
+    if (!resolveFromSolrUrl) {
+      String solrConnectionProp = EnvUtils.getProperty("solr.connection");
+      if (solrConnectionProp != null && !solrConnectionProp.isBlank()) {
+        var connection = 
CloudSolrClient.CloudSolrClientConnection.parse(solrConnectionProp);
+        if (connection.isZookeeper()) {
+          return solrConnectionProp;
+        }
+      }
+
+      String zkHostProp = EnvUtils.getProperty("zkHost");
+      if (zkHostProp != null && !zkHostProp.isBlank()) {
+        return zkHostProp;
+      }
+    }
+
+    try (SolrClient solrClient = CLIUtils.getSolrClient(solrUrl, credentials)) 
{
+      Map<String, Object> status = StatusTool.reportStatus(solrClient);
+      @SuppressWarnings("unchecked")
+      Map<String, Object> cloud = (Map<String, Object>) status.get("cloud");
+      if (cloud != null) {
+        String zookeeper = cloud.get("ZooKeeper").toString();
+        if (zookeeper != null && zookeeper.endsWith("(embedded)")) {
+          zookeeper = zookeeper.substring(0, zookeeper.length() - 
"(embedded)".length());
+        }
+        return zookeeper;
+      }
+    }
+    return null;
+  }
 }
diff --git a/solr/core/src/java/org/apache/solr/cli/PackageAddKey.java 
b/solr/core/src/java/org/apache/solr/cli/PackageAddKey.java
new file mode 100644
index 00000000000..97f6bc286be
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/cli/PackageAddKey.java
@@ -0,0 +1,56 @@
+/*
+ * 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.solr.cli;
+
+import java.nio.file.Path;
+
+/** Supports package add-key command in the bin/solr script. */
+@SuppressWarnings("UnnecessarilyFullyQualified")
[email protected](
+    name = "add-key",
+    description = "Add a trusted key to Solr.",
+    exitCodeListHeading = "%nExit Codes:%n",
+    exitCodeList = {
+      "0: Operation completed successfully.",
+      "1: Operation failed; check output for details."
+    },
+    footerHeading = "%nExamples:%n",
+    footer = {
+      "  # Add a trusted public key",
+      "  bin/solr package add-key /path/to/public.key.der",
+    })
+public class PackageAddKey extends PackageSubCommand {
+
+  @picocli.CommandLine.Parameters(
+      index = "0",
+      arity = "1",
+      paramLabel = "KEY-FILE",
+      description = "Path to a file containing the trusted key.")
+  private Path keyFile;
+
+  @Override
+  public int callTool() throws Exception {
+    return runWithManagers(
+        ((packageManager, repositoryManager) -> 
packageTool.addKey(repositoryManager, keyFile)));
+  }
+
+  @Override
+  public String getName() {
+    return "add-key";
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/cli/PackageAddRepo.java 
b/solr/core/src/java/org/apache/solr/cli/PackageAddRepo.java
new file mode 100644
index 00000000000..cb3bc7067a3
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/cli/PackageAddRepo.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.solr.cli;
+
+/** Supports package add-repo command in the bin/solr script. */
+@SuppressWarnings("UnnecessarilyFullyQualified")
[email protected](
+    name = "add-repo",
+    description = "Add a package repository to Solr.",
+    exitCodeListHeading = "%nExit Codes:%n",
+    exitCodeList = {
+      "0: Operation completed successfully.",
+      "1: Operation failed; check output for details."
+    },
+    footerHeading = "%nExamples:%n",
+    footer = {
+      "  # Add a package repository",
+      "  bin/solr package add-repo myrepo https://my.repo.example/repo";,
+    })
+public class PackageAddRepo extends PackageSubCommand {
+
+  @picocli.CommandLine.Parameters(
+      index = "0",
+      arity = "1",
+      paramLabel = "REPOSITORY-NAME",
+      description = "Name of the package repository.")
+  private String repoName;
+
+  @picocli.CommandLine.Parameters(
+      index = "1",
+      arity = "1",
+      paramLabel = "REPOSITORY-URL",
+      description = "URL of the package repository.")
+  private String repoUrl;
+
+  @Override
+  public int callTool() throws Exception {
+    return runWithManagers(
+        ((packageManager, repositoryManager) ->
+            packageTool.addRepo(repositoryManager, repoName, repoUrl)));
+  }
+
+  @Override
+  public String getName() {
+    return "add-repo";
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/cli/PackageDeploy.java 
b/solr/core/src/java/org/apache/solr/cli/PackageDeploy.java
new file mode 100644
index 00000000000..ff24a7a1692
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/cli/PackageDeploy.java
@@ -0,0 +1,100 @@
+/*
+ * 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.solr.cli;
+
+import static org.apache.solr.cli.SolrCLI.printRed;
+
+/** Supports package deploy command in the bin/solr script. */
+@SuppressWarnings("UnnecessarilyFullyQualified")
[email protected](
+    name = "deploy",
+    description = "Deploy an installed package to collections or at cluster 
level.",
+    exitCodeListHeading = "%nExit Codes:%n",
+    exitCodeList = {
+      "0: Operation completed successfully.",
+      "1: Operation failed; check output for details."
+    },
+    footerHeading = "%nExamples:%n",
+    footer = {
+      "  # Deploy a package to a collection",
+      "  bin/solr package deploy mypkg:1.0.0 --collections myCollection -y",
+      "",
+      "  # Update an existing deployment",
+      "  bin/solr package deploy mypkg --update --collections myCollection -y"
+    })
+public class PackageDeploy extends PackageSubCommand {
+
+  @picocli.CommandLine.Parameters(
+      index = "0",
+      arity = "1",
+      paramLabel = "PACKAGE[:VERSION]",
+      description = "Package name, optionally with :version.")
+  private String packageNameAndVersion;
+
+  @picocli.CommandLine.Option(
+      names = {"--cluster"},
+      description = "Specifies that this action should affect cluster-level 
plugins only.")
+  private boolean cluster;
+
+  @picocli.CommandLine.Option(
+      names = {"--collections"},
+      paramLabel = "COLLECTIONS",
+      description =
+          "Specifies that this action should affect plugins for the given 
collections only, excluding cluster level plugins.")
+  private String collections;
+
+  @picocli.CommandLine.Option(
+      names = {"-p", "--param"},
+      paramLabel = "PARAMS",
+      description = "List of parameters to be used with deploy command.")
+  private String[] params;
+
+  @picocli.CommandLine.Option(
+      names = {"--update"},
+      description = "If a deployment is an update over a previous deployment.")
+  private boolean update;
+
+  @picocli.CommandLine.Option(
+      names = {"-y", "--no-prompt"},
+      description = "Don't prompt for input; accept all default choices, 
defaults to false.")
+  private boolean noPrompt;
+
+  @Override
+  public int callTool() throws Exception {
+    if (!cluster && collections == null) {
+      printRed(
+          "Either specify --cluster to deploy cluster level plugins or 
--collections <list-of-collections> to deploy collection level plugins");
+      return 1;
+    }
+    return runWithManagers(
+        (packageManager, repositoryManager) ->
+            packageTool.deploy(
+                packageManager,
+                packageNameAndVersion,
+                cluster,
+                collections,
+                params,
+                update,
+                noPrompt));
+  }
+
+  @Override
+  public String getName() {
+    return "deploy";
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/cli/PackageInstall.java 
b/solr/core/src/java/org/apache/solr/cli/PackageInstall.java
new file mode 100644
index 00000000000..b48c65f4a12
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/cli/PackageInstall.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.cli;
+
+/** Supports package install command in the bin/solr script. */
+@SuppressWarnings("UnnecessarilyFullyQualified")
[email protected](
+    name = "install",
+    description = "Install a package into Solr.",
+    exitCodeListHeading = "%nExit Codes:%n",
+    exitCodeList = {
+      "0: Operation completed successfully.",
+      "1: Operation failed; check output for details."
+    },
+    footerHeading = "%nExamples:%n",
+    footer = {
+      "  # Install a specific package version",
+      "  bin/solr package install mypkg:1.0.0",
+      "",
+      "  # Install the latest available version",
+      "  bin/solr package install mypkg"
+    })
+public class PackageInstall extends PackageSubCommand {
+
+  @picocli.CommandLine.Parameters(
+      index = "0",
+      arity = "1",
+      paramLabel = "PACKAGE[:VERSION]",
+      description = "Package name, optionally with :version.")
+  private String packageNameAndVersion;
+
+  @Override
+  public int callTool() throws Exception {
+    final boolean[] status = {true};
+    runWithManagers(
+        (packageManager, repositoryManager) ->
+            status[0] = packageTool.install(repositoryManager, 
packageNameAndVersion));
+    return status[0] ? 0 : 1;
+  }
+
+  @Override
+  public String getName() {
+    return "install";
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/cli/PackageListAvailable.java 
b/solr/core/src/java/org/apache/solr/cli/PackageListAvailable.java
new file mode 100644
index 00000000000..e0420ecb7d4
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/cli/PackageListAvailable.java
@@ -0,0 +1,47 @@
+/*
+ * 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.solr.cli;
+
+/** Supports package list-available command in the bin/solr script. */
+@SuppressWarnings("UnnecessarilyFullyQualified")
[email protected](
+    name = "list-available",
+    description = "Print a list of packages available in the repositories.",
+    exitCodeListHeading = "%nExit Codes:%n",
+    exitCodeList = {
+      "0: Operation completed successfully.",
+      "1: Operation failed; check output for details."
+    },
+    footerHeading = "%nExamples:%n",
+    footer = {
+      "  # List packages available from configured repositories",
+      "  bin/solr package list-available",
+    })
+public class PackageListAvailable extends PackageSubCommand {
+
+  @Override
+  public int callTool() throws Exception {
+    return runWithManagers(
+        (packageManager, repositoryManager) -> 
packageTool.listAvailable(repositoryManager));
+  }
+
+  @Override
+  public String getName() {
+    return "list-available";
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/cli/PackageListDeployed.java 
b/solr/core/src/java/org/apache/solr/cli/PackageListDeployed.java
new file mode 100644
index 00000000000..944fce6591b
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/cli/PackageListDeployed.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.cli;
+
+import static org.apache.solr.cli.SolrCLI.printRed;
+
+/** Supports package list-deployed command in the bin/solr script. */
+@SuppressWarnings("UnnecessarilyFullyQualified")
[email protected](
+    name = "list-deployed",
+    description =
+        "Print packages deployed on a collection, or collections where a 
package is deployed.",
+    exitCodeListHeading = "%nExit Codes:%n",
+    exitCodeList = {
+      "0: Operation completed successfully.",
+      "1: Operation failed; check output for details."
+    },
+    footerHeading = "%nExamples:%n",
+    footer = {
+      "  # List packages deployed on a collection",
+      "  bin/solr package list-deployed -c myCollection",
+      "",
+      "  # List collections where a package is deployed",
+      "  bin/solr package list-deployed mypkg"
+    })
+public class PackageListDeployed extends PackageSubCommand {
+
+  @picocli.CommandLine.Option(
+      names = {"-c", "--collection"},
+      paramLabel = "COLLECTION",
+      description = "The collection to apply the package to, not required.")
+  private String collection;
+
+  @picocli.CommandLine.Parameters(
+      index = "0",
+      arity = "0..1",
+      paramLabel = "PACKAGE",
+      description = "Package name; lists collections where this package is 
deployed.")
+  private String packageName;
+
+  @Override
+  public int callTool() throws Exception {
+    if (collection == null && packageName == null) {
+      printRed("Either -c/--collection <collection> or a package name is 
required.");
+      return 1;
+    }
+
+    return runWithManagers(
+        (packageManager, repositoryManager) -> {
+          if (collection != null) {
+            packageTool.listPackagesDeployedOnCollection(packageManager, 
collection);
+          } else {
+            packageTool.listCollectionsWithPackageDeployed(packageManager, 
packageName);
+          }
+        });
+  }
+
+  @Override
+  public String getName() {
+    return "list-deployed";
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/cli/PackageListInstalled.java 
b/solr/core/src/java/org/apache/solr/cli/PackageListInstalled.java
new file mode 100644
index 00000000000..e1d62b26d95
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/cli/PackageListInstalled.java
@@ -0,0 +1,47 @@
+/*
+ * 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.solr.cli;
+
+/** Supports package list-installed command in the bin/solr script. */
+@SuppressWarnings("UnnecessarilyFullyQualified")
[email protected](
+    name = "list-installed",
+    description = "Print a list of packages installed in Solr.",
+    exitCodeListHeading = "%nExit Codes:%n",
+    exitCodeList = {
+      "0: Operation completed successfully.",
+      "1: Operation failed; check output for details."
+    },
+    footerHeading = "%nExamples:%n",
+    footer = {
+      "  # List all installed packages",
+      "  bin/solr package list-installed",
+    })
+public class PackageListInstalled extends PackageSubCommand {
+
+  @Override
+  public int callTool() throws Exception {
+    return runWithManagers(
+        (packageManager, repositoryManager) -> 
packageTool.listInstalled(packageManager));
+  }
+
+  @Override
+  public String getName() {
+    return "list-installed";
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/cli/PackageSubCommand.java 
b/solr/core/src/java/org/apache/solr/cli/PackageSubCommand.java
new file mode 100644
index 00000000000..9b8130227af
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/cli/PackageSubCommand.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.cli;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.solr.packagemanager.PackageManager;
+import org.apache.solr.packagemanager.RepositoryManager;
+
+/** Shared picocli wiring for {@code bin/solr package <subcommand> leaves} */
+@SuppressWarnings("UnnecessarilyFullyQualified")
+abstract class PackageSubCommand extends ToolBase {
+
+  @FunctionalInterface
+  interface PackageAction {
+    void run(PackageManager packageManager, RepositoryManager 
repositoryManager) throws Exception;
+  }
+
+  @picocli.CommandLine.ParentCommand PackageTool packageTool;
+
+  @picocli.CommandLine.Mixin CredentialsOptions credentialsOptions;
+
+  @picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1")
+  ConnectionOptions connectionOptions;
+
+  PackageSubCommand() {
+    super(new DefaultToolRuntime());
+  }
+
+  @Override
+  public void runImpl(CommandLine cli) throws Exception {
+    throw new UnsupportedOperationException(
+        getName() + " is implemented via the commons-cli path under 
PackageTool");
+  }
+
+  final int runWithManagers(PackageAction action) throws Exception {
+    String credentials =
+        credentialsOptions.credentials != null
+            ? credentialsOptions.credentials
+            : packageTool.credentialsOptions != null
+                ? packageTool.credentialsOptions.credentials
+                : null;
+
+    packageTool.runWithManagers(
+        connectionOptions != null ? connectionOptions : 
packageTool.connectionOptions,
+        credentials,
+        action);
+    return 0;
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/cli/PackageTool.java 
b/solr/core/src/java/org/apache/solr/cli/PackageTool.java
index cba26c58a3a..5e7ebe39a9d 100644
--- a/solr/core/src/java/org/apache/solr/cli/PackageTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/PackageTool.java
@@ -24,6 +24,7 @@ import static 
org.apache.solr.packagemanager.PackageUtils.formatGreen;
 import java.lang.invoke.MethodHandles;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.Arrays;
 import java.util.Map;
 import org.apache.commons.cli.CommandLine;
 import org.apache.commons.cli.Option;
@@ -46,6 +47,43 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /** Supports package command in the bin/solr script. */
+@SuppressWarnings("UnnecessarilyFullyQualified")
[email protected](
+    name = "package",
+    description = {
+      "Install, deploy and manage Solr packages in SolrCloud.",
+      "Pass --help or -h after any command to see command-specific usage 
information.",
+      " Note: (a) Please add '--solr-url http://host:port' parameter if needed 
(usually on Windows).",
+      "       (b) Please make sure that all solr nodes are started with 
'-Dsolr.packages.enabled=true'."
+    },
+    exitCodeListHeading = "%nExit Codes:%n",
+    exitCodeList = {
+      "0:Operation completed successfully.",
+      "1:Operation failed; check output for details."
+    },
+    footerHeading = "%nExamples:%n",
+    subcommands = {
+      PackageAddRepo.class,
+      PackageAddKey.class,
+      PackageListInstalled.class,
+      PackageListAvailable.class,
+      PackageListDeployed.class,
+      PackageInstall.class,
+      PackageDeploy.class,
+      PackageUndeploy.class,
+      PackageUninstall.class
+    },
+    footer = {
+      "  # Add a package repository",
+      "  bin/solr package add-repo myrepo https://my.repo.example/repo";,
+      "",
+      "  # Install a package and deploy it to a collection",
+      "  bin/solr package install mypkg:1.0.0",
+      "  bin/solr package deploy mypkg:1.0.0 --collections myCollection -y",
+      "",
+      "  # List packages deployed on a collection",
+      "  bin/solr package list-deployed -c myCollection"
+    })
 public class PackageTool extends ToolBase {
 
   private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@@ -93,6 +131,29 @@ public class PackageTool extends ToolBase {
           .desc("Don't prompt for input; accept all default choices, defaults 
to false.")
           .get();
 
+  /** Parameters for package sub-commands, independent of the command line 
parser */
+  record PackageFlags(
+      String collections,
+      boolean cluster,
+      String[] parameters,
+      boolean update,
+      String collection,
+      boolean noPrompt) {}
+
+  @picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1")
+  ConnectionOptions connectionOptions;
+
+  @picocli.CommandLine.Mixin CredentialsOptions credentialsOptions;
+
+  @picocli.CommandLine.Spec private picocli.CommandLine.Model.CommandSpec spec;
+
+  private PackageManager packageManager;
+  private RepositoryManager repositoryManager;
+
+  public PackageTool() {
+    this(new DefaultToolRuntime());
+  }
+
   public PackageTool(ToolRuntime runtime) {
     super(runtime);
   }
@@ -102,17 +163,36 @@ public class PackageTool extends ToolBase {
     return "package";
   }
 
-  public PackageManager packageManager;
-  public RepositoryManager repositoryManager;
-
   @Override
+  public void runImpl(CommandLine cli) throws Exception {
+    String credentials = 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION);
+    String command = cli.getArgs()[0];
+    String[] cmdArgs = Arrays.copyOfRange(cli.getArgs(), 1, 
cli.getArgs().length);
+    PackageFlags packageFlags =
+        new PackageFlags(
+            cli.getOptionValue(COLLECTIONS_OPTION),
+            cli.hasOption(CLUSTER_OPTION),
+            cli.getOptionValues(PARAM_OPTION),
+            cli.hasOption(UPDATE_OPTION),
+            cli.getOptionValue(COLLECTION_OPTION),
+            cli.hasOption(NO_PROMPT_OPTION));
+
+    executePackage(cli, credentials, command, cmdArgs, packageFlags);
+  }
+
   @SuppressForbidden(
       reason =
           "We really need to print the stacktrace here, otherwise "
               + "there shall be little else information to debug problems. 
Other SolrCLI tools "
               + "don't print stack traces, hence special treatment is needed 
here."
               + "Need to turn off logging, and SLF4J doesn't seem to provide 
for a way.")
-  public void runImpl(CommandLine cli) throws Exception {
+  private void executePackage(
+      CommandLine cli,
+      String credentials,
+      String command,
+      String[] cmdArgs,
+      PackageFlags packageFlags)
+      throws Exception {
 
     // Need a logging free, clean output going through to the user.
     Level oldLevel = 
LoggerContext.getContext(false).getRootLogger().getLevel();
@@ -121,83 +201,23 @@ public class PackageTool extends ToolBase {
     try {
       String solrUrl = CLIUtils.normalizeSolrUrl(cli);
       String zkHost = CLIUtils.getZkHost(cli);
+
       if (zkHost == null) {
         throw new SolrException(ErrorCode.INVALID_STATE, "Package manager runs 
only in SolrCloud");
       }
 
       log.info("ZK: {}", zkHost);
 
-      String cmd = cli.getArgs()[0];
-
-      try (SolrClient solrClient =
-          CLIUtils.getSolrClient(
-              solrUrl, 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION), true)) {
+      try (SolrClient solrClient = CLIUtils.getSolrClient(solrUrl, 
credentials, true)) {
         packageManager = new PackageManager(runtime, solrClient, solrUrl, 
zkHost);
         try {
           repositoryManager = new RepositoryManager(solrClient, 
packageManager);
-
-          // Dispatches to a parser-independent method per sub-command
-          switch (cmd) {
-            case "add-repo":
-              addRepo(cli.getArgs()[1], cli.getArgs()[2]);
-              break;
-            case "add-key":
-              addKey(Path.of(cli.getArgs()[1]));
-              break;
-            case "list-installed":
-              listInstalled();
-              break;
-            case "list-available":
-              listAvailable();
-              break;
-            case "list-deployed":
-              if (cli.hasOption(COLLECTION_OPTION)) {
-                
listPackagesDeployedOnCollection(cli.getOptionValue(COLLECTION_OPTION));
-              } else {
-                // nuance that we use an arg here instead of requiring a 
--package parameter with a
-                // value in this code path
-                listCollectionsWithPackageDeployed(cli.getArgs()[1]);
-              }
-              break;
-            case "install":
-              install(cli.getArgList().get(1));
-              break;
-            case "deploy":
-              if (cli.hasOption(CLUSTER_OPTION) || 
cli.hasOption(COLLECTIONS_OPTION)) {
-                deploy(
-                    cli.getArgList().get(1),
-                    cli.hasOption(CLUSTER_OPTION),
-                    cli.getOptionValue(COLLECTIONS_OPTION),
-                    cli.getOptionValues(PARAM_OPTION),
-                    cli.hasOption(UPDATE_OPTION),
-                    cli.hasOption(NO_PROMPT_OPTION));
-              } else {
-                printRed(
-                    "Either specify --cluster to deploy cluster level plugins 
or --collections <list-of-collections> to deploy collection level plugins");
-              }
-              break;
-            case "undeploy":
-              if (cli.hasOption(CLUSTER_OPTION) || 
cli.hasOption(COLLECTIONS_OPTION)) {
-                undeploy(
-                    cli.getArgList().get(1),
-                    cli.hasOption(CLUSTER_OPTION),
-                    cli.getOptionValue(COLLECTIONS_OPTION));
-              } else {
-                printRed(
-                    "Either specify --cluster to undeploy cluster level 
plugins or -collections <list-of-collections> to undeploy collection level 
plugins");
-              }
-              break;
-            case "uninstall":
-              uninstall(cli.getArgList().get(1));
-              break;
-            default:
-              throw new RuntimeException("Unrecognized command: " + cmd);
-          }
+          handleCommand(command, cmdArgs, packageFlags);
         } finally {
           packageManager.close();
         }
       }
-      log.info("Finished: {}", cmd);
+      log.info("Finished: {}", command);
 
     } catch (Exception ex) {
       // We need to print this since SolrCLI drops the stack trace in favour
@@ -210,23 +230,66 @@ public class PackageTool extends ToolBase {
     }
   }
 
-  private void addRepo(String repoName, String repoUrl) throws Exception {
+  /**
+   * Picocli subcommand path: resolve connection, open managers, run action, 
close. Commons-cli runs
+   * through {@link #executePackage(CommandLine, String, String, String[], 
PackageFlags)} instead.
+   */
+  @SuppressForbidden(
+      reason =
+          "Package tool prints stack traces and turns off logging; this is 
same as executePackage.")
+  void runWithManagers(
+      ConnectionOptions opts, String credentials, 
PackageSubCommand.PackageAction action)
+      throws Exception {
+    // Need a logging free, clean output going through to the user.
+    Level oldLevel = 
LoggerContext.getContext(false).getRootLogger().getLevel();
+    Configurator.setRootLevel(Level.OFF);
+
+    try {
+      String solrUrl = ConnectionOptions.resolveSolrUrl(opts, credentials);
+      String zkHost = ConnectionOptions.resolveZkHost(opts, solrUrl, 
credentials);
+
+      if (zkHost == null) {
+        throw new SolrException(ErrorCode.INVALID_STATE, "Package manager runs 
only in SolrCloud");
+      }
+
+      log.info("ZK: {}", zkHost);
+
+      try (SolrClient solrClient = CLIUtils.getSolrClient(solrUrl, 
credentials, true)) {
+        packageManager = new PackageManager(runtime, solrClient, solrUrl, 
zkHost);
+        try {
+          repositoryManager = new RepositoryManager(solrClient, 
packageManager);
+          action.run(packageManager, repositoryManager);
+        } finally {
+          packageManager.close();
+        }
+      }
+
+    } catch (Exception ex) {
+      ex.printStackTrace();
+      throw ex;
+    } finally {
+      Configurator.setRootLevel(oldLevel);
+    }
+  }
+
+  void addRepo(RepositoryManager repositoryManager, String repoName, String 
repoUrl)
+      throws Exception {
     repositoryManager.addRepository(repoName, repoUrl);
     printGreen("Added repository: " + repoName);
   }
 
-  private void addKey(Path keyFile) throws Exception {
+  void addKey(RepositoryManager repositoryManager, Path keyFile) throws 
Exception {
     repositoryManager.addKey(Files.readAllBytes(keyFile), 
keyFile.getFileName().toString());
   }
 
-  private void listInstalled() throws Exception {
+  void listInstalled(PackageManager packageManager) throws Exception {
     printGreen("Installed packages:\n-----");
     for (SolrPackageInstance pkg : 
packageManager.fetchInstalledPackageInstances()) {
       printGreen(pkg);
     }
   }
 
-  private void listAvailable() throws Exception {
+  void listAvailable(RepositoryManager repositoryManager) throws Exception {
     printGreen("Available packages:\n-----");
     for (SolrPackage pkg : repositoryManager.getPackages()) {
       printGreen(pkg.name + " \t\t" + pkg.description);
@@ -236,7 +299,7 @@ public class PackageTool extends ToolBase {
     }
   }
 
-  private void listPackagesDeployedOnCollection(String collection) {
+  void listPackagesDeployedOnCollection(PackageManager packageManager, String 
collection) {
     Map<String, SolrPackageInstance> packages = 
packageManager.getPackagesDeployed(collection);
     printGreen("Packages deployed on " + collection + ":");
     for (String packageName : packages.keySet()) {
@@ -244,7 +307,7 @@ public class PackageTool extends ToolBase {
     }
   }
 
-  private void listCollectionsWithPackageDeployed(String packageName) {
+  void listCollectionsWithPackageDeployed(PackageManager packageManager, 
String packageName) {
     Map<String, String> deployedCollections = 
packageManager.getDeployedCollections(packageName);
     if (!deployedCollections.isEmpty()) {
       printGreen("Collections on which package " + packageName + " was 
deployed:");
@@ -263,7 +326,8 @@ public class PackageTool extends ToolBase {
     }
   }
 
-  private void install(String packageNameAndVersion) throws Exception {
+  boolean install(RepositoryManager repositoryManager, String 
packageNameAndVersion)
+      throws Exception {
     Pair<String, String> parsedVersion = 
parsePackageVersion(packageNameAndVersion);
     String packageName = parsedVersion.first();
     String version = parsedVersion.second();
@@ -273,12 +337,14 @@ public class PackageTool extends ToolBase {
     } else {
       printRed(packageName + " installation failed.");
     }
+    return success;
   }
 
   /**
    * @param collections raw comma-separated value of the --collections option, 
or null
    */
-  private void deploy(
+  void deploy(
+      PackageManager packageManager,
       String packageNameAndVersion,
       boolean cluster,
       String collections,
@@ -300,7 +366,11 @@ public class PackageTool extends ToolBase {
   /**
    * @param collections raw comma-separated value of the --collections option, 
or null
    */
-  private void undeploy(String packageNameAndVersion, boolean cluster, String 
collections)
+  void undeploy(
+      PackageManager packageManager,
+      String packageNameAndVersion,
+      boolean cluster,
+      String collections)
       throws Exception {
     Pair<String, String> parsedVersion = 
parsePackageVersion(packageNameAndVersion);
     if (parsedVersion.second() != null) {
@@ -316,7 +386,7 @@ public class PackageTool extends ToolBase {
     packageManager.undeploy(packageName, collectionArray, cluster);
   }
 
-  private void uninstall(String packageNameAndVersion) throws Exception {
+  void uninstall(PackageManager packageManager, String packageNameAndVersion) 
throws Exception {
     Pair<String, String> parsedVersion = 
parsePackageVersion(packageNameAndVersion);
     if (parsedVersion.second() == null) {
       throw new SolrException(
@@ -326,6 +396,62 @@ public class PackageTool extends ToolBase {
     packageManager.uninstall(parsedVersion.first(), parsedVersion.second());
   }
 
+  private void handleCommand(String command, String[] cmdArgs, PackageFlags 
packageFlags)
+      throws Exception {
+    switch (command) {
+      case "add-repo":
+        addRepo(repositoryManager, cmdArgs[0], cmdArgs[1]);
+        break;
+      case "add-key":
+        addKey(repositoryManager, Path.of(cmdArgs[0]));
+        break;
+      case "list-installed":
+        listInstalled(packageManager);
+        break;
+      case "list-available":
+        listAvailable(repositoryManager);
+        break;
+      case "list-deployed":
+        if (packageFlags.collection() != null) {
+          listPackagesDeployedOnCollection(packageManager, 
packageFlags.collection());
+        } else {
+          listCollectionsWithPackageDeployed(packageManager, cmdArgs[0]);
+        }
+        break;
+      case "install":
+        install(repositoryManager, cmdArgs[0]);
+        break;
+      case "deploy":
+        if (packageFlags.cluster() || packageFlags.collections() != null) {
+          deploy(
+              packageManager,
+              cmdArgs[0],
+              packageFlags.cluster(),
+              packageFlags.collections(),
+              packageFlags.parameters(),
+              packageFlags.update(),
+              packageFlags.noPrompt());
+        } else {
+          printRed(
+              "Either specify --cluster to deploy cluster level plugins or 
--collections <list-of-collections> to deploy collection level plugins");
+        }
+        break;
+      case "undeploy":
+        if (packageFlags.cluster() || packageFlags.collections() != null) {
+          undeploy(packageManager, cmdArgs[0], packageFlags.cluster(), 
packageFlags.collections());
+        } else {
+          printRed(
+              "Either specify --cluster to undeploy cluster level plugins or 
--collections <list-of-collections> to undeploy collection level plugins");
+        }
+        break;
+      case "uninstall":
+        uninstall(packageManager, cmdArgs[0]);
+        break;
+      default:
+        throw new RuntimeException("Unrecognized command: " + command);
+    }
+  }
+
   @Override
   public String getHeader() {
     StringBuilder sb = new StringBuilder();
@@ -415,6 +541,7 @@ public class PackageTool extends ToolBase {
 
   @Override
   public int callTool() throws Exception {
-    throw new UnsupportedOperationException("This tool does not yet support 
PicoCli");
+    spec.commandLine().usage(CLIO.getOutStream());
+    return 0;
   }
 }
diff --git a/solr/core/src/java/org/apache/solr/cli/PackageUndeploy.java 
b/solr/core/src/java/org/apache/solr/cli/PackageUndeploy.java
new file mode 100644
index 00000000000..d38da340407
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/cli/PackageUndeploy.java
@@ -0,0 +1,74 @@
+/*
+ * 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.solr.cli;
+
+import static org.apache.solr.cli.SolrCLI.printRed;
+
+/** Supports package undeploy command in the bin/solr script. */
+@SuppressWarnings("UnnecessarilyFullyQualified")
[email protected](
+    name = "undeploy",
+    description = "Undeploy a package from specified collection(s) or at 
cluster level.",
+    exitCodeListHeading = "%nExit Codes:%n",
+    exitCodeList = {
+      "0: Operation completed successfully.",
+      "1: Operation failed; check output for details."
+    },
+    footerHeading = "%nExamples:%n",
+    footer = {
+      "  # Undeploy a package from a collection",
+      "  bin/solr package undeploy mypkg --collections myCollection"
+    })
+public class PackageUndeploy extends PackageSubCommand {
+
+  @picocli.CommandLine.Parameters(
+      index = "0",
+      arity = "1",
+      paramLabel = "PACKAGE",
+      description = "Package name")
+  private String packageName;
+
+  @picocli.CommandLine.Option(
+      names = {"--cluster"},
+      description = "Specifies that this action should affect cluster-level 
plugins only.")
+  private boolean cluster;
+
+  @picocli.CommandLine.Option(
+      names = {"--collections"},
+      paramLabel = "COLLECTIONS",
+      description =
+          "Specifies that this action should affect plugins for the given 
collections only, excluding cluster level plugins.")
+  private String collections;
+
+  @Override
+  public int callTool() throws Exception {
+    if (!cluster && collections == null) {
+      printRed(
+          "Either specify --cluster to undeploy cluster level plugins or 
--collections <list-of-collections> to undeploy collection level plugins");
+      return 1;
+    }
+    return runWithManagers(
+        (packageManager, repositoryManager) ->
+            packageTool.undeploy(packageManager, packageName, cluster, 
collections));
+  }
+
+  @Override
+  public String getName() {
+    return "undeploy";
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/cli/PackageUninstall.java 
b/solr/core/src/java/org/apache/solr/cli/PackageUninstall.java
new file mode 100644
index 00000000000..0171ec552b3
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/cli/PackageUninstall.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.cli;
+
+/** Supports package uninstall command in the bin/solr script. */
+@SuppressWarnings("UnnecessarilyFullyQualified")
[email protected](
+    name = "uninstall",
+    description = "Uninstall any package with a specified version from Solr.",
+    exitCodeListHeading = "%nExit Codes:%n",
+    exitCodeList = {
+      "0: Operation completed successfully.",
+      "1: Operation failed; check output for details."
+    },
+    footerHeading = "%nExamples:%n",
+    footer = {
+      "  # Uninstall a specific package version",
+      "  bin/solr package uninstall mypkg:1.0.0",
+    })
+public class PackageUninstall extends PackageSubCommand {
+
+  @picocli.CommandLine.Parameters(
+      index = "0",
+      arity = "1",
+      paramLabel = "PACKAGE:VERSION",
+      description = "Package name and version, separated by a colon.")
+  private String packageNameAndVersion;
+
+  @Override
+  public int callTool() throws Exception {
+    return runWithManagers(
+        (packageManager, repositoryManager) ->
+            packageTool.uninstall(packageManager, packageNameAndVersion));
+  }
+
+  @Override
+  public String getName() {
+    return "uninstall";
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java 
b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java
index 6a40ce532db..555b30bf7c8 100755
--- a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java
+++ b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java
@@ -96,7 +96,8 @@ import org.slf4j.LoggerFactory;
       SnapshotDescribeShim.class,
       SnapshotExportShim.class,
       SnapshotListShim.class,
-      RunExampleTool.class
+      RunExampleTool.class,
+      PackageTool.class
     })
 public class SolrCLI implements CLIO, java.util.concurrent.Callable<Integer> {
 
diff --git a/solr/core/src/java/org/apache/solr/pkg/PackageAPI.java 
b/solr/core/src/java/org/apache/solr/pkg/PackageAPI.java
index 0fd1e29dc4a..4574f7b3318 100644
--- a/solr/core/src/java/org/apache/solr/pkg/PackageAPI.java
+++ b/solr/core/src/java/org/apache/solr/pkg/PackageAPI.java
@@ -407,13 +407,11 @@ public class PackageAPI {
     }
 
     private void syncToVersion(int expectedVersion) {
-      int origVersion = pkgs.znodeVersion;
       for (int i = 0; i < 10; i++) {
         log.debug("my version is {} , and expected version {}", 
pkgs.znodeVersion, expectedVersion);
         if (pkgs.znodeVersion >= expectedVersion) {
-          if (origVersion < pkgs.znodeVersion) {
-            packageLoader.refreshPackageConf();
-          }
+          // Blocks until any in-flight refresh started by the ZK watcher has 
been applied
+          packageLoader.refreshPackageConf();
           return;
         }
         try {
diff --git a/solr/core/src/java/org/apache/solr/pkg/SolrPackageLoader.java 
b/solr/core/src/java/org/apache/solr/pkg/SolrPackageLoader.java
index 34f6b136eb6..a512fe77914 100644
--- a/solr/core/src/java/org/apache/solr/pkg/SolrPackageLoader.java
+++ b/solr/core/src/java/org/apache/solr/pkg/SolrPackageLoader.java
@@ -80,7 +80,7 @@ public class SolrPackageLoader implements Closeable {
     return Map.of();
   }
 
-  public void refreshPackageConf() {
+  public synchronized void refreshPackageConf() {
     log.debug(
         "{} updated to version {}", ZkStateReader.SOLR_PKGS_PATH, 
packageAPI.pkgs.znodeVersion);
 
@@ -139,7 +139,7 @@ public class SolrPackageLoader implements Closeable {
     return changed;
   }
 
-  public void notifyListeners(String pkg) {
+  public synchronized void notifyListeners(String pkg) {
     SolrPackage p = packageClassLoaders.get(pkg);
     if (p != null) {
       List<SolrPackage> l = List.of(p);
diff --git a/solr/core/src/test/org/apache/solr/cli/ConnectionOptionsTest.java 
b/solr/core/src/test/org/apache/solr/cli/ConnectionOptionsTest.java
new file mode 100644
index 00000000000..c5b794c1f24
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/cli/ConnectionOptionsTest.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.cli;
+
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class ConnectionOptionsTest extends SolrCloudTestCase {
+
+  @BeforeClass
+  public static void setupCluster() throws Exception {
+    configureCluster(1).configure();
+  }
+
+  @Test
+  public void testResolveZkHostIgnoresEnvWhenSolrUrlProvided() throws 
Exception {
+    String solrUrl = cluster.getJettySolrRunner(0).getBaseUrl().toString();
+
+    System.setProperty("zkHost", "other-cluster:2181/solr");
+    System.setProperty("solr.connection", "other-cluster:2181/solr");
+
+    ConnectionOptions opts = new ConnectionOptions();
+    opts.solrUrl = solrUrl;
+
+    String resolved = ConnectionOptions.resolveZkHost(opts, solrUrl, null);
+    assertNotNull(resolved);
+    assertFalse(
+        "HTTP CLI target must not pick up a conflicting zkHost/sys property",
+        resolved.contains("other-cluster"));
+  }
+}
diff --git a/solr/core/src/test/org/apache/solr/cli/PackageToolPicocliTest.java 
b/solr/core/src/test/org/apache/solr/cli/PackageToolPicocliTest.java
new file mode 100644
index 00000000000..b850ee70cf1
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/cli/PackageToolPicocliTest.java
@@ -0,0 +1,32 @@
+/*
+ * 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.solr.cli;
+
+import picocli.CommandLine;
+
+/** Runs all {@link PackageToolTest} tests through the picocli invocation 
path. */
+public class PackageToolPicocliTest extends PackageToolTest {
+
+  @Override
+  protected int runTool(String[] args, ToolRuntime runtime) throws Exception {
+    PackageTool tool = new PackageTool(runtime);
+    return new CommandLine(tool)
+        .setDefaultValueProvider(new CliDefaultValueProvider())
+        .execute(args);
+  }
+}
diff --git a/solr/core/src/test/org/apache/solr/cli/PackageToolTest.java 
b/solr/core/src/test/org/apache/solr/cli/PackageToolTest.java
index 9282af37e52..a919b30555b 100644
--- a/solr/core/src/test/org/apache/solr/cli/PackageToolTest.java
+++ b/solr/core/src/test/org/apache/solr/cli/PackageToolTest.java
@@ -68,6 +68,9 @@ public class PackageToolTest extends SolrCloudTestCase {
             "conf1", 
TEST_PATH().resolve("configsets").resolve("cloud-minimal").resolve("conf"))
         .addConfig(
             "conf3", 
TEST_PATH().resolve("configsets").resolve("cloud-minimal").resolve("conf"))
+        .addConfig(
+            "conf-validation",
+            
TEST_PATH().resolve("configsets").resolve("cloud-minimal").resolve("conf"))
         .withSecurityJson(SecurityJson.SIMPLE)
         .configure();
 
@@ -89,21 +92,21 @@ public class PackageToolTest extends SolrCloudTestCase {
     return req;
   }
 
-  @Test
-  public void testPackageTool() throws Exception {
-    ToolRuntime runtime = new CLITestHelper.TestingRuntime(false);
+  protected int runTool(String[] args, ToolRuntime runtime) throws Exception {
     PackageTool tool = new PackageTool(runtime);
+    return tool.runTool(SolrCLI.processCommandLineArgs(tool, args));
+  }
 
+  @Test
+  public void testPackageTool() throws Exception {
     String solrUrl = cluster.getJettySolrRunner(0).getBaseUrl().toString();
 
     run(
-        tool,
         new String[] {
           "--solr-url", solrUrl, "list-installed", "--credentials", 
SecurityJson.USER_PASS
         });
 
     run(
-        tool,
         new String[] {
           "--solr-url",
           solrUrl,
@@ -115,13 +118,11 @@ public class PackageToolTest extends SolrCloudTestCase {
         });
 
     run(
-        tool,
         new String[] {
           "--solr-url", solrUrl, "list-available", "--credentials", 
SecurityJson.USER_PASS
         });
 
     run(
-        tool,
         new String[] {
           "--solr-url",
           solrUrl,
@@ -132,7 +133,6 @@ public class PackageToolTest extends SolrCloudTestCase {
         });
 
     run(
-        tool,
         new String[] {
           "--solr-url", solrUrl, "list-installed", "--credentials", 
SecurityJson.USER_PASS
         });
@@ -145,7 +145,6 @@ public class PackageToolTest extends SolrCloudTestCase {
     String rhPath = "/mypath2";
 
     run(
-        tool,
         new String[] {
           "--solr-url",
           solrUrl,
@@ -157,7 +156,6 @@ public class PackageToolTest extends SolrCloudTestCase {
 
     // Leaving -p in for --param to test the deprecated value continues to 
work.
     run(
-        tool,
         new String[] {
           "--solr-url",
           solrUrl,
@@ -175,7 +173,6 @@ public class PackageToolTest extends SolrCloudTestCase {
         "abc", "question-answer", "1.0.0", rhPath, "1.0.0", 
SecurityJson.USER_PASS);
 
     run(
-        tool,
         new String[] {
           "--solr-url",
           solrUrl,
@@ -186,7 +183,6 @@ public class PackageToolTest extends SolrCloudTestCase {
         });
 
     run(
-        tool,
         new String[] {
           "--solr-url",
           solrUrl,
@@ -206,7 +202,6 @@ public class PackageToolTest extends SolrCloudTestCase {
 
       // This command pegs the version to the latest available
       run(
-          tool,
           new String[] {
             "--solr-url",
             solrUrl,
@@ -222,7 +217,6 @@ public class PackageToolTest extends SolrCloudTestCase {
           "abc", "question-answer", "$LATEST", rhPath, "1.0.0", 
SecurityJson.USER_PASS);
 
       run(
-          tool,
           new String[] {
             "--solr-url",
             solrUrl,
@@ -237,7 +231,6 @@ public class PackageToolTest extends SolrCloudTestCase {
       log.info("Testing explicit deployment to a different/newer version");
 
       run(
-          tool,
           new String[] {
             "--solr-url",
             solrUrl,
@@ -252,7 +245,6 @@ public class PackageToolTest extends SolrCloudTestCase {
       // even if parameters are not passed in, they should be picked up from 
previous deployment
       if (random().nextBoolean()) {
         run(
-            tool,
             new String[] {
               "--solr-url",
               solrUrl,
@@ -269,7 +261,6 @@ public class PackageToolTest extends SolrCloudTestCase {
             });
       } else {
         run(
-            tool,
             new String[] {
               "--solr-url",
               solrUrl,
@@ -289,7 +280,6 @@ public class PackageToolTest extends SolrCloudTestCase {
 
     log.info("Running undeploy...");
     run(
-        tool,
         new String[] {
           "--solr-url",
           solrUrl,
@@ -302,7 +292,6 @@ public class PackageToolTest extends SolrCloudTestCase {
         });
 
     run(
-        tool,
         new String[] {
           "--solr-url",
           solrUrl,
@@ -378,27 +367,26 @@ public class PackageToolTest extends SolrCloudTestCase {
   public void testDeployValidationMessages() throws Exception {
     String solrUrl = cluster.getJettySolrRunner(0).getBaseUrl().toString();
 
-    withBasicAuth(CollectionAdminRequest.createCollection("validation-test", 
"conf1", 1, 1))
+    withBasicAuth(
+            CollectionAdminRequest.createCollection("validation-test", 
"conf-validation", 1, 1))
         .processAndWait(cluster.getSolrClient(), 10);
 
     CLITestHelper.TestingRuntime captureRuntime = new 
CLITestHelper.TestingRuntime(true);
-    PackageTool tool = new PackageTool(captureRuntime);
 
     // Collection exists but package does not — collection validation should 
pass,
     // package lookup should fail.
-    tool.runTool(
-        SolrCLI.processCommandLineArgs(
-            tool,
-            new String[] {
-              "--solr-url",
-              solrUrl,
-              "deploy",
-              "NONEXISTENT_PKG",
-              "--collections",
-              "validation-test",
-              "--credentials",
-              SecurityJson.USER_PASS
-            }));
+    runTool(
+        new String[] {
+          "--solr-url",
+          solrUrl,
+          "deploy",
+          "NONEXISTENT_PKG",
+          "--collections",
+          "validation-test",
+          "--credentials",
+          SecurityJson.USER_PASS
+        },
+        captureRuntime);
     String deployOut = captureRuntime.getOutput();
     assertFalse(
         "Should not complain about invalid collection", 
deployOut.contains("Invalid collection"));
@@ -409,19 +397,18 @@ public class PackageToolTest extends SolrCloudTestCase {
     captureRuntime.clearOutput();
 
     // Undeploy of a package that was never deployed should give a clear 
message.
-    tool.runTool(
-        SolrCLI.processCommandLineArgs(
-            tool,
-            new String[] {
-              "--solr-url",
-              solrUrl,
-              "undeploy",
-              "NONEXISTENT_PKG",
-              "--collections",
-              "validation-test",
-              "--credentials",
-              SecurityJson.USER_PASS
-            }));
+    runTool(
+        new String[] {
+          "--solr-url",
+          solrUrl,
+          "undeploy",
+          "NONEXISTENT_PKG",
+          "--collections",
+          "validation-test",
+          "--credentials",
+          SecurityJson.USER_PASS
+        },
+        captureRuntime);
     String undeployOut = captureRuntime.getOutput();
     assertFalse(
         "Should not complain about invalid collection", 
undeployOut.contains("Invalid collection"));
@@ -430,8 +417,8 @@ public class PackageToolTest extends SolrCloudTestCase {
         undeployOut.contains("Package NONEXISTENT_PKG not deployed on 
collection validation-test"));
   }
 
-  private void run(PackageTool tool, String[] args) throws Exception {
-    int res = tool.runTool(SolrCLI.processCommandLineArgs(tool, args));
+  private void run(String[] args) throws Exception {
+    int res = runTool(args, new CLITestHelper.TestingRuntime(false));
     assertEquals("Non-zero status returned for: " + Arrays.toString(args), 0, 
res);
   }
 
diff --git a/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc 
b/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc
index 034346e499d..b001d404ba3 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc
@@ -29,6 +29,16 @@
 *** xref:cli/solr-delete.adoc[delete]
 *** xref:cli/solr-export.adoc[export]
 *** xref:cli/solr-healthcheck.adoc[healthcheck]
+*** xref:cli/solr-package.adoc[package]
+**** xref:cli/solr-package-add-key.adoc[key]
+**** xref:cli/solr-package-add-repo.adoc[repo]
+**** xref:cli/solr-package-deploy.adoc[deploy]
+**** xref:cli/solr-package-install.adoc[install]
+**** xref:cli/solr-package-list-available.adoc[available]
+**** xref:cli/solr-package-list-deployed.adoc[deployed]
+**** xref:cli/solr-package-list-installed.adoc[installed]
+**** xref:cli/solr-package-undeploy.adoc[undeploy]
+**** xref:cli/solr-package-uninstall.adoc[uninstall]
 *** xref:cli/solr-post.adoc[post]
 *** xref:cli/solr-postlogs.adoc[postlogs]
 *** xref:cli/solr-snapshot.adoc[snapshot]
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/index.adoc 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/index.adoc
index 4dbe2b5dea4..318010391d5 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/index.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/index.adoc
@@ -68,6 +68,16 @@ Any command not listed here is unavailable when the picocli 
implementation is en
 |xref:cli/solr-delete.adoc[bin/solr delete]
 |xref:cli/solr-export.adoc[bin/solr export]
 |xref:cli/solr-healthcheck.adoc[bin/solr healthcheck]
+|xref:cli/solr-package-add-key.adoc[bin/solr package add key]
+|xref:cli/solr-package-add-repo.adoc[bin/solr package add repo]
+|xref:cli/solr-package-deploy.adoc[bin/solr package deploy]
+|xref:cli/solr-package-install.adoc[bin/solr package install]
+|xref:cli/solr-package-list-available.adoc[bin/solr package list available]
+|xref:cli/solr-package-list-deployed.adoc[bin/solr package list deployed]
+|xref:cli/solr-package-list-installed.adoc[bin/solr package list installed]
+|xref:cli/solr-package-undeploy.adoc[bin/solr package undeploy]
+|xref:cli/solr-package-uninstall.adoc[bin/solr package uninstall]
+|xref:cli/solr-package.adoc[bin/solr package]
 |xref:cli/solr-post.adoc[bin/solr post]
 |xref:cli/solr-postlogs.adoc[bin/solr postlogs]
 |xref:cli/solr-snapshot-create.adoc[bin/solr snapshot create]
diff --git 
a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-add-key.adoc
 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-add-key.adoc
new file mode 100644
index 00000000000..801afac3091
--- /dev/null
+++ 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-add-key.adoc
@@ -0,0 +1,96 @@
+// 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.
+//
+// DO NOT EDIT -- this page is auto-generated from picocli annotations.
+// To update: modify the @Command/@Option annotations in the Java source, then 
run:
+//   ./gradlew :solr:solr-ref-guide:generateCliDocs
+
+= bin/solr package add key
+:page-toclevels: 2
+
+[WARNING]
+====
+This page documents the *experimental* picocli implementation of `bin/solr`, 
which is not enabled by default.
+For what `bin/solr` does by default, see 
xref:solr-control-script-reference.adoc[].
+See xref:cli/index.adoc[] to enable this implementation and for its known 
limitations.
+====
+
+// tag::picocli-generated-man-section-synopsis[]
+== Synopsis
+
+....
+bin/solr package add-key [-hv] [-u=<credentials>] [-s=<solrConnection> |
+                         --solr-url=<solrUrl> | -z=<zkHost>] KEY-FILE
+....
+
+// end::picocli-generated-man-section-synopsis[]
+
+// tag::picocli-generated-man-section-description[]
+== Description
+
+Add a trusted key to Solr.
+
+// end::picocli-generated-man-section-description[]
+
+// tag::picocli-generated-man-section-options[]
+== Options
+
+*-h*, *--help*::
+  Print this help message and exit.
+
+*-s*, *--solr-connection*=_<solrConnection>_::
+  Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is 
defined in solr.in.sh; otherwise, defaults to localhost:9983.
+
+*--solr-url*=_<solrUrl>_::
+  Base Solr URL, which can be used to determine the zk-host if that's not 
known.
+
+*-u*, *--credentials*=_<credentials>_::
+  Credentials in the format username:password. Example: --credentials 
solr:SolrRocks
+
+*-v*, *--verbose*::
+  Enable verbose mode.
+
+*-z*, *--zk-host*=_<zkHost>_::
+  Zookeeper connection string; unnecessary if ZK_HOST is defined in 
solr.in.sh; otherwise, defaults to localhost:9983.
+
+// end::picocli-generated-man-section-options[]
+
+// tag::picocli-generated-man-section-arguments[]
+== Arguments
+
+_KEY-FILE_::
+  Path to a file containing the trusted key.
+
+// end::picocli-generated-man-section-arguments[]
+
+// tag::picocli-generated-man-section-exit-status[]
+== Exit Codes
+*0*::
+   Operation completed successfully.
+
+*1*::
+   Operation failed; check output for details.
+
+// end::picocli-generated-man-section-exit-status[]
+
+// tag::picocli-generated-man-section-footer[]
+== Examples
+[%hardbreaks]
+  # Add a trusted public key
+  bin/solr package add-key /path/to/public.key.der
+
+// end::picocli-generated-man-section-footer[]
diff --git 
a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-add-repo.adoc
 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-add-repo.adoc
new file mode 100644
index 00000000000..04f1eccd2fc
--- /dev/null
+++ 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-add-repo.adoc
@@ -0,0 +1,100 @@
+// 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.
+//
+// DO NOT EDIT -- this page is auto-generated from picocli annotations.
+// To update: modify the @Command/@Option annotations in the Java source, then 
run:
+//   ./gradlew :solr:solr-ref-guide:generateCliDocs
+
+= bin/solr package add repo
+:page-toclevels: 2
+
+[WARNING]
+====
+This page documents the *experimental* picocli implementation of `bin/solr`, 
which is not enabled by default.
+For what `bin/solr` does by default, see 
xref:solr-control-script-reference.adoc[].
+See xref:cli/index.adoc[] to enable this implementation and for its known 
limitations.
+====
+
+// tag::picocli-generated-man-section-synopsis[]
+== Synopsis
+
+....
+bin/solr package add-repo [-hv] [-u=<credentials>] [-s=<solrConnection> |
+                          --solr-url=<solrUrl> | -z=<zkHost>] REPOSITORY-NAME
+                          REPOSITORY-URL
+....
+
+// end::picocli-generated-man-section-synopsis[]
+
+// tag::picocli-generated-man-section-description[]
+== Description
+
+Add a package repository to Solr.
+
+// end::picocli-generated-man-section-description[]
+
+// tag::picocli-generated-man-section-options[]
+== Options
+
+*-h*, *--help*::
+  Print this help message and exit.
+
+*-s*, *--solr-connection*=_<solrConnection>_::
+  Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is 
defined in solr.in.sh; otherwise, defaults to localhost:9983.
+
+*--solr-url*=_<solrUrl>_::
+  Base Solr URL, which can be used to determine the zk-host if that's not 
known.
+
+*-u*, *--credentials*=_<credentials>_::
+  Credentials in the format username:password. Example: --credentials 
solr:SolrRocks
+
+*-v*, *--verbose*::
+  Enable verbose mode.
+
+*-z*, *--zk-host*=_<zkHost>_::
+  Zookeeper connection string; unnecessary if ZK_HOST is defined in 
solr.in.sh; otherwise, defaults to localhost:9983.
+
+// end::picocli-generated-man-section-options[]
+
+// tag::picocli-generated-man-section-arguments[]
+== Arguments
+
+_REPOSITORY-NAME_::
+  Name of the package repository.
+
+_REPOSITORY-URL_::
+  URL of the package repository.
+
+// end::picocli-generated-man-section-arguments[]
+
+// tag::picocli-generated-man-section-exit-status[]
+== Exit Codes
+*0*::
+   Operation completed successfully.
+
+*1*::
+   Operation failed; check output for details.
+
+// end::picocli-generated-man-section-exit-status[]
+
+// tag::picocli-generated-man-section-footer[]
+== Examples
+[%hardbreaks]
+  # Add a package repository
+  bin/solr package add-repo myrepo https://my.repo.example/repo
+
+// end::picocli-generated-man-section-footer[]
diff --git 
a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-deploy.adoc
 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-deploy.adoc
new file mode 100644
index 00000000000..cd1b181a2a4
--- /dev/null
+++ 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-deploy.adoc
@@ -0,0 +1,116 @@
+// 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.
+//
+// DO NOT EDIT -- this page is auto-generated from picocli annotations.
+// To update: modify the @Command/@Option annotations in the Java source, then 
run:
+//   ./gradlew :solr:solr-ref-guide:generateCliDocs
+
+= bin/solr package deploy
+:page-toclevels: 2
+
+[WARNING]
+====
+This page documents the *experimental* picocli implementation of `bin/solr`, 
which is not enabled by default.
+For what `bin/solr` does by default, see 
xref:solr-control-script-reference.adoc[].
+See xref:cli/index.adoc[] to enable this implementation and for its known 
limitations.
+====
+
+// tag::picocli-generated-man-section-synopsis[]
+== Synopsis
+
+....
+bin/solr package deploy [-hvy] [--cluster] [--update] 
[--collections=COLLECTIONS]
+                        [-u=<credentials>] [-p=PARAMS]... [-s=<solrConnection> 
|
+                        --solr-url=<solrUrl> | -z=<zkHost>] PACKAGE[:VERSION]
+....
+
+// end::picocli-generated-man-section-synopsis[]
+
+// tag::picocli-generated-man-section-description[]
+== Description
+
+Deploy an installed package to collections or at cluster level.
+
+// end::picocli-generated-man-section-description[]
+
+// tag::picocli-generated-man-section-options[]
+== Options
+
+*--cluster*::
+  Specifies that this action should affect cluster-level plugins only.
+
+*--collections*=_COLLECTIONS_::
+  Specifies that this action should affect plugins for the given collections 
only, excluding cluster level plugins.
+
+*-h*, *--help*::
+  Print this help message and exit.
+
+*-p*, *--param*=_PARAMS_::
+  List of parameters to be used with deploy command.
+
+*-s*, *--solr-connection*=_<solrConnection>_::
+  Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is 
defined in solr.in.sh; otherwise, defaults to localhost:9983.
+
+*--solr-url*=_<solrUrl>_::
+  Base Solr URL, which can be used to determine the zk-host if that's not 
known.
+
+*-u*, *--credentials*=_<credentials>_::
+  Credentials in the format username:password. Example: --credentials 
solr:SolrRocks
+
+*--update*::
+  If a deployment is an update over a previous deployment.
+
+*-v*, *--verbose*::
+  Enable verbose mode.
+
+*-y*, *--no-prompt*::
+  Don't prompt for input; accept all default choices, defaults to false.
+
+*-z*, *--zk-host*=_<zkHost>_::
+  Zookeeper connection string; unnecessary if ZK_HOST is defined in 
solr.in.sh; otherwise, defaults to localhost:9983.
+
+// end::picocli-generated-man-section-options[]
+
+// tag::picocli-generated-man-section-arguments[]
+== Arguments
+
+_PACKAGE[:VERSION]_::
+  Package name, optionally with :version.
+
+// end::picocli-generated-man-section-arguments[]
+
+// tag::picocli-generated-man-section-exit-status[]
+== Exit Codes
+*0*::
+   Operation completed successfully.
+
+*1*::
+   Operation failed; check output for details.
+
+// end::picocli-generated-man-section-exit-status[]
+
+// tag::picocli-generated-man-section-footer[]
+== Examples
+[%hardbreaks]
+  # Deploy a package to a collection
+  bin/solr package deploy mypkg:1.0.0 --collections myCollection -y
+
+[%hardbreaks]
+  # Update an existing deployment
+  bin/solr package deploy mypkg --update --collections myCollection -y
+
+// end::picocli-generated-man-section-footer[]
diff --git 
a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-install.adoc
 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-install.adoc
new file mode 100644
index 00000000000..7734cedff14
--- /dev/null
+++ 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-install.adoc
@@ -0,0 +1,100 @@
+// 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.
+//
+// DO NOT EDIT -- this page is auto-generated from picocli annotations.
+// To update: modify the @Command/@Option annotations in the Java source, then 
run:
+//   ./gradlew :solr:solr-ref-guide:generateCliDocs
+
+= bin/solr package install
+:page-toclevels: 2
+
+[WARNING]
+====
+This page documents the *experimental* picocli implementation of `bin/solr`, 
which is not enabled by default.
+For what `bin/solr` does by default, see 
xref:solr-control-script-reference.adoc[].
+See xref:cli/index.adoc[] to enable this implementation and for its known 
limitations.
+====
+
+// tag::picocli-generated-man-section-synopsis[]
+== Synopsis
+
+....
+bin/solr package install [-hv] [-u=<credentials>] [-s=<solrConnection> |
+                         --solr-url=<solrUrl> | -z=<zkHost>] PACKAGE[:VERSION]
+....
+
+// end::picocli-generated-man-section-synopsis[]
+
+// tag::picocli-generated-man-section-description[]
+== Description
+
+Install a package into Solr.
+
+// end::picocli-generated-man-section-description[]
+
+// tag::picocli-generated-man-section-options[]
+== Options
+
+*-h*, *--help*::
+  Print this help message and exit.
+
+*-s*, *--solr-connection*=_<solrConnection>_::
+  Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is 
defined in solr.in.sh; otherwise, defaults to localhost:9983.
+
+*--solr-url*=_<solrUrl>_::
+  Base Solr URL, which can be used to determine the zk-host if that's not 
known.
+
+*-u*, *--credentials*=_<credentials>_::
+  Credentials in the format username:password. Example: --credentials 
solr:SolrRocks
+
+*-v*, *--verbose*::
+  Enable verbose mode.
+
+*-z*, *--zk-host*=_<zkHost>_::
+  Zookeeper connection string; unnecessary if ZK_HOST is defined in 
solr.in.sh; otherwise, defaults to localhost:9983.
+
+// end::picocli-generated-man-section-options[]
+
+// tag::picocli-generated-man-section-arguments[]
+== Arguments
+
+_PACKAGE[:VERSION]_::
+  Package name, optionally with :version.
+
+// end::picocli-generated-man-section-arguments[]
+
+// tag::picocli-generated-man-section-exit-status[]
+== Exit Codes
+*0*::
+   Operation completed successfully.
+
+*1*::
+   Operation failed; check output for details.
+
+// end::picocli-generated-man-section-exit-status[]
+
+// tag::picocli-generated-man-section-footer[]
+== Examples
+[%hardbreaks]
+  # Install a specific package version
+  bin/solr package install mypkg:1.0.0
+
+[%hardbreaks]
+  # Install the latest available version
+  bin/solr package install mypkg
+
+// end::picocli-generated-man-section-footer[]
diff --git 
a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-list-available.adoc
 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-list-available.adoc
new file mode 100644
index 00000000000..83110eb433d
--- /dev/null
+++ 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-list-available.adoc
@@ -0,0 +1,88 @@
+// 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.
+//
+// DO NOT EDIT -- this page is auto-generated from picocli annotations.
+// To update: modify the @Command/@Option annotations in the Java source, then 
run:
+//   ./gradlew :solr:solr-ref-guide:generateCliDocs
+
+= bin/solr package list available
+:page-toclevels: 2
+
+[WARNING]
+====
+This page documents the *experimental* picocli implementation of `bin/solr`, 
which is not enabled by default.
+For what `bin/solr` does by default, see 
xref:solr-control-script-reference.adoc[].
+See xref:cli/index.adoc[] to enable this implementation and for its known 
limitations.
+====
+
+// tag::picocli-generated-man-section-synopsis[]
+== Synopsis
+
+....
+bin/solr package list-available [-hv] [-u=<credentials>] [-s=<solrConnection> |
+                                --solr-url=<solrUrl> | -z=<zkHost>]
+....
+
+// end::picocli-generated-man-section-synopsis[]
+
+// tag::picocli-generated-man-section-description[]
+== Description
+
+Print a list of packages available in the repositories.
+
+// end::picocli-generated-man-section-description[]
+
+// tag::picocli-generated-man-section-options[]
+== Options
+
+*-h*, *--help*::
+  Print this help message and exit.
+
+*-s*, *--solr-connection*=_<solrConnection>_::
+  Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is 
defined in solr.in.sh; otherwise, defaults to localhost:9983.
+
+*--solr-url*=_<solrUrl>_::
+  Base Solr URL, which can be used to determine the zk-host if that's not 
known.
+
+*-u*, *--credentials*=_<credentials>_::
+  Credentials in the format username:password. Example: --credentials 
solr:SolrRocks
+
+*-v*, *--verbose*::
+  Enable verbose mode.
+
+*-z*, *--zk-host*=_<zkHost>_::
+  Zookeeper connection string; unnecessary if ZK_HOST is defined in 
solr.in.sh; otherwise, defaults to localhost:9983.
+
+// end::picocli-generated-man-section-options[]
+
+// tag::picocli-generated-man-section-exit-status[]
+== Exit Codes
+*0*::
+   Operation completed successfully.
+
+*1*::
+   Operation failed; check output for details.
+
+// end::picocli-generated-man-section-exit-status[]
+
+// tag::picocli-generated-man-section-footer[]
+== Examples
+[%hardbreaks]
+  # List packages available from configured repositories
+  bin/solr package list-available
+
+// end::picocli-generated-man-section-footer[]
diff --git 
a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-list-deployed.adoc
 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-list-deployed.adoc
new file mode 100644
index 00000000000..ecd8cdfb609
--- /dev/null
+++ 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-list-deployed.adoc
@@ -0,0 +1,104 @@
+// 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.
+//
+// DO NOT EDIT -- this page is auto-generated from picocli annotations.
+// To update: modify the @Command/@Option annotations in the Java source, then 
run:
+//   ./gradlew :solr:solr-ref-guide:generateCliDocs
+
+= bin/solr package list deployed
+:page-toclevels: 2
+
+[WARNING]
+====
+This page documents the *experimental* picocli implementation of `bin/solr`, 
which is not enabled by default.
+For what `bin/solr` does by default, see 
xref:solr-control-script-reference.adoc[].
+See xref:cli/index.adoc[] to enable this implementation and for its known 
limitations.
+====
+
+// tag::picocli-generated-man-section-synopsis[]
+== Synopsis
+
+....
+bin/solr package list-deployed [-hv] [-c=COLLECTION] [-u=<credentials>]
+                               [-s=<solrConnection> | --solr-url=<solrUrl> |
+                               -z=<zkHost>] [PACKAGE]
+....
+
+// end::picocli-generated-man-section-synopsis[]
+
+// tag::picocli-generated-man-section-description[]
+== Description
+
+Print packages deployed on a collection, or collections where a package is 
deployed.
+
+// end::picocli-generated-man-section-description[]
+
+// tag::picocli-generated-man-section-options[]
+== Options
+
+*-c*, *--collection*=_COLLECTION_::
+  The collection to apply the package to, not required.
+
+*-h*, *--help*::
+  Print this help message and exit.
+
+*-s*, *--solr-connection*=_<solrConnection>_::
+  Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is 
defined in solr.in.sh; otherwise, defaults to localhost:9983.
+
+*--solr-url*=_<solrUrl>_::
+  Base Solr URL, which can be used to determine the zk-host if that's not 
known.
+
+*-u*, *--credentials*=_<credentials>_::
+  Credentials in the format username:password. Example: --credentials 
solr:SolrRocks
+
+*-v*, *--verbose*::
+  Enable verbose mode.
+
+*-z*, *--zk-host*=_<zkHost>_::
+  Zookeeper connection string; unnecessary if ZK_HOST is defined in 
solr.in.sh; otherwise, defaults to localhost:9983.
+
+// end::picocli-generated-man-section-options[]
+
+// tag::picocli-generated-man-section-arguments[]
+== Arguments
+
+[_PACKAGE_]::
+  Package name; lists collections where this package is deployed.
+
+// end::picocli-generated-man-section-arguments[]
+
+// tag::picocli-generated-man-section-exit-status[]
+== Exit Codes
+*0*::
+   Operation completed successfully.
+
+*1*::
+   Operation failed; check output for details.
+
+// end::picocli-generated-man-section-exit-status[]
+
+// tag::picocli-generated-man-section-footer[]
+== Examples
+[%hardbreaks]
+  # List packages deployed on a collection
+  bin/solr package list-deployed -c myCollection
+
+[%hardbreaks]
+  # List collections where a package is deployed
+  bin/solr package list-deployed mypkg
+
+// end::picocli-generated-man-section-footer[]
diff --git 
a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-list-installed.adoc
 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-list-installed.adoc
new file mode 100644
index 00000000000..216b3e58bfb
--- /dev/null
+++ 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-list-installed.adoc
@@ -0,0 +1,88 @@
+// 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.
+//
+// DO NOT EDIT -- this page is auto-generated from picocli annotations.
+// To update: modify the @Command/@Option annotations in the Java source, then 
run:
+//   ./gradlew :solr:solr-ref-guide:generateCliDocs
+
+= bin/solr package list installed
+:page-toclevels: 2
+
+[WARNING]
+====
+This page documents the *experimental* picocli implementation of `bin/solr`, 
which is not enabled by default.
+For what `bin/solr` does by default, see 
xref:solr-control-script-reference.adoc[].
+See xref:cli/index.adoc[] to enable this implementation and for its known 
limitations.
+====
+
+// tag::picocli-generated-man-section-synopsis[]
+== Synopsis
+
+....
+bin/solr package list-installed [-hv] [-u=<credentials>] [-s=<solrConnection> |
+                                --solr-url=<solrUrl> | -z=<zkHost>]
+....
+
+// end::picocli-generated-man-section-synopsis[]
+
+// tag::picocli-generated-man-section-description[]
+== Description
+
+Print a list of packages installed in Solr.
+
+// end::picocli-generated-man-section-description[]
+
+// tag::picocli-generated-man-section-options[]
+== Options
+
+*-h*, *--help*::
+  Print this help message and exit.
+
+*-s*, *--solr-connection*=_<solrConnection>_::
+  Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is 
defined in solr.in.sh; otherwise, defaults to localhost:9983.
+
+*--solr-url*=_<solrUrl>_::
+  Base Solr URL, which can be used to determine the zk-host if that's not 
known.
+
+*-u*, *--credentials*=_<credentials>_::
+  Credentials in the format username:password. Example: --credentials 
solr:SolrRocks
+
+*-v*, *--verbose*::
+  Enable verbose mode.
+
+*-z*, *--zk-host*=_<zkHost>_::
+  Zookeeper connection string; unnecessary if ZK_HOST is defined in 
solr.in.sh; otherwise, defaults to localhost:9983.
+
+// end::picocli-generated-man-section-options[]
+
+// tag::picocli-generated-man-section-exit-status[]
+== Exit Codes
+*0*::
+   Operation completed successfully.
+
+*1*::
+   Operation failed; check output for details.
+
+// end::picocli-generated-man-section-exit-status[]
+
+// tag::picocli-generated-man-section-footer[]
+== Examples
+[%hardbreaks]
+  # List all installed packages
+  bin/solr package list-installed
+
+// end::picocli-generated-man-section-footer[]
diff --git 
a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-undeploy.adoc
 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-undeploy.adoc
new file mode 100644
index 00000000000..dd61fbb36a0
--- /dev/null
+++ 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-undeploy.adoc
@@ -0,0 +1,103 @@
+// 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.
+//
+// DO NOT EDIT -- this page is auto-generated from picocli annotations.
+// To update: modify the @Command/@Option annotations in the Java source, then 
run:
+//   ./gradlew :solr:solr-ref-guide:generateCliDocs
+
+= bin/solr package undeploy
+:page-toclevels: 2
+
+[WARNING]
+====
+This page documents the *experimental* picocli implementation of `bin/solr`, 
which is not enabled by default.
+For what `bin/solr` does by default, see 
xref:solr-control-script-reference.adoc[].
+See xref:cli/index.adoc[] to enable this implementation and for its known 
limitations.
+====
+
+// tag::picocli-generated-man-section-synopsis[]
+== Synopsis
+
+....
+bin/solr package undeploy [-hv] [--cluster] [--collections=COLLECTIONS]
+                          [-u=<credentials>] [-s=<solrConnection> |
+                          --solr-url=<solrUrl> | -z=<zkHost>] PACKAGE
+....
+
+// end::picocli-generated-man-section-synopsis[]
+
+// tag::picocli-generated-man-section-description[]
+== Description
+
+Undeploy a package from specified collection(s) or at cluster level.
+
+// end::picocli-generated-man-section-description[]
+
+// tag::picocli-generated-man-section-options[]
+== Options
+
+*--cluster*::
+  Specifies that this action should affect cluster-level plugins only.
+
+*--collections*=_COLLECTIONS_::
+  Specifies that this action should affect plugins for the given collections 
only, excluding cluster level plugins.
+
+*-h*, *--help*::
+  Print this help message and exit.
+
+*-s*, *--solr-connection*=_<solrConnection>_::
+  Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is 
defined in solr.in.sh; otherwise, defaults to localhost:9983.
+
+*--solr-url*=_<solrUrl>_::
+  Base Solr URL, which can be used to determine the zk-host if that's not 
known.
+
+*-u*, *--credentials*=_<credentials>_::
+  Credentials in the format username:password. Example: --credentials 
solr:SolrRocks
+
+*-v*, *--verbose*::
+  Enable verbose mode.
+
+*-z*, *--zk-host*=_<zkHost>_::
+  Zookeeper connection string; unnecessary if ZK_HOST is defined in 
solr.in.sh; otherwise, defaults to localhost:9983.
+
+// end::picocli-generated-man-section-options[]
+
+// tag::picocli-generated-man-section-arguments[]
+== Arguments
+
+_PACKAGE_::
+  Package name
+
+// end::picocli-generated-man-section-arguments[]
+
+// tag::picocli-generated-man-section-exit-status[]
+== Exit Codes
+*0*::
+   Operation completed successfully.
+
+*1*::
+   Operation failed; check output for details.
+
+// end::picocli-generated-man-section-exit-status[]
+
+// tag::picocli-generated-man-section-footer[]
+== Examples
+[%hardbreaks]
+  # Undeploy a package from a collection
+  bin/solr package undeploy mypkg --collections myCollection
+
+// end::picocli-generated-man-section-footer[]
diff --git 
a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-uninstall.adoc
 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-uninstall.adoc
new file mode 100644
index 00000000000..689984fa85a
--- /dev/null
+++ 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package-uninstall.adoc
@@ -0,0 +1,96 @@
+// 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.
+//
+// DO NOT EDIT -- this page is auto-generated from picocli annotations.
+// To update: modify the @Command/@Option annotations in the Java source, then 
run:
+//   ./gradlew :solr:solr-ref-guide:generateCliDocs
+
+= bin/solr package uninstall
+:page-toclevels: 2
+
+[WARNING]
+====
+This page documents the *experimental* picocli implementation of `bin/solr`, 
which is not enabled by default.
+For what `bin/solr` does by default, see 
xref:solr-control-script-reference.adoc[].
+See xref:cli/index.adoc[] to enable this implementation and for its known 
limitations.
+====
+
+// tag::picocli-generated-man-section-synopsis[]
+== Synopsis
+
+....
+bin/solr package uninstall [-hv] [-u=<credentials>] [-s=<solrConnection> |
+                           --solr-url=<solrUrl> | -z=<zkHost>] PACKAGE:VERSION
+....
+
+// end::picocli-generated-man-section-synopsis[]
+
+// tag::picocli-generated-man-section-description[]
+== Description
+
+Uninstall any package with a specified version from Solr.
+
+// end::picocli-generated-man-section-description[]
+
+// tag::picocli-generated-man-section-options[]
+== Options
+
+*-h*, *--help*::
+  Print this help message and exit.
+
+*-s*, *--solr-connection*=_<solrConnection>_::
+  Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is 
defined in solr.in.sh; otherwise, defaults to localhost:9983.
+
+*--solr-url*=_<solrUrl>_::
+  Base Solr URL, which can be used to determine the zk-host if that's not 
known.
+
+*-u*, *--credentials*=_<credentials>_::
+  Credentials in the format username:password. Example: --credentials 
solr:SolrRocks
+
+*-v*, *--verbose*::
+  Enable verbose mode.
+
+*-z*, *--zk-host*=_<zkHost>_::
+  Zookeeper connection string; unnecessary if ZK_HOST is defined in 
solr.in.sh; otherwise, defaults to localhost:9983.
+
+// end::picocli-generated-man-section-options[]
+
+// tag::picocli-generated-man-section-arguments[]
+== Arguments
+
+_PACKAGE:VERSION_::
+  Package name and version, separated by a colon.
+
+// end::picocli-generated-man-section-arguments[]
+
+// tag::picocli-generated-man-section-exit-status[]
+== Exit Codes
+*0*::
+   Operation completed successfully.
+
+*1*::
+   Operation failed; check output for details.
+
+// end::picocli-generated-man-section-exit-status[]
+
+// tag::picocli-generated-man-section-footer[]
+== Examples
+[%hardbreaks]
+  # Uninstall a specific package version
+  bin/solr package uninstall mypkg:1.0.0
+
+// end::picocli-generated-man-section-footer[]
diff --git 
a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package.adoc 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package.adoc
new file mode 100644
index 00000000000..ba00cf3e03b
--- /dev/null
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package.adoc
@@ -0,0 +1,132 @@
+// 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.
+//
+// DO NOT EDIT -- this page is auto-generated from picocli annotations.
+// To update: modify the @Command/@Option annotations in the Java source, then 
run:
+//   ./gradlew :solr:solr-ref-guide:generateCliDocs
+
+= bin/solr package
+:page-toclevels: 2
+
+[WARNING]
+====
+This page documents the *experimental* picocli implementation of `bin/solr`, 
which is not enabled by default.
+For what `bin/solr` does by default, see 
xref:solr-control-script-reference.adoc[].
+See xref:cli/index.adoc[] to enable this implementation and for its known 
limitations.
+====
+
+// tag::picocli-generated-man-section-synopsis[]
+== Synopsis
+
+....
+bin/solr package [-hv] [-u=<credentials>] [-s=<solrConnection> |
+                 --solr-url=<solrUrl> | -z=<zkHost>] [COMMAND]
+....
+
+// end::picocli-generated-man-section-synopsis[]
+
+// tag::picocli-generated-man-section-description[]
+== Description
+
+Install, deploy and manage Solr packages in SolrCloud.
+Pass --help or -h after any command to see command-specific usage information.
+ Note: (a) Please add '--solr-url http://host:port' parameter if needed 
(usually on Windows).
+       (b) Please make sure that all solr nodes are started with 
'-Dsolr.packages.enabled=true'.
+
+// end::picocli-generated-man-section-description[]
+
+// tag::picocli-generated-man-section-options[]
+== Options
+
+*-h*, *--help*::
+  Print this help message and exit.
+
+*-s*, *--solr-connection*=_<solrConnection>_::
+  Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is 
defined in solr.in.sh; otherwise, defaults to localhost:9983.
+
+*--solr-url*=_<solrUrl>_::
+  Base Solr URL, which can be used to determine the zk-host if that's not 
known.
+
+*-u*, *--credentials*=_<credentials>_::
+  Credentials in the format username:password. Example: --credentials 
solr:SolrRocks
+
+*-v*, *--verbose*::
+  Enable verbose mode.
+
+*-z*, *--zk-host*=_<zkHost>_::
+  Zookeeper connection string; unnecessary if ZK_HOST is defined in 
solr.in.sh; otherwise, defaults to localhost:9983.
+
+// end::picocli-generated-man-section-options[]
+
+// tag::picocli-generated-man-section-commands[]
+== Commands
+
+xref:cli/solr-package-add-repo.adoc[*add-repo*]::
+  Add a package repository to Solr.
+
+xref:cli/solr-package-add-key.adoc[*add-key*]::
+  Add a trusted key to Solr.
+
+xref:cli/solr-package-list-installed.adoc[*list-installed*]::
+  Print a list of packages installed in Solr.
+
+xref:cli/solr-package-list-available.adoc[*list-available*]::
+  Print a list of packages available in the repositories.
+
+xref:cli/solr-package-list-deployed.adoc[*list-deployed*]::
+  Print packages deployed on a collection, or collections where a package is 
deployed.
+
+xref:cli/solr-package-install.adoc[*install*]::
+  Install a package into Solr.
+
+xref:cli/solr-package-deploy.adoc[*deploy*]::
+  Deploy an installed package to collections or at cluster level.
+
+xref:cli/solr-package-undeploy.adoc[*undeploy*]::
+  Undeploy a package from specified collection(s) or at cluster level.
+
+xref:cli/solr-package-uninstall.adoc[*uninstall*]::
+  Uninstall any package with a specified version from Solr.
+
+// end::picocli-generated-man-section-commands[]
+
+// tag::picocli-generated-man-section-exit-status[]
+== Exit Codes
+*0*::
+  Operation completed successfully.
+
+*1*::
+  Operation failed; check output for details.
+
+// end::picocli-generated-man-section-exit-status[]
+
+// tag::picocli-generated-man-section-footer[]
+== Examples
+[%hardbreaks]
+  # Add a package repository
+  bin/solr package add-repo myrepo https://my.repo.example/repo
+
+[%hardbreaks]
+  # Install a package and deploy it to a collection
+  bin/solr package install mypkg:1.0.0
+  bin/solr package deploy mypkg:1.0.0 --collections myCollection -y
+
+[%hardbreaks]
+  # List packages deployed on a collection
+  bin/solr package list-deployed -c myCollection
+
+// end::picocli-generated-man-section-footer[]

Reply via email to