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

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


The following commit(s) were added to refs/heads/main by this push:
     new d4c36141a4c3 CAMEL-23688: camel-jbang - Tests for update run, 
dependency copy, get error, init and doc (#27964)
d4c36141a4c3 is described below

commit d4c36141a4c3ddc3e4a507ece3a250a084084377
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 16 21:07:58 2026 +0200

    CAMEL-23688: camel-jbang - Tests for update run, dependency copy, get 
error, init and doc (#27964)
    
    Tests for commands that work on the project and the catalog, which had
    little coverage (update run 0-5%, dependency copy 13%, init 39%, doc 33%,
    get error 41%). They found these bugs, fixed here:
    
    * camel dependency copy deleted the whole --output-directory before it
      copied (--output-directory=. deleted the project); it deletes the JARs of
      an earlier copy only. It exited with 0 when a dependency could not be
      resolved or copied (now 1), and went on when --output-directory is a
      file (now 2).
    * camel update run: --download=false without --quarkusMavenPluginVersion
      failed with an IllegalStateException that named an option that does not
      exist (now a usage error), a version without recipes failed with a
      NullPointerException, and the error without a project named the command
      camel upgrade run. The project directory, the downloader and the Maven
      invocation can be replaced in a test.
    * camel get error --last showed the newest error of the integration with
      the lowest pid, not the newest one; an --ago that is not a duration was
      ignored, a --handled that is not true or false filtered out every error
      (both now 2); the errors of --diagram went to stdout.
    * camel init of a Java file in src/main/java failed with a
      StringIndexOutOfBoundsException, and a folder such as src/main/javafx
      gave a package.
    * camel doc documented an EIP in JSON, but not as text ("not found"); it
      does now, also with the prefix eip: for an EIP with the name of a
      component, such as eip:log.
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  12 +
 .../ROOT/pages/camel-jbang-getting-started.adoc    |   2 +
 .../pages/jbang-commands/camel-jbang-commands.adoc |   2 +-
 .../ROOT/pages/jbang-commands/camel-jbang-doc.adoc |   2 +-
 .../META-INF/camel-jbang-commands-metadata.json    |   2 +-
 .../dsl/jbang/core/commands/DependencyCopy.java    |  49 ++--
 .../apache/camel/dsl/jbang/core/commands/Init.java |  23 +-
 .../jbang/core/commands/catalog/CatalogDoc.java    |  60 ++++-
 .../dsl/jbang/core/commands/process/ListError.java |  66 ++---
 .../jbang/core/commands/update/CamelUpdate.java    |   5 +
 .../core/commands/update/CamelUpdateMixin.java     |   5 +-
 .../dsl/jbang/core/commands/update/UpdateRun.java  |  33 +--
 .../jbang/core/commands/DependencyCopyTest.java    | 149 ++++++++++++
 .../camel/dsl/jbang/core/commands/InitTest.java    | 111 +++++++++
 .../core/commands/catalog/CatalogDocTest.java      |  98 ++++++++
 .../jbang/core/commands/process/ListErrorTest.java | 247 ++++++++++++++-----
 .../jbang/core/commands/update/UpdateRunTest.java  | 269 +++++++++++++++++++++
 17 files changed, 996 insertions(+), 139 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 0cd7ed47f3a3..cf05e55fa98e 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -531,6 +531,18 @@ the integrations unsorted. A row without the value of the 
key now comes first, i
   of the CLI (`3.27.3.1` came after `3.27.4`), and a `-SNAPSHOT` always comes 
before its release, as in Maven
   (`4.24.10-SNAPSHOT` came after `4.24.10`).
 
+=== camel-jbang - camel dependency copy, update run and get error
+
+* `camel dependency copy` deletes the JAR files an earlier copy left in 
`--output-directory`, and keeps its other files:
+  it deleted the whole directory (with `--output-directory=.` the project). It 
exits with `1` when a dependency cannot
+  be resolved or copied (it printed the error and exited with `0`), and with 
`2` when `--output-directory` is a file.
+* `camel update run --runtime=quarkus --download=false` without 
`--quarkusMavenPluginVersion` exits with `2`, and a
+  version without upgrade recipes exits with `1`: both failed with an internal 
error (`70`).
+* `camel get error --last` shows the newest error of all the integrations (it 
showed the newest error of the integration
+  with the lowest pid). An `--ago` that is not a duration, and a `--handled` 
that is not `true` or `false`, exit with
+  `2`: the first showed every error, the second none. The errors of 
`--diagram` are printed on stderr, and a graphical
+  `--theme` in a terminal without graphics exits with `2`.
+
 === camel-jbang - the shared action file is deprecated for plugin commands
 
 `CamelCommand.getActionFile` and `CamelCommand.getOutputFile`, the action and 
output files that all the clients of an
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-jbang-getting-started.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-getting-started.adoc
index 996ad00194d3..12b90c61a9b8 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-getting-started.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-getting-started.adoc
@@ -172,11 +172,13 @@ Show quick reference documentation (description + all 
configuration options):
 camel doc kafka
 camel doc jackson
 camel doc aws-kinesis-sink
+camel doc split
 ----
 
 NOTE: This shows catalog-level documentation with option tables, not the full 
website documentation.
 
 If a component and data format share the same name, prefix with `dataformat:` 
(e.g., `camel doc dataformat:thrift`).
+The same goes for an EIP with the name of a component, such as `camel doc 
eip:log` for the Log EIP.
 
 Open the online documentation in a browser with `--open-url`, or get just the 
URL with `--url`:
 
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc
index ad5934d6c699..b8645eaee247 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc
@@ -21,7 +21,7 @@ TIP: You can also use `camel --help` or `camel <command> 
--help` to see availabl
 | xref:jbang-commands/camel-jbang-dependency.adoc[camel dependency] | Displays 
all Camel dependencies required to run
 | xref:jbang-commands/camel-jbang-dev.adoc[camel dev] | Run in dev mode with 
live reload
 | xref:jbang-commands/camel-jbang-dirty.adoc[camel dirty] | Check if there are 
dirty files from previous Camel runs that did not terminate gracefully
-| xref:jbang-commands/camel-jbang-doc.adoc[camel doc] | Shows documentation 
for kamelet, component, and other Camel resources
+| xref:jbang-commands/camel-jbang-doc.adoc[camel doc] | Shows documentation 
for kamelet, component, EIP, and other Camel resources
 | xref:jbang-commands/camel-jbang-doctor.adoc[camel doctor] | Checks the 
environment and reports potential issues
 | xref:jbang-commands/camel-jbang-eval.adoc[camel eval] | Evaluate Camel 
expressions and scripts
 | xref:jbang-commands/camel-jbang-explain.adoc[camel explain] | Explain what a 
Camel route does using AI/LLM
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-doc.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-doc.adoc
index 3cdcab43ce71..786f9f4278ea 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-doc.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-doc.adoc
@@ -2,7 +2,7 @@
 // AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE
 = camel doc
 
-Shows documentation for kamelet, component, and other Camel resources
+Shows documentation for kamelet, component, EIP, and other Camel resources
 
 
 == Usage
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index ce92c5bcddae..dc38a95d8ecf 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -10,7 +10,7 @@
     { "name": "dependency", "fullName": "dependency", "description": "Displays 
all Camel dependencies required to run", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.DependencyCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"copy", "fullName": "dependency copy", "description": "Copies all Camel 
dependencies required to run to a specific directory", "sourc [...]
     { "name": "dev", "fullName": "dev", "description": "Run in dev mode with 
live reload", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Dev", 
"options": [ { "names": "--background", "description": "Run in the background", 
"defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background-wait", "description": "How long to wait for the integration in 
the background to start, before returning, in millis or as a duration such as 
2m (true waits 60 secon [...]
     { "name": "dirty", "fullName": "dirty", "description": "Check if there are 
dirty files from previous Camel runs that did not terminate gracefully", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.Dirty", 
"options": [ { "names": "--clean", "description": "Clean dirty files which are 
no longer in use", "defaultValue": "false", "javaType": "boolean", "type": 
"boolean" }, { "names": "-h,--help", "description": "Display the help and 
sub-commands", "javaType": "boolean", " [...]
-    { "name": "doc", "fullName": "doc", "description": "Shows documentation 
for kamelet, component, and other Camel resources", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.catalog.CatalogDoc", "options": [ { 
"names": "--camel-version", "description": "To use a different Camel version 
than the default version", "javaType": "java.lang.String", "type": "string" }, 
{ "names": "--download", "description": "Whether to allow automatic downloading 
JAR dependencies (over the internet [...]
+    { "name": "doc", "fullName": "doc", "description": "Shows documentation 
for kamelet, component, EIP, and other Camel resources", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.catalog.CatalogDoc", "options": [ { 
"names": "--camel-version", "description": "To use a different Camel version 
than the default version", "javaType": "java.lang.String", "type": "string" }, 
{ "names": "--download", "description": "Whether to allow automatic downloading 
JAR dependencies (over the int [...]
     { "name": "doctor", "fullName": "doctor", "description": "Checks the 
environment and reports potential issues", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Doctor", "options": [ { "names": 
"--json", "description": "Output in JSON Format: the status (OK, WARN or FAIL) 
of each check", "javaType": "boolean", "type": "boolean" }, { "names": 
"-h,--help", "description": "Display the help and sub-commands", "javaType": 
"boolean", "type": "boolean" } ] },
     { "name": "eval", "fullName": "eval", "description": "Evaluate Camel 
expressions and scripts", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.EvalCommand", "options": [ { "names": 
"-h,--help", "description": "Display the help and sub-commands", "javaType": 
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "expression", 
"fullName": "eval expression", "description": "Evaluates Camel expression", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.EvalEx [...]
     { "name": "explain", "fullName": "explain", "description": "Explain what a 
Camel route does using AI\/LLM", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Explain", "options": [ { "names": 
"--api-key", "description": "API key for authentication. Also reads 
ANTHROPIC_API_KEY, OPENAI_API_KEY, WATSONX_APIKEY, or LLM_API_KEY env vars", 
"javaType": "java.lang.String", "type": "string" }, { "names": "--api-type", 
"description": "API type: 'ollama', 'openai' (OpenAI-compatible), ' [...]
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/DependencyCopy.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/DependencyCopy.java
index 7f4d447b5973..d1ec34795362 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/DependencyCopy.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/DependencyCopy.java
@@ -17,18 +17,19 @@
 package org.apache.camel.dsl.jbang.core.commands;
 
 import java.io.IOException;
-import java.io.UncheckedIOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.List;
 import java.util.Set;
+import java.util.stream.Stream;
 
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
 import org.apache.camel.tooling.maven.MavenArtifact;
 import org.apache.camel.tooling.maven.MavenDownloader;
 import org.apache.camel.tooling.maven.MavenGav;
 import org.apache.camel.tooling.maven.MavenResolutionException;
-import org.apache.camel.util.FileUtil;
 import picocli.CommandLine;
 
 @CommandLine.Command(name = "copy",
@@ -49,27 +50,45 @@ public class DependencyCopy extends DependencyList {
 
     private boolean outputDirectoryCreated = false;
 
+    // the artifacts that could not be resolved or copied
+    private int failures;
+
     public DependencyCopy(CamelJBangMain main) {
         super(main);
     }
 
+    @Override
+    public Integer doCall() throws Exception {
+        Path dir = Paths.get(outputDirectory);
+        if (Files.exists(dir) && !Files.isDirectory(dir)) {
+            throw CliException.usage("--output-directory must be a directory, 
was: " + outputDirectory);
+        }
+        int answer = super.doCall();
+        if (answer == 0 && failures > 0) {
+            // each artifact that failed is printed on stderr
+            return CliExitCode.FAILURE;
+        }
+        return answer;
+    }
+
     private void createOutputDirectory() {
         if (!outputDirectoryCreated) {
             outputDirectoryCreated = true;
-            Path outputDirectoryPath = Paths.get(outputDirectory);
-            if (Files.exists(outputDirectoryPath)) {
-                if (Files.isDirectory(outputDirectoryPath)) {
-                    FileUtil.removeDir(outputDirectoryPath.toFile());
-                } else {
-                    printer().printErr("Error creating the output directory: " 
+ outputDirectory
-                                       + " is not a directory");
-                    return;
-                }
-            }
+            Path dir = Paths.get(outputDirectory);
             try {
-                Files.createDirectories(outputDirectoryPath);
+                if (Files.isDirectory(dir)) {
+                    // the JARs of an earlier copy, which can be of other 
versions, but not the other files of the
+                    // directory: it is not always one that only holds the 
dependencies, such as with
+                    // --output-directory=.
+                    try (Stream<Path> files = Files.list(dir)) {
+                        for (Path jar : files.filter(f -> 
f.getFileName().toString().endsWith(".jar")).toList()) {
+                            Files.delete(jar);
+                        }
+                    }
+                }
+                Files.createDirectories(dir);
             } catch (IOException e) {
-                throw new UncheckedIOException("Error creating the output 
directory: " + outputDirectory, e);
+                throw CliException.failure("Cannot create the output directory 
" + outputDirectory + ": " + e.getMessage());
             }
         }
     }
@@ -88,8 +107,10 @@ public class DependencyCopy extends DependencyList {
                 Files.copy(artifact.getFile().toPath(), target);
             }
         } catch (MavenResolutionException e) {
+            failures++;
             printer().printErr("Error resolving the artifact: " + gav + " due 
to: " + e.getMessage());
         } catch (IOException e) {
+            failures++;
             printer().printErr("Error copying the artifact: " + gav + " due 
to: " + e.getMessage());
         }
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java
index dd6fbf6df210..8bb3caf32a0c 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java
@@ -16,7 +16,6 @@
  */
 package org.apache.camel.dsl.jbang.core.commands;
 
-import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -240,19 +239,19 @@ public class Init extends CamelCommand {
     /**
      * @return The package declaration lines to insert at the beginning of the 
file or empty string if no package found
      */
-    private String computeJavaPackageDeclaration(Path targetPath) throws 
IOException {
-        String packageDeclaration = "";
-        String canonicalPath = 
targetPath.getParent().toAbsolutePath().normalize().toString();
-        String srcMainJavaPath = Paths.get("src", "main", "java").toString();
-        int index = canonicalPath.indexOf(srcMainJavaPath);
-        if (index != -1) {
-            String packagePath = canonicalPath.substring(index + 
srcMainJavaPath.length() + 1);
-            String packageName = packagePath.replace(File.separatorChar, '.');
-            if (!packageName.isEmpty()) {
-                packageDeclaration = "package " + packageName + ";\n\n";
+    private String computeJavaPackageDeclaration(Path targetPath) {
+        // the directories after src/main/java (as whole names: not 
src/main/javafx, and none for src/main/java itself)
+        List<String> names = new ArrayList<>();
+        for (Path name : targetPath.toAbsolutePath().normalize().getParent()) {
+            names.add(name.toString());
+        }
+        for (int i = names.size() - 3; i >= 0; i--) {
+            if ("src".equals(names.get(i)) && "main".equals(names.get(i + 1)) 
&& "java".equals(names.get(i + 2))) {
+                String packageName = String.join(".", names.subList(i + 3, 
names.size()));
+                return packageName.isEmpty() ? "" : "package " + packageName + 
";\n\n";
             }
         }
-        return packageDeclaration;
+        return "";
     }
 
     private int listTemplates() {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDoc.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDoc.java
index 5de4bbdd0b93..c3f250438043 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDoc.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDoc.java
@@ -48,6 +48,7 @@ import org.apache.camel.tooling.maven.MavenGav;
 import org.apache.camel.tooling.model.BaseOptionModel;
 import org.apache.camel.tooling.model.ComponentModel;
 import org.apache.camel.tooling.model.DataFormatModel;
+import org.apache.camel.tooling.model.EipModel;
 import org.apache.camel.tooling.model.JsonMapper;
 import org.apache.camel.tooling.model.LanguageModel;
 import org.apache.camel.tooling.model.MainModel;
@@ -59,7 +60,8 @@ import picocli.CommandLine;
 import static 
org.apache.camel.dsl.jbang.core.commands.catalog.CatalogBaseCommand.findComponentNames;
 
 @CommandLine.Command(name = "doc",
-                     description = "Shows documentation for kamelet, 
component, and other Camel resources", sortOptions = false,
+                     description = "Shows documentation for kamelet, 
component, EIP, and other Camel resources",
+                     sortOptions = false,
                      showDefaultValues = true,
                      footer = {
                              "%nExamples:",
@@ -67,10 +69,12 @@ import static 
org.apache.camel.dsl.jbang.core.commands.catalog.CatalogBaseComman
                              "  camel doc timer",
                              "  camel doc aws-s3-source",
                              "  camel doc jsonpath",
+                             "  camel doc split",
                              "  camel doc kafka --example" })
 public class CatalogDoc extends CamelCommand {
 
-    @CommandLine.Parameters(description = "Name of kamelet, component, 
dataformat, or other Camel resource",
+    @CommandLine.Parameters(description = "Name of kamelet, component, 
dataformat, language, EIP, or other Camel resource"
+                                          + " (prefix with eip: for an EIP 
with the name of a component, such as eip:log)",
                             arity = "1")
     String name;
 
@@ -192,6 +196,13 @@ public class CatalogDoc extends CamelCommand {
                 return 0;
             }
         }
+        if (prefix == null || "eip".equals(prefix)) {
+            EipModel em = catalog.eipModel(name);
+            if (em != null) {
+                docEip(em);
+                return 0;
+            }
+        }
         // last, as the Kamelet catalog is downloaded (a component is 
documented also offline)
         if (prefix == null || "kamelet".equals(prefix)) {
             KameletModel km = KameletCatalogHelper.loadKameletModel(name, 
kameletsVersion, mavenResolver.repos());
@@ -230,6 +241,7 @@ public class CatalogDoc extends CamelCommand {
                 case "dataformat" -> 
SuggestSimilarHelper.didYouMean(catalog.findDataFormatNames(), name);
                 case "language" -> 
SuggestSimilarHelper.didYouMean(catalog.findLanguageNames(), name);
                 case "other" -> 
SuggestSimilarHelper.didYouMean(catalog.findOtherNames(), name);
+                case "eip" -> 
SuggestSimilarHelper.didYouMean(catalog.findModelNames(), name);
                 default -> List.of();
             };
             if (!suggestions.isEmpty()) {
@@ -709,6 +721,48 @@ public class CatalogDoc extends CamelCommand {
         }
     }
 
+    private void docEip(EipModel em) throws Exception {
+        String link = websiteLink("eip", em.getName(), 
catalog.getCatalogVersion());
+        if (openUrl) {
+            if (link != null) {
+                Desktop.getDesktop().browse(new URI(link));
+            }
+            return;
+        }
+        if (url) {
+            if (link != null) {
+                printer().println(link);
+            }
+            return;
+        }
+
+        if (em.isDeprecated()) {
+            printer().printf("EIP Name: %s (deprecated)%n", em.getName());
+        } else {
+            printer().printf("EIP Name: %s%n", em.getName());
+        }
+        printer().printf("Since: %s%n", 
fixQuarkusSince(em.getFirstVersionShort()));
+        printer().println("");
+        printer().printf("%s%n", em.getDescription());
+        printer().println("");
+        var filtered = filter(filter, em.getOptions());
+        var total1 = em.getOptions().size();
+        var total2 = filtered.size();
+        if (total1 == total2) {
+            printer().printf("The %s EIP supports (total: %s) options, which 
are listed below.%n%n", em.getName(), total1);
+        } else {
+            printer().printf("The %s EIP supports (total: %s match-filter: %s) 
options, which are listed below.%n%n",
+                    em.getName(), total1, total2);
+        }
+        printer().println(optionTable(filtered));
+        printer().println("");
+
+        if (link != null) {
+            printer().println(link);
+            printer().println("");
+        }
+    }
+
     private int[] docColumnWidths(int nameMax, int descMax, int defaultMax, 
int typeMax) {
         int tw = terminalWidth();
         int borders = TerminalWidthHelper.fancyBorderOverhead(4);
@@ -825,6 +879,8 @@ public class CatalogDoc extends CamelCommand {
             return 
String.format("https://camel.apache.org/components/%s/others/%s.html";, v, name);
         } else if ("kamelet".equals(prefix)) {
             return 
String.format("https://camel.apache.org/camel-kamelets/%s/%s.html";, v, name);
+        } else if ("eip".equals(prefix)) {
+            return 
String.format("https://camel.apache.org/components/%s/eips/%s-eip.html";, v, 
name);
         }
 
         return null;
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListError.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListError.java
index b83e6c4a31e7..e6914be3dffb 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListError.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListError.java
@@ -19,6 +19,7 @@ package org.apache.camel.dsl.jbang.core.commands.process;
 import java.awt.image.BufferedImage;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Comparator;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
@@ -39,6 +40,8 @@ import 
org.apache.camel.diagram.RouteDiagramRenderer.DiagramColors;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.commands.action.MessageTableHelper;
 import org.apache.camel.dsl.jbang.core.common.ActionClient;
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
 import org.apache.camel.dsl.jbang.core.common.CopyableRow;
 import org.apache.camel.dsl.jbang.core.common.PidNameAgeCompletionCandidates;
 import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
@@ -129,6 +132,21 @@ public class ListError extends ProcessWatchCommand {
     public Integer doProcessWatchCall() throws Exception {
         List<Row> rows = new ArrayList<>();
 
+        // the options are wrong for every integration, so before reading them
+        Comparator<Row> order = ORDER.comparator(sort);
+        if (handled != null && !"true".equalsIgnoreCase(handled) && 
!"false".equalsIgnoreCase(handled)) {
+            throw CliException.usage("--handled must be true or false, was: " 
+ handled);
+        }
+        long since = 0;
+        if (ago != null) {
+            try {
+                since = System.currentTimeMillis() - 
TimeUtils.toMilliSeconds(ago);
+            } catch (IllegalArgumentException e) {
+                throw CliException.usage("--ago must be a duration such as 
60s, 5m or 1h, was: " + ago);
+            }
+        }
+        long cutoff = since;
+
         if (diagram) {
             System.setProperty("java.awt.headless", "true");
             last = true;
@@ -200,7 +218,7 @@ public class ListError extends ProcessWatchCommand {
                         }
 
                         // apply client-side filters
-                        if (matchesFilters(row)) {
+                        if (matchesFilters(row, cutoff)) {
                             rows.add(row);
                         }
                     }
@@ -208,8 +226,8 @@ public class ListError extends ProcessWatchCommand {
             }
         });
 
-        // sort rows
-        rows.sort(ORDER.comparator(sort));
+        // the newest error of any integration, or the order of --sort
+        rows.sort(last ? ORDER.comparator("-age") : order);
 
         // apply limit
         List<Row> display = rows;
@@ -222,8 +240,8 @@ public class ListError extends ProcessWatchCommand {
             if (diagram) {
                 Row row = display.get(0);
                 if (row.messageHistory == null || row.messageHistory.length == 
0) {
-                    printer().println("No message history available for this 
error (enable message history in Camel)");
-                    return 1;
+                    printer().printErr("No message history available for this 
error (enable message history in Camel)");
+                    return CliExitCode.FAILURE;
                 }
                 return doDiagramCall(Long.parseLong(row.pid), row);
             }
@@ -329,8 +347,8 @@ public class ListError extends ProcessWatchCommand {
         }
 
         if (nodeIds.isEmpty()) {
-            printer().println("No node IDs found in error message history");
-            return 1;
+            printer().printErr("No node IDs found in error message history");
+            return CliExitCode.FAILURE;
         }
 
         RouteDiagramHelper.HighlightStyle hlStyle = 
RouteDiagramHelper.HighlightStyle.FAIL;
@@ -345,8 +363,8 @@ public class ListError extends ProcessWatchCommand {
 
         List<RouteInfo> routes = RouteDiagramHelper.parseRoutes(structureJson);
         if (routes.isEmpty()) {
-            printer().println("No routes found");
-            return 1;
+            printer().printErr("No routes found");
+            return CliExitCode.FAILURE;
         }
 
         // add structural parent node IDs for each route that has highlighted 
nodes
@@ -362,8 +380,8 @@ public class ListError extends ProcessWatchCommand {
                 = new RouteDiagramHelper.HighlightInfo(nodeIds, new 
ArrayList<>(routeOrderSet), hlStyle);
         routes = RouteDiagramHelper.filterAndOrderRoutes(routes, 
highlightInfo);
         if (routes.isEmpty()) {
-            printer().println("No routes contain highlighted nodes from error 
history");
-            return 1;
+            printer().printErr("No routes contain highlighted nodes from error 
history");
+            return CliExitCode.FAILURE;
         }
 
         // layout
@@ -393,16 +411,16 @@ public class ListError extends ProcessWatchCommand {
             try {
                 image = renderer.renderDiagram(layoutRoutes, currentY, colors, 
nodeIds, hlStyle);
             } catch (IllegalStateException e) {
-                printer().println(e.getMessage());
-                return 1;
+                printer().printErr(e.getMessage());
+                return CliExitCode.FAILURE;
             }
 
             try (Terminal terminal = 
TerminalBuilder.builder().system(true).build()) {
                 TerminalGraphics tg = 
TerminalGraphicsManager.getBestProtocol(terminal).orElse(null);
                 if (tg == null) {
-                    printer().println(
+                    printer().printErr(
                             "Terminal does not support graphics. Use 
--theme=ascii or --theme=unicode for text output.");
-                    return 1;
+                    return CliExitCode.USAGE;
                 }
                 TerminalGraphics.ImageOptions opts = new 
TerminalGraphics.ImageOptions().preserveAspectRatio(true);
                 tg.displayImage(terminal, image, opts);
@@ -436,7 +454,7 @@ public class ListError extends ProcessWatchCommand {
         }
     }
 
-    private boolean matchesFilters(Row row) {
+    private boolean matchesFilters(Row row, long cutoff) {
         if (id != null && !id.equals(row.exchangeId)) {
             return false;
         }
@@ -447,21 +465,11 @@ public class ListError extends ProcessWatchCommand {
                 || 
!row.exceptionType.toLowerCase().contains(exception.toLowerCase()))) {
             return false;
         }
-        if (handled != null && !handled.equals(String.valueOf(row.handled))) {
+        if (handled != null && 
!handled.equalsIgnoreCase(String.valueOf(row.handled))) {
             return false;
         }
-        if (ago != null) {
-            try {
-                long millis = TimeUtils.toMilliSeconds(ago);
-                long cutoff = System.currentTimeMillis() - millis;
-                if (row.timestamp < cutoff) {
-                    return false;
-                }
-            } catch (Exception e) {
-                // ignore invalid ago value
-            }
-        }
-        return true;
+        // older than --ago
+        return cutoff <= 0 || row.timestamp >= cutoff;
     }
 
     private String getAge(Row r) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/update/CamelUpdate.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/update/CamelUpdate.java
index d3e927c7c2cf..20805b7fb8d5 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/update/CamelUpdate.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/update/CamelUpdate.java
@@ -53,6 +53,11 @@ public final class CamelUpdate implements Update {
         List<Recipe> recipes;
         MavenArtifact mavenArtifact
                 = downloader.downloadArtifact("org.apache.camel.upgrade", 
getArtifactCoordinates(), updateMixin.version);
+        if (mavenArtifact == null) {
+            throw new CamelUpdateException(
+                    "Cannot find the Camel upgrade recipes 
org.apache.camel.upgrade:" + getArtifactCoordinates() + ":"
+                                           + updateMixin.version);
+        }
 
         try {
             recipes = getRecipesInJar(mavenArtifact.getFile().toPath());
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/update/CamelUpdateMixin.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/update/CamelUpdateMixin.java
index a3e0ee52d993..443f07ea2610 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/update/CamelUpdateMixin.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/update/CamelUpdateMixin.java
@@ -20,6 +20,7 @@ import java.util.List;
 
 import org.apache.camel.dsl.jbang.core.commands.MavenResolverMixin;
 import org.apache.camel.dsl.jbang.core.commands.QuarkusExtensionRegistryMixin;
+import org.apache.camel.dsl.jbang.core.common.CliException;
 import org.apache.camel.dsl.jbang.core.common.QuarkusHelper;
 import org.apache.camel.dsl.jbang.core.common.QuarkusHelper.QuarkusPlatformBom;
 import org.apache.camel.dsl.jbang.core.common.RuntimeCompletionCandidates;
@@ -100,8 +101,8 @@ public class CamelUpdateMixin {
             return MavenGav.fromCoordinates(quarkusMavenPluginGroupId, 
"quarkus-maven-plugin", quarkusMavenPluginVersion, "jar",
                     null);
         } else if (!mavenResolver.download()) {
-            throw new IllegalStateException(
-                    "Could not find default Quarkus Platform Maven BOM 
coordinates without --download true. You may want to either pass --download 
true or set --quarkus-version");
+            throw CliException.usage("--quarkusMavenPluginVersion is required 
with --download=false, as the version of "
+                                     + "the Quarkus Maven plugin is looked up 
in the Quarkus Extension Registry");
         } else {
             QuarkusPlatformBom result
                     = QuarkusHelper.findQuarkusPlatformBom(
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/update/UpdateRun.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/update/UpdateRun.java
index 561680fda17f..d6fb46faa642 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/update/UpdateRun.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/update/UpdateRun.java
@@ -24,7 +24,6 @@ import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
-import java.util.stream.Stream;
 
 import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
@@ -49,6 +48,9 @@ public class UpdateRun extends CamelCommand {
     @CommandLine.Mixin
     private CamelUpdateMixin updateMixin;
 
+    // the directory of the Maven project to update
+    Path projectDir = Path.of(".");
+
     public UpdateRun(CamelJBangMain main) {
         super(main);
     }
@@ -63,18 +65,14 @@ public class UpdateRun extends CamelCommand {
      */
     @Override
     public Integer doCall() throws Exception {
-        try (Stream<Path> files = Files.list(Path.of("."))) {
-            // Check if the current directory contains a Maven project (i.e., 
a pom.xml file)
-            if (files.noneMatch(f -> 
f.getFileName().toString().equals("pom.xml"))) {
-                printer().printErr("No Maven Project found in the current 
directory, " +
-                                   "please execute camel upgrade run command 
in the directory containing the Maven project to update");
-
-                return CliExitCode.NOT_FOUND;
-            }
+        if (!Files.isRegularFile(projectDir.resolve("pom.xml"))) {
+            printer().printErr("No Maven project found in the current 
directory: run camel update run in the directory "
+                               + "with the pom.xml of the project to update");
+            return CliExitCode.NOT_FOUND;
         }
 
         List<String> command = new ArrayList<>();
-        try (MavenDependencyDownloader downloader = new 
MavenDependencyDownloader();) {
+        try (MavenDependencyDownloader downloader = createDownloader()) {
             downloader.setRepositories(updateMixin.mavenResolver.repos());
             downloader.start();
 
@@ -94,8 +92,8 @@ public class UpdateRun extends CamelCommand {
 
                 return CliExitCode.FAILURE;
             } catch (DownloadException e) {
-                printer().printErr(String.format("Cannot find Camel Upgrade 
Recipes %s:%s:%s",
-                        "org.apache.camel.upgrade", 
update.getArtifactCoordinates(), updateMixin.version));
+                printer().printErr("Cannot download what the update to Camel " 
+ updateMixin.version + " needs: "
+                                   + e.getMessage());
 
                 return CliExitCode.FAILURE;
             }
@@ -104,6 +102,13 @@ public class UpdateRun extends CamelCommand {
         return executeCommand(command);
     }
 
+    /**
+     * The downloader of the upgrade recipes and versions.
+     */
+    protected MavenDependencyDownloader createDownloader() {
+        return new MavenDependencyDownloader();
+    }
+
     /**
      * Executes a shell command and prints its output.
      *
@@ -112,8 +117,8 @@ public class UpdateRun extends CamelCommand {
      * @throws IOException          if an I/O error occurs
      * @throws InterruptedException if the command execution is interrupted
      */
-    private int executeCommand(List<String> command) throws IOException, 
InterruptedException {
-        ProcessBuilder pb = new ProcessBuilder(command);
+    protected int executeCommand(List<String> command) throws IOException, 
InterruptedException {
+        ProcessBuilder pb = new 
ProcessBuilder(command).directory(projectDir.toFile());
         Process p = pb.redirectErrorStream(true)
                 .start();
 
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DependencyCopyTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DependencyCopyTest.java
new file mode 100644
index 000000000000..d50387268eb9
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DependencyCopyTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.camel.dsl.jbang.core.commands;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
+import org.apache.camel.tooling.maven.MavenArtifact;
+import org.apache.camel.tooling.maven.MavenDownloader;
+import org.apache.camel.tooling.maven.MavenGav;
+import org.apache.camel.tooling.maven.MavenResolutionException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.anySet;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.spy;
+
+/**
+ * camel dependency copy: the JARs of the dependencies of the integration, 
copied to a directory. The dependencies are
+ * not downloaded: each resolves to a JAR of its own.
+ */
+class DependencyCopyTest extends CamelCommandBaseTestSupport {
+
+    @TempDir
+    Path work;
+
+    @TempDir
+    Path repo;
+
+    @TempDir
+    Path lib;
+
+    @Test
+    void copiesTheJarsOfTheDependencies() throws Exception {
+        assertThat(command(resolver(null)).doCall()).isZero();
+
+        assertThat(lib.resolve("camel-main-4.11.0.jar")).exists();
+        assertThat(jars()).hasSizeGreaterThan(1).allMatch(name -> 
name.endsWith("-4.11.0.jar"));
+    }
+
+    @Test
+    void replacesTheJarsOfAnEarlierCopyAndKeepsTheOtherFiles() throws 
Exception {
+        Files.writeString(lib.resolve("camel-main-4.10.0.jar"), "older");
+        Files.writeString(lib.resolve("README.txt"), "my notes");
+        Files.createDirectories(lib.resolve("config"));
+
+        assertThat(command(resolver(null)).doCall()).isZero();
+
+        assertThat(lib.resolve("camel-main-4.10.0.jar")).doesNotExist();
+        assertThat(lib.resolve("camel-main-4.11.0.jar")).exists();
+        assertThat(lib.resolve("README.txt")).hasContent("my notes");
+        assertThat(lib.resolve("config")).isDirectory();
+    }
+
+    @Test
+    void anArtifactThatCannotBeResolvedIsAFailure() throws Exception {
+        
assertThat(command(resolver("camel-main")).doCall()).isEqualTo(CliExitCode.FAILURE);
+
+        assertThat(printer.getStderr())
+                .contains("Error resolving the artifact: 
org.apache.camel:camel-main:4.11.0 due to: not in the repository");
+        // the other dependencies are copied
+        
assertThat(jars()).isNotEmpty().doesNotContain("camel-main-4.11.0.jar");
+    }
+
+    @Test
+    void outputDirectoryThatIsAFileIsAUsageError() throws Exception {
+        Path file = Files.writeString(work.resolve("lib"), "not a directory");
+        DependencyCopy command = command(resolver(null));
+        command.outputDirectory = file.toString();
+
+        assertThatThrownBy(command::doCall)
+                .isInstanceOfSatisfying(CliException.class,
+                        e -> 
assertThat(e.getExitCode()).isEqualTo(CliExitCode.USAGE))
+                .hasMessage("--output-directory must be a directory, was: " + 
file);
+    }
+
+    @Test
+    void theOutputDirectoryIsLibByDefault() {
+        DependencyCopy command = new DependencyCopy(new 
CamelJBangMain().withPrinter(printer));
+
+        CommandLine.populateCommand(command);
+
+        assertThat(command.outputDirectory).isEqualTo("lib");
+    }
+
+    private DependencyCopy command(MavenDownloader downloader) {
+        DependencyCopy command = new DependencyCopy(new 
CamelJBangMain().withPrinter(printer));
+        CommandLine.populateCommand(command, "--gav=examples:route:1.0.0", 
"--dir=" + work, "--quiet",
+                "--runtime=camel-main", "--camel-version=4.11.0", 
"--quarkus-version=3.22.2",
+                CamelCommandBaseTestSupport.quarkusExtRegistry(), 
"--output-directory=" + lib);
+        command.files = List.of("classpath:route.yaml");
+        command.mavenResolver = new MavenResolverMixin() {
+            @Override
+            public MavenDownloader downloader() {
+                return downloader;
+            }
+        };
+        return command;
+    }
+
+    // resolves each dependency to a JAR of its own, except the one with the 
artifact id that is missing (the export
+    // before the copy resolves the rest as usual)
+    private MavenDownloader resolver(String missing) throws Exception {
+        MavenDownloader downloader = spy(new 
MavenResolverMixin().downloader());
+        doAnswer(inv -> {
+            List<String> gavs = inv.getArgument(0);
+            MavenGav gav = MavenGav.parseGav(gavs.get(0));
+            if (gav.getArtifactId().equals(missing)) {
+                throw new MavenResolutionException("not in the repository");
+            }
+            Path jar = repo.resolve(gav.getArtifactId() + "-" + 
gav.getVersion() + ".jar");
+            if (!Files.exists(jar)) {
+                Files.writeString(jar, gav.toString());
+            }
+            return List.of(new MavenArtifact(gav, jar.toFile()));
+        }).when(downloader).resolveArtifacts(anyList(), anySet(), 
anyBoolean(), anyBoolean());
+        return downloader;
+    }
+
+    private List<String> jars() throws Exception {
+        try (Stream<Path> files = Files.list(lib)) {
+            return files.map(f -> f.getFileName().toString()).filter(name -> 
name.endsWith(".jar")).toList();
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/InitTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/InitTest.java
index a7a1e8cf756f..5ae9694efb97 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/InitTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/InitTest.java
@@ -125,6 +125,117 @@ class InitTest {
         Files.delete(f);
     }
 
+    @Test
+    void initJavaInTheSourceFolderHasNoPackage() throws Exception {
+        Path sourceFolder = 
Files.createDirectories(workingDir.resolve("src/main/java"));
+
+        Init initCommand = new Init(new CamelJBangMain());
+        CommandLine.populateCommand(initCommand, "MyRoute.java", "--dir=" + 
sourceFolder);
+
+        int exit = initCommand.doCall();
+
+        assertEquals(0, exit);
+        List<String> lines = 
Files.readAllLines(sourceFolder.resolve("MyRoute.java"));
+        assertEquals("import org.apache.camel.builder.RouteBuilder;", 
lines.get(0));
+    }
+
+    @Test
+    void initJavaInAFolderThatOnlyStartsLikeTheSourceFolderHasNoPackage() 
throws Exception {
+        Path folder = 
Files.createDirectories(workingDir.resolve("src/main/javafx/demo"));
+
+        Init initCommand = new Init(new CamelJBangMain());
+        CommandLine.populateCommand(initCommand, "MyRoute.java", "--dir=" + 
folder);
+
+        int exit = initCommand.doCall();
+
+        assertEquals(0, exit);
+        List<String> lines = 
Files.readAllLines(folder.resolve("MyRoute.java"));
+        assertEquals("import org.apache.camel.builder.RouteBuilder;", 
lines.get(0));
+    }
+
+    @Test
+    void initXml() throws Exception {
+        Init initCommand = new Init(new CamelJBangMain());
+        CommandLine.populateCommand(initCommand, "my.camel.xml", "--dir=" + 
workingDir);
+
+        int exit = initCommand.doCall();
+
+        assertEquals(0, exit);
+        String content = Files.readString(workingDir.resolve("my.camel.xml"));
+        assertTrue(content.contains("<route"), content);
+    }
+
+    @Test
+    void initKamelets() throws Exception {
+        for (String name : List.of("my-source.kamelet.yaml", 
"my-sink.kamelet.yaml", "my-action.kamelet.yaml")) {
+            Init initCommand = new Init(new CamelJBangMain());
+            CommandLine.populateCommand(initCommand, name, "--dir=" + 
workingDir);
+
+            int exit = initCommand.doCall();
+
+            assertEquals(0, exit);
+            String content = Files.readString(workingDir.resolve(name));
+            assertTrue(content.contains("kind: Kamelet"), content);
+            // named after the file
+            assertTrue(content.contains("name: " + name.substring(0, 
name.indexOf('.'))), content);
+        }
+        
assertTrue(Files.readString(workingDir.resolve("my-source.kamelet.yaml"))
+                .contains("camel.apache.org/kamelet.type: \"source\""));
+        assertTrue(Files.readString(workingDir.resolve("my-sink.kamelet.yaml"))
+                .contains("camel.apache.org/kamelet.type: \"sink\""));
+    }
+
+    @Test
+    void initPipe() throws Exception {
+        Init initCommand = new Init(new CamelJBangMain());
+        CommandLine.populateCommand(initCommand, "my.yaml", "--pipe", "--dir=" 
+ workingDir);
+
+        int exit = initCommand.doCall();
+
+        assertEquals(0, exit);
+        
assertTrue(Files.readString(workingDir.resolve("my.yaml")).contains("kind: 
Pipe"));
+    }
+
+    @Test
+    void initCleansTheDirectoryFirst() throws Exception {
+        Path dir = Files.createDirectories(workingDir.resolve("project"));
+        Path old = Files.writeString(dir.resolve("old.yaml"), "- route: {}");
+
+        Init initCommand = new Init(new CamelJBangMain());
+        CommandLine.populateCommand(initCommand, "my.camel.yaml", "--dir=" + 
dir, "--clean-dir");
+
+        int exit = initCommand.doCall();
+
+        assertEquals(0, exit);
+        assertFalse(Files.exists(old), "the directory is cleaned first");
+        assertTrue(Files.exists(dir.resolve("my.camel.yaml")));
+    }
+
+    @Test
+    void initUnsupportedFileTypeIsAUsageError() throws Exception {
+        StringPrinter printer = new StringPrinter();
+        Init initCommand = new Init(new CamelJBangMain().withPrinter(printer));
+        CommandLine.populateCommand(initCommand, "notes.txt", "--dir=" + 
workingDir);
+
+        int exit = initCommand.doCall();
+
+        assertEquals(2, exit);
+        assertTrue(printer.getStderr().contains("Unsupported file type: txt"), 
printer.getStderr());
+        assertFalse(Files.exists(workingDir.resolve("notes.txt")));
+    }
+
+    @Test
+    void initFromKameletNeedsAKameletFile() throws Exception {
+        StringPrinter printer = new StringPrinter();
+        Init initCommand = new Init(new CamelJBangMain().withPrinter(printer));
+        CommandLine.populateCommand(initCommand, "my.yaml", 
"--from-kamelet=timer-source", "--dir=" + workingDir);
+
+        int exit = initCommand.doCall();
+
+        assertEquals(2, exit);
+        assertTrue(printer.getStderr().contains("file must have extension 
.kamelet.yaml"), printer.getStderr());
+    }
+
     @Test
     void initListTemplates() throws Exception {
         StringPrinter printer = new StringPrinter();
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDocTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDocTest.java
index 209321c9260d..30e04440c94a 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDocTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDocTest.java
@@ -21,6 +21,7 @@ import 
org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.commands.MavenResolverMixin;
 import org.apache.camel.dsl.jbang.core.common.CliException;
 import org.apache.camel.dsl.jbang.core.common.CliExitCode;
+import org.apache.camel.dsl.jbang.core.common.StringPrinter;
 import org.apache.camel.util.json.JsonObject;
 import org.apache.camel.util.json.Jsoner;
 import org.junit.jupiter.api.Assertions;
@@ -116,6 +117,103 @@ class CatalogDocTest extends CamelCommandBaseTestSupport {
         Assertions.assertEquals("timer", doc.getString("name"));
     }
 
+    @Test
+    public void shouldDocumentAnEip() throws Exception {
+        CatalogDoc command = createCommand("split", false);
+
+        int exit = command.doCall();
+
+        Assertions.assertEquals(0, exit);
+        String output = printer.getOutput();
+        Assertions.assertTrue(output.startsWith("EIP Name: split"), output);
+        Assertions.assertTrue(output.contains("The split EIP supports (total: 
"), output);
+        Assertions.assertTrue(output.contains("aggregationStrategy"), output);
+        Assertions.assertTrue(output.contains("/eips/split-eip.html"), output);
+    }
+
+    @Test
+    public void shouldDocumentTheEipWithTheNameOfAComponentWithThePrefix() 
throws Exception {
+        CatalogDoc command = createCommand("log", false);
+        Assertions.assertEquals(0, command.doCall());
+        Assertions.assertTrue(printer.getOutput().startsWith("Component Name: 
log"), printer.getOutput());
+
+        printer = new StringPrinter();
+        command = createCommand("eip:log", false);
+        Assertions.assertEquals(0, command.doCall());
+        Assertions.assertTrue(printer.getOutput().startsWith("EIP Name: log"), 
printer.getOutput());
+    }
+
+    @Test
+    public void shouldPrintTheLinkOfAnEip() throws Exception {
+        CatalogDoc command = createCommand("split", false);
+        command.url = true;
+
+        Assertions.assertEquals(0, command.doCall());
+
+        String output = printer.getOutput();
+        
Assertions.assertTrue(output.matches("https://camel\\.apache\\.org/components/[^/]+/eips/split-eip\\.html";),
+                output);
+    }
+
+    @Test
+    public void shouldSuggestASimilarEip() throws Exception {
+        CatalogDoc command = createCommand("eip:spllit", false);
+
+        CliException e = Assertions.assertThrows(CliException.class, 
command::doCall);
+
+        Assertions.assertEquals(CliExitCode.NOT_FOUND, e.getExitCode());
+        Assertions.assertTrue(e.getMessage().startsWith("Camel eip: spllit not 
found. Did you mean? split"),
+                e.getMessage());
+    }
+
+    @Test
+    public void shouldPrintEipDocAsJson() throws Exception {
+        CatalogDoc command = createCommand("split", false);
+        command.jsonOutput = true;
+
+        Assertions.assertEquals(0, command.doCall());
+
+        JsonObject doc = Jsoner.deserialize(printer.getOutput(), (JsonObject) 
null);
+        Assertions.assertNotNull(doc, printer.getOutput());
+        Assertions.assertEquals("eip", doc.getString("kind"));
+    }
+
+    @Test
+    public void shouldDocumentADataFormatAndALanguage() throws Exception {
+        CatalogDoc command = createCommand("jackson", false);
+        Assertions.assertEquals(0, command.doCall());
+        Assertions.assertTrue(printer.getOutput().startsWith("Dataformat Name: 
jackson"), printer.getOutput());
+        
Assertions.assertTrue(printer.getOutput().contains("<artifactId>camel-jackson</artifactId>"),
+                printer.getOutput());
+
+        printer = new StringPrinter();
+        command = createCommand("language:simple", false);
+        Assertions.assertEquals(0, command.doCall());
+        Assertions.assertTrue(printer.getOutput().startsWith("Language Name: 
simple"), printer.getOutput());
+    }
+
+    @Test
+    public void shouldFilterTheOptions() throws Exception {
+        CatalogDoc command = createCommand("timer", false);
+        command.filter = "delay";
+
+        Assertions.assertEquals(0, command.doCall());
+
+        String output = printer.getOutput();
+        Assertions.assertTrue(output.contains("match-filter: "), output);
+        Assertions.assertTrue(output.contains("delay"), output);
+        Assertions.assertFalse(output.contains("repeatCount"), output);
+    }
+
+    @Test
+    public void shouldDocumentCamelMain() throws Exception {
+        CatalogDoc command = createCommand("main", false);
+
+        Assertions.assertEquals(0, command.doCall());
+
+        Assertions.assertTrue(printer.getOutput().startsWith("Name: main"), 
printer.getOutput());
+    }
+
     private CatalogDoc createCommand(String name, boolean example) {
         CatalogDoc command = new CatalogDoc(new 
CamelJBangMain().withPrinter(printer));
         command.name = name;
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListErrorTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListErrorTest.java
index c17661f43cb5..7cee8e177fa3 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListErrorTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListErrorTest.java
@@ -18,116 +18,237 @@ package org.apache.camel.dsl.jbang.core.commands.process;
 
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.Collections;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
 import java.util.stream.Stream;
 
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.MockedStatic;
 import org.mockito.junit.jupiter.MockitoExtension;
+import picocli.CommandLine;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.Mockito.mockStatic;
 
+/**
+ * camel get error: the errors the integrations recorded, filtered, and with 
their details.
+ */
 @ExtendWith(MockitoExtension.class)
 class ListErrorTest extends ProcessCommandTestSupport {
 
+    private static final long OTHER_PID = 23456L;
+
     @Test
     void testEmptyOutputWhenNoErrors() throws Exception {
-        JsonObject status = buildContextStatus("myApp", 5);
-        writeStatusFile(TEST_PID, status);
+        writeStatusFile(TEST_PID, buildContextStatus("myApp", 5));
         // no error file written
 
-        ListError command = new ListError(new 
CamelJBangMain().withPrinter(printer));
-        command.sort = "pid";
+        assertThat(call(command())).isZero();
 
-        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
-            ProcessHandle ph = mockProcessHandle(TEST_PID);
-            ProcessHandle currentHandle = mockCurrentHandle();
-            mocked.when(ProcessHandle::current).thenReturn(currentHandle);
-            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph));
+        assertThat(printer.getOutput()).isEmpty();
+    }
 
-            int exit = command.doCall();
+    @Test
+    void testShowsErrorRouteId() throws Exception {
+        writeStatusFile(TEST_PID, buildContextStatus("myApp", 5));
+        writeErrorFile(TEST_PID, errorEntry("myRoute", "abc-123", false, 0));
 
-            assertEquals(0, exit);
-            assertEquals("", printer.getOutput().trim());
-        }
+        assertThat(call(command())).isZero();
+
+        assertThat(printer.getOutput()).contains("myApp", "myRoute", 
"abc-123", "RuntimeException", "Test error");
     }
 
     @Test
-    void testShowsErrorRouteId() throws Exception {
-        JsonObject status = buildContextStatus("myApp", 5);
-        writeStatusFile(TEST_PID, status);
-        writeErrorFile(errorEntry());
+    void testJsonOutput() throws Exception {
+        writeStatusFile(TEST_PID, buildContextStatus("myApp", 5));
+        writeErrorFile(TEST_PID, errorEntry("myRoute", "abc-123", false, 0));
 
-        ListError command = new ListError(new 
CamelJBangMain().withPrinter(printer));
-        command.sort = "pid";
+        assertThat(call(command("--json"))).isZero();
 
-        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
-            ProcessHandle ph = mockProcessHandle(TEST_PID);
-            ProcessHandle currentHandle = mockCurrentHandle();
-            mocked.when(ProcessHandle::current).thenReturn(currentHandle);
-            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph));
+        JsonArray errors = Jsoner.deserialize(printer.getOutput(), (JsonArray) 
null);
+        assertThat(errors).hasSize(1);
+        JsonObject error = errors.getMap(0);
+        assertThat(error.getString("routeId")).isEqualTo("myRoute");
+    }
 
-            int exit = command.doCall();
+    @Test
+    void jsonWithoutErrorsIsAnEmptyArray() throws Exception {
+        writeStatusFile(TEST_PID, buildContextStatus("myApp", 5));
 
-            assertEquals(0, exit);
-            String output = printer.getOutput();
-            assertTrue(output.contains("myRoute"), "Should show routeId");
-        }
+        assertThat(call(command("--json"))).isZero();
+
+        assertThat(printer.getOutput()).isEqualTo("[]");
     }
 
     @Test
-    void testJsonOutput() throws Exception {
-        JsonObject status = buildContextStatus("myApp", 5);
-        writeStatusFile(TEST_PID, status);
-        writeErrorFile(errorEntry());
+    void filterByRoute() throws Exception {
+        writeTwoErrors();
+
+        assertThat(call(command("--route=orders"))).isZero();
+
+        
assertThat(printer.getOutput()).contains("ex-1").doesNotContain("ex-2");
+    }
+
+    @Test
+    void filterByHandledInAnyCase() throws Exception {
+        writeTwoErrors();
+
+        assertThat(call(command("--handled=FALSE"))).isZero();
+
+        
assertThat(printer.getOutput()).contains("ex-2").doesNotContain("ex-1");
+    }
 
+    @Test
+    void filterByExchangeAndException() throws Exception {
+        writeTwoErrors();
+
+        assertThat(call(command("--id=ex-1", "--exception=runtime"))).isZero();
+
+        
assertThat(printer.getOutput()).contains("ex-1").doesNotContain("ex-2");
+    }
+
+    @Test
+    void agoShowsTheRecentErrors() throws Exception {
+        writeStatusFile(TEST_PID, buildContextStatus("myApp", 5));
+        writeErrorFile(TEST_PID, errorEntry("orders", "ex-1", false, 5), 
errorEntry("orders", "ex-2", false, 600));
+
+        assertThat(call(command("--ago=1m"))).isZero();
+
+        
assertThat(printer.getOutput()).contains("ex-1").doesNotContain("ex-2");
+    }
+
+    @Test
+    void handledThatIsNotABooleanIsAUsageError() {
+        // before the integrations are read
+        ListError command = command("--handled=yes");
+
+        assertThatThrownBy(command::doCall)
+                .isInstanceOfSatisfying(CliException.class,
+                        e -> 
assertThat(e.getExitCode()).isEqualTo(CliExitCode.USAGE))
+                .hasMessage("--handled must be true or false, was: yes");
+    }
+
+    @Test
+    void agoThatIsNotADurationIsAUsageError() {
+        ListError command = command("--ago=recently");
+
+        assertThatThrownBy(command::doCall)
+                .isInstanceOfSatisfying(CliException.class,
+                        e -> 
assertThat(e.getExitCode()).isEqualTo(CliExitCode.USAGE))
+                .hasMessage("--ago must be a duration such as 60s, 5m or 1h, 
was: recently");
+    }
+
+    @Test
+    void limitShowsTheFirstErrors() throws Exception {
+        writeStatusFile(TEST_PID, buildContextStatus("myApp", 5));
+        writeErrorFile(TEST_PID, errorEntry("orders", "ex-1", false, 0), 
errorEntry("orders", "ex-2", false, 10));
+
+        assertThat(call(command("--limit=1"))).isZero();
+
+        
assertThat(printer.getOutput()).contains("ex-1").doesNotContain("ex-2");
+    }
+
+    @Test
+    void lastShowsTheNewestErrorOfAnyIntegration() throws Exception {
+        writeStatusFile(TEST_PID, buildContextStatus("orders", 5));
+        writeStatusFile(OTHER_PID, buildContextStatus("payments", 5));
+        writeErrorFile(TEST_PID, errorEntry("orders", "ex-old", false, 60));
+        writeErrorFile(OTHER_PID, errorEntry("payments", "ex-new", false, 1));
+
+        assertThat(call(command("--last"))).isZero();
+
+        // with the details of the error
+        assertThat(printer.getOutput()).contains("ex-new", "Hello", "History", 
"payments[log1]")
+                .doesNotContain("ex-old");
+    }
+
+    @Test
+    void showTheBodyAndHistoryOfTheErrors() throws Exception {
+        writeStatusFile(TEST_PID, buildContextStatus("myApp", 5));
+        writeErrorFile(TEST_PID, errorEntry("orders", "ex-1", false, 0));
+
+        assertThat(call(command("--show=body,history"))).isZero();
+
+        assertThat(printer.getOutput()).contains("Hello", "History", 
"orders[log1]");
+    }
+
+    @Test
+    void diagramWithoutMessageHistoryIsAnError() throws Exception {
+        writeStatusFile(TEST_PID, buildContextStatus("myApp", 5));
+        JsonObject error = errorEntry("orders", "ex-1", false, 0);
+        error.remove("messageHistory");
+        writeErrorFile(TEST_PID, error);
+
+        assertThat(call(command("--diagram"))).isEqualTo(CliExitCode.FAILURE);
+
+        assertThat(printer.getStderr()).contains("No message history available 
for this error");
+    }
+
+    @Test
+    void unknownSortIsAUsageError() {
+        ListError command = command("--sort=size");
+
+        assertThatThrownBy(command::doCall)
+                .isInstanceOfSatisfying(CliException.class,
+                        e -> 
assertThat(e.getExitCode()).isEqualTo(CliExitCode.USAGE));
+    }
+
+    private ListError command(String... args) {
         ListError command = new ListError(new 
CamelJBangMain().withPrinter(printer));
-        command.sort = "pid";
-        command.jsonOutput = true;
+        CommandLine.populateCommand(command, 
Stream.concat(Stream.of("--logging-color=false"), Arrays.stream(args))
+                .toArray(String[]::new));
+        return command;
+    }
 
+    // runs the command with the integrations of the status files of the test
+    private static int call(ListError command) throws Exception {
         try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
-            ProcessHandle ph = mockProcessHandle(TEST_PID);
+            List<ProcessHandle> handles = Stream.of(TEST_PID, OTHER_PID)
+                    .filter(pid -> 
Files.exists(CommandLineHelper.getCamelDir().resolve(pid + "-status.json")))
+                    
.map(ProcessCommandTestSupport::mockProcessHandle).toList();
             ProcessHandle currentHandle = mockCurrentHandle();
             mocked.when(ProcessHandle::current).thenReturn(currentHandle);
-            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph));
-
-            int exit = command.doCall();
-
-            assertEquals(0, exit);
-            String output = printer.getOutput();
-            assertTrue(output.startsWith("["), "JSON output should be array");
-            assertTrue(output.contains("myRoute"));
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
handles.stream());
+            return command.doCall();
         }
     }
 
-    private static void writeErrorFile(JsonObject... errors) throws Exception {
-        JsonArray arr = new JsonArray();
-        Collections.addAll(arr, errors);
+    private static void writeTwoErrors() throws Exception {
+        writeStatusFile(TEST_PID, buildContextStatus("myApp", 5));
+        // newest first, as the error registry lists them
+        writeErrorFile(TEST_PID, errorEntry("orders", "ex-1", true, 0), 
errorEntry("payments", "ex-2", false, 10));
+    }
+
+    private static void writeErrorFile(long pid, JsonObject... errors) throws 
Exception {
         JsonObject root = new JsonObject();
-        root.put("errors", arr);
-        Path f = 
CommandLineHelper.getCamelDir().resolve(ProcessCommandTestSupport.TEST_PID + 
"-error.json");
+        root.put("errors", new JsonArray(Arrays.asList(errors)));
+        Path f = CommandLineHelper.getCamelDir().resolve(pid + "-error.json");
         Files.writeString(f, root.toJson());
     }
 
-    private static JsonObject errorEntry() {
-        JsonObject ex = new JsonObject();
-        ex.put("type", "java.lang.RuntimeException");
-        ex.put("message", "Test error");
-
+    private static JsonObject errorEntry(String routeId, String exchangeId, 
boolean handled, long secondsAgo) {
+        JsonObject body = new JsonObject();
+        body.put("type", "java.lang.String");
+        body.put("value", "Hello");
         JsonObject e = new JsonObject();
-        e.put("routeId", "myRoute");
-        e.put("nodeId", "myNode");
-        e.put("exchangeId", "abc-123");
-        e.put("handled", false);
-        e.put("timestamp", System.currentTimeMillis());
-        e.put("exception", ex);
+        e.put("routeId", routeId);
+        e.put("nodeId", "log1");
+        e.put("exchangeId", exchangeId);
+        e.put("handled", handled);
+        e.put("timestamp", System.currentTimeMillis() - secondsAgo * 1000);
+        e.put("exception", new JsonObject(Map.of("type", 
"java.lang.RuntimeException", "message", "Test error")));
+        e.put("message", new JsonObject(Map.of("body", body)));
+        e.put("messageHistory", new JsonArray(List.of(routeId + "[from1]", 
routeId + "[log1]")));
         return e;
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/update/UpdateRunTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/update/UpdateRunTest.java
new file mode 100644
index 000000000000..aa1cfc553788
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/update/UpdateRunTest.java
@@ -0,0 +1,269 @@
+/*
+ * 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.camel.dsl.jbang.core.commands.update;
+
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.jar.JarOutputStream;
+import java.util.zip.ZipEntry;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelCommandBaseTestSupport;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
+import org.apache.camel.main.download.MavenDependencyDownloader;
+import org.apache.camel.tooling.maven.MavenArtifact;
+import org.apache.camel.tooling.maven.MavenGav;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * camel update run: the Maven command that updates a project with the Camel 
upgrade recipes (Camel Main and Spring
+ * Boot) or the Quarkus Maven plugin (Quarkus). The command is not run, and 
nothing is downloaded.
+ */
+class UpdateRunTest extends CamelCommandBaseTestSupport {
+
+    @TempDir
+    Path project;
+
+    @TempDir
+    Path repo;
+
+    // what the fake downloader has, by artifact id
+    private final Map<String, Path> artifacts = new HashMap<>();
+    // the versions of the Camel Quarkus catalog: the Camel version and the 
Camel Quarkus version
+    private final List<String[]> quarkusVersions = new ArrayList<>();
+    // the command the update ran
+    private List<String> command;
+
+    @BeforeEach
+    void pom() throws Exception {
+        Files.writeString(project.resolve("pom.xml"), "<project/>");
+    }
+
+    @Test
+    void withoutAMavenProjectIsNotFound() throws Exception {
+        Files.delete(project.resolve("pom.xml"));
+
+        
assertThat(command("4.18.0").doCall()).isEqualTo(CliExitCode.NOT_FOUND);
+
+        assertThat(printer.getStderr()).contains("No Maven project found in 
the current directory");
+        assertThat(command).isNull();
+    }
+
+    @Test
+    void camelMainRunsTheRewritePluginWithTheLatestRecipe() throws Exception {
+        recipes("camel-upgrade-recipes", "4.4.yaml", 
"org.apache.camel.upgrade.camel44.CamelMigrationRecipe",
+                "latest.yaml", 
"org.apache.camel.upgrade.CamelMigrationRecipe");
+
+        assertThat(command("4.18.0").doCall()).isZero();
+
+        assertThat(command).contains("--no-transfer-progress",
+                "org.openrewrite.maven:rewrite-maven-plugin:6.0.4:run",
+                
"-Drewrite.recipeArtifactCoordinates=org.apache.camel.upgrade:camel-upgrade-recipes:4.18.0",
+                
"-Drewrite.activeRecipes=org.apache.camel.upgrade.CamelMigrationRecipe");
+    }
+
+    @Test
+    void everyRecipeWithoutALatestOne() throws Exception {
+        recipes("camel-upgrade-recipes", "4.4.yaml", 
"org.apache.camel.upgrade.camel44.CamelMigrationRecipe");
+
+        assertThat(command("4.4.0").doCall()).isZero();
+
+        
assertThat(command).contains("-Drewrite.activeRecipes=org.apache.camel.upgrade.camel44.CamelMigrationRecipe");
+    }
+
+    @Test
+    void dryRunWithDebugAndExtraRecipes() throws Exception {
+        recipes("camel-upgrade-recipes", "latest.yaml", 
"org.apache.camel.upgrade.CamelMigrationRecipe");
+
+        assertThat(command("4.18.0", "--dryRun", "--debug", 
"--openRewriteVersion=6.1.0",
+                "--extraActiveRecipes=org.example.MyRecipe", 
"--extraRecipeArtifactCoordinates=org.example:recipes:1.0")
+                .doCall()).isZero();
+
+        assertThat(command).contains("-X", 
"org.openrewrite.maven:rewrite-maven-plugin:6.1.0:dryRun",
+                
"-Drewrite.recipeArtifactCoordinates=org.apache.camel.upgrade:camel-upgrade-recipes:4.18.0,"
+                                                                               
                       + "org.example:recipes:1.0",
+                
"-Drewrite.activeRecipes=org.apache.camel.upgrade.CamelMigrationRecipe,org.example.MyRecipe");
+    }
+
+    @Test
+    void springBootUsesItsRecipes() throws Exception {
+        recipes("camel-spring-boot-upgrade-recipes", "latest.yaml",
+                "org.apache.camel.upgrade.CamelSpringBootMigrationRecipe");
+
+        assertThat(command("4.18.0", 
"--runtime=spring-boot").doCall()).isZero();
+
+        assertThat(command).contains(
+                
"-Drewrite.recipeArtifactCoordinates=org.apache.camel.upgrade:camel-spring-boot-upgrade-recipes:4.18.0",
+                
"-Drewrite.activeRecipes=org.apache.camel.upgrade.CamelSpringBootMigrationRecipe");
+    }
+
+    @Test
+    void versionWithoutRecipesIsAFailure() throws Exception {
+        assertThat(command("9.9.9").doCall()).isEqualTo(CliExitCode.FAILURE);
+
+        assertThat(printer.getStderr())
+                .contains("Cannot find the Camel upgrade recipes 
org.apache.camel.upgrade:camel-upgrade-recipes:9.9.9");
+        assertThat(command).isNull();
+    }
+
+    @Test
+    void quarkusRunsTheQuarkusUpdateOfTheStreamOfTheCamelVersion() throws 
Exception {
+        quarkusVersions.add(new String[] { "4.14.0", "3.27.1" });
+        quarkusVersions.add(new String[] { "4.18.0", "3.33.2" });
+
+        assertThat(command("4.18.0", "--runtime=quarkus", 
"--quarkusMavenPluginVersion=3.33.2").doCall()).isZero();
+
+        
assertThat(command).contains("io.quarkus.platform:quarkus-maven-plugin:3.33.2:update",
 "-Dstream=3.33",
+                "-DrewriteFullRun");
+    }
+
+    @Test
+    void quarkusDryRun() throws Exception {
+        quarkusVersions.add(new String[] { "4.18.0", "3.33.2" });
+
+        assertThat(command("4.18.0", "--runtime=quarkus", 
"--quarkusMavenPluginVersion=3.33.2", "--dryRun").doCall())
+                .isZero();
+
+        assertThat(command).contains("-DrewriteDryRun");
+    }
+
+    @Test
+    void quarkusWithoutTheCamelVersionIsAFailure() throws Exception {
+        quarkusVersions.add(new String[] { "4.14.0", "3.27.1" });
+
+        assertThat(command("4.18.0", "--runtime=quarkus", 
"--quarkusMavenPluginVersion=3.33.2").doCall())
+                .isEqualTo(CliExitCode.FAILURE);
+
+        assertThat(printer.getStderr()).contains("Cannot determine Quarkus 
stream version for Camel version 4.18.0");
+    }
+
+    @Test
+    void quarkusWithoutDownloadNeedsThePluginVersion() {
+        UpdateRun command = command("4.18.0", "--runtime=quarkus", 
"--download=false");
+
+        assertThatThrownBy(command::doCall)
+                .isInstanceOfSatisfying(CliException.class,
+                        e -> 
assertThat(e.getExitCode()).isEqualTo(CliExitCode.USAGE))
+                .hasMessageContaining("--quarkusMavenPluginVersion is required 
with --download=false");
+    }
+
+    @Test
+    void theExitCodeOfMaven() throws Exception {
+        recipes("camel-upgrade-recipes", "latest.yaml", 
"org.apache.camel.upgrade.CamelMigrationRecipe");
+        UpdateRun command = new UpdateRun(new 
CamelJBangMain().withPrinter(printer)) {
+            @Override
+            protected MavenDependencyDownloader createDownloader() {
+                return downloader();
+            }
+
+            @Override
+            protected int executeCommand(List<String> command) {
+                return 1;
+            }
+        };
+        CommandLine.populateCommand(command, "4.18.0");
+        command.projectDir = project;
+
+        assertThat(command.doCall()).isEqualTo(1);
+    }
+
+    private UpdateRun command(String... args) {
+        UpdateRun command = new UpdateRun(new 
CamelJBangMain().withPrinter(printer)) {
+            @Override
+            protected MavenDependencyDownloader createDownloader() {
+                return downloader();
+            }
+
+            @Override
+            protected int executeCommand(List<String> command) {
+                UpdateRunTest.this.command = command;
+                return 0;
+            }
+        };
+        CommandLine.populateCommand(command, args);
+        command.projectDir = project;
+        return command;
+    }
+
+    // a downloader with the recipes and versions of the test, and nothing else
+    private MavenDependencyDownloader downloader() {
+        return new MavenDependencyDownloader() {
+            @Override
+            public MavenArtifact downloadArtifact(String groupId, String 
artifactId, String version) {
+                Path jar = artifacts.get(artifactId);
+                return jar != null
+                        ? new MavenArtifact(
+                                MavenGav.parseGav(groupId + ":" + artifactId + 
":" + version),
+                                jar.toFile())
+                        : null;
+            }
+
+            @Override
+            public List<String[]> resolveAvailableVersions(
+                    String groupId, String artifactId, String minimumVersion, 
String repo) {
+                return quarkusVersions;
+            }
+
+            @Override
+            protected void doBuild() {
+            }
+
+            @Override
+            protected void doInit() {
+            }
+
+            @Override
+            protected void doStart() {
+            }
+
+            @Override
+            protected void doStop() {
+            }
+        };
+    }
+
+    // a jar of recipes: pairs of the file name and the name of the recipe in 
it
+    private void recipes(String artifactId, String... files) throws Exception {
+        Path jar = repo.resolve(artifactId + ".jar");
+        try (JarOutputStream out = new 
JarOutputStream(Files.newOutputStream(jar))) {
+            for (int i = 0; i < files.length; i += 2) {
+                out.putNextEntry(new ZipEntry("META-INF/rewrite/" + files[i]));
+                write(out, "---\ntype: 
specs.openrewrite.org/v1beta/recipe\nname: " + files[i + 1]
+                           + "\ndisplayName: Migrates Camel\n");
+                out.closeEntry();
+            }
+        }
+        artifacts.put(artifactId, jar);
+    }
+
+    private static void write(OutputStream out, String text) throws Exception {
+        out.write(text.getBytes(StandardCharsets.UTF_8));
+    }
+}

Reply via email to