You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/10/16 05:52:47 UTC

(camel) branch main updated: CAMEL-25399: camel-jbang - Remaining command exit code and error fixes (#27932)

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 7089992f4a97 CAMEL-25399: camel-jbang - Remaining command exit code and error fixes (#27932)
7089992f4a97 is described below

commit 7089992f4a977a51eac52f95e67c4f0c5f29edb7
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Fri Oct 16 07:52:39 2026 +0200

    CAMEL-25399: camel-jbang - Remaining command exit code and error fixes (#27932)
    
    - harden --url that cannot be reached exits 1, instead of sending the route
      to a local Ollama.
    - export does not clean the current directory, or one that contains it, as
      the export directory without --clean-dir: ./, $PWD and .. compared as
      text with "." and deleted the files of the project.
    - doc <name> looks in the catalog of Camel before it downloads the Kamelet
      catalog (works offline), catalog kamelet uses --repos, and --sort on a
      row without a description or support level no longer fails.
    - version get shows a configuration with only the kamelets version,
      version set --reset removes it too, and version list exits 1 when the
      versions cannot be downloaded.
    - sbom: an unknown --sbom-format or --sbom-output-format exits 2 before
      exporting (it exited 0 doing nothing), and Maven not finishing in time
      exits 124 with a message.
    - update run applies --upgradeTimeout (the output was read to the end
      before the wait) and exits 124 when it is reached.
    - debug does not run the jar of a Spring Boot or Quarkus build that failed.
    - validate yaml/properties with no file to validate exits 3.
    - kubernetes logs exits 3 when no pod shows up and 1 when reading fails,
      kubernetes run prints the end of the Maven output when the build or
      deployment fails (it was hidden without --verbose), and a pod that is not
      running after 10 minutes exits 124.
    - --jvm-args keeps a quoted value with spaces as one argument.
    
    camel kubernetes run removes the Maven log once Maven has ended.
    
    Co-authored-by: Claude Opus 5.5 (1M context) <no...@anthropic.com>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  21 ++++
 .../camel-jbang-catalog-kamelet.adoc               |   3 +
 .../META-INF/camel-jbang-commands-metadata.json    |   2 +-
 .../camel/dsl/jbang/core/commands/Debug.java       |  14 ++-
 .../dsl/jbang/core/commands/ExportBaseCommand.java |  10 ++
 .../dsl/jbang/core/commands/ExportCamelMain.java   |   2 +-
 .../dsl/jbang/core/commands/ExportQuarkus.java     |   2 +-
 .../dsl/jbang/core/commands/ExportSpringBoot.java  |   2 +-
 .../camel/dsl/jbang/core/commands/Harden.java      |   7 +-
 .../apache/camel/dsl/jbang/core/commands/Run.java  |   2 +-
 .../dsl/jbang/core/commands/SBOMGenerator.java     |  14 ++-
 .../core/commands/catalog/CatalogBaseCommand.java  |   9 +-
 .../jbang/core/commands/catalog/CatalogDoc.java    |  29 +++---
 .../core/commands/catalog/CatalogKamelet.java      |  36 ++-----
 .../dsl/jbang/core/commands/update/UpdateRun.java  |  35 +++----
 .../jbang/core/commands/version/VersionGet.java    |   2 +-
 .../jbang/core/commands/version/VersionList.java   |   5 +-
 .../jbang/core/commands/version/VersionSet.java    |   1 +
 .../dsl/jbang/core/common/CommandLineHelper.java   |  36 +++++++
 .../core/commands/CommandInputErrorsTest.java      |  30 ++++++
 .../core/commands/catalog/CatalogDocTest.java      |  12 +++
 .../core/commands/catalog/CatalogKameletTest.java  |  12 +++
 .../core/commands/version/VersionGetTest.java      |  15 +++
 .../core/commands/version/VersionSetTest.java      |   4 +-
 .../core/common/CommandLineArgumentsTest.java      |  50 ++++++++++
 .../commands/kubernetes/KubernetesPodLogs.java     |  18 +++-
 .../core/commands/kubernetes/KubernetesRun.java    | 107 +++++++++++++++------
 .../commands/kubernetes/KubernetesPodLogsTest.java |   7 +-
 .../validate/PropertiesValidateCommand.java        |   4 +
 .../commands/validate/YamlValidateCommand.java     |   4 +
 .../core/commands/validate/ValidateJsonTest.java   |  21 +++-
 31 files changed, 401 insertions(+), 115 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 5ee9c372ca85..51bc0359f6c2 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
@@ -425,6 +425,27 @@ or follows, like Enter does.
   answered at once), and `camel_control stop-routes` and `start-routes` answer with what was done instead of the
   answer of the integration.
 
+=== camel-jbang - exit codes and errors of more commands
+
+* `camel export` does not clean the export directory when it is the current directory, or one that contains it, unless
+  `--clean-dir` is given: `--dir=./`, `--dir=$PWD` and `--dir=..` deleted the files of the project, as only `--dir=.`
+  was recognized.
+* `camel harden --url` with an LLM that cannot be reached exits with `1`, instead of sending the route to a local
+  Ollama.
+* `camel sbom` with an unknown `--sbom-format` or `--sbom-output-format` exits with `2` (it exited with `0` without
+  doing anything), and exits with `124` when Maven does not finish in time.
+* `camel update run --upgradeTimeout` applies (the output was read to the end before the wait), and exits with `124`
+  when it is reached.
+* `camel version list` exits with `1` when the versions cannot be downloaded. `camel version set --reset` removes the
+  Kamelets version too, and `camel version get` shows a configuration with only the Kamelets version.
+* `camel validate yaml` and `camel validate properties` with no file to validate exit with `3`.
+* `camel doc` looks a name up in the catalog of Camel before it downloads the Kamelet catalog, so a component is
+  documented also offline. `camel catalog kamelet` has `--repos`.
+* `camel kubernetes logs` exits with `3` when no pod with the label shows up, and with `1` when reading the log fails.
+  `camel kubernetes run` prints the end of the Maven output when the build or deployment fails (it was hidden without
+  `--verbose`), and exits with `124` when the pod is not running after 10 minutes.
+* `--jvm-args` keeps a quoted value with spaces as one argument.
+
 === camel-jpa, camel-hazelcast - an exchange marked rollback only is not consumed
 
 When a route marks the exchange rollback only (for example with `markRollbackOnly()` or `markRollbackOnlyLast()`)
diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-catalog-kamelet.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-catalog-kamelet.adoc
index 838371b055c7..fd8a4208c305 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-catalog-kamelet.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-catalog-kamelet.adoc
@@ -19,8 +19,11 @@ camel catalog kamelet [options]
 [cols="2,5,1,2",options="header"]
 |===
 | Option | Description | Default | Type
+| `--download` | Whether to allow automatic downloading JAR dependencies (over the internet) | true | boolean
 | `--filter` | Filter by name or description |  | String
+| `--fresh` | Make sure we use fresh (i.e. non-cached) resources | false | boolean
 | `--kamelets-version` | Apache Camel Kamelets version (auto-detected from classpath if not set) |  | String
+| `--repo,--repos` | Additional maven repositories for download on-demand (Use commas to separate multiple repositories) |  | String
 | `--sort` | Sort by name, type, support-level, or description | name | String
 | `--type,--filter-type` | Filter by type: source, sink, or action |  | String
 | `-h,--help` | Display the help and sub-commands |  | boolean
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 76bd529af26a..22520bf0328c 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
@@ -2,7 +2,7 @@
   "commands": [
     { "name": "ask", "fullName": "ask", "description": "Ask a question about a running Camel application using AI", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Ask", "options": [ { "names": "--api-key", "description": "API key. 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', 'anthropic', or 'watsonx'", "javaType":  [...]
     { "name": "bind", "fullName": "bind", "description": "DEPRECATED: Bind source and sink Kamelets as a new Camel integration", "deprecated": true, "sourceClass": "org.apache.camel.dsl.jbang.core.commands.bind.Bind", "options": [ { "names": "--error-handler", "description": "Add error handler (none|log|sink:<endpoint>). Sink endpoints are expected in the format [[apigroup\/]version:]kind:[namespace\/]name, plain Camel URIs or Kamelet name.", "javaType": "java.lang.String", "type": "stri [...]
-    { "name": "catalog", "fullName": "catalog", "description": "List artifacts from Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "component", "fullName": "catalog component", "description": "List components from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...]
+    { "name": "catalog", "fullName": "catalog", "description": "List artifacts from Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "component", "fullName": "catalog component", "description": "List components from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...]
     { "name": "cmd", "fullName": "cmd", "description": "Performs commands in the running Camel integrations, such as start\/stop route, or change logging levels.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "browse", "fullName": "cmd browse", "description": "Browse pending messages on endpoints [...]
     { "name": "completion", "fullName": "completion", "description": "Generate completion script for bash\/zsh", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] },
     { "name": "config", "fullName": "config", "description": "Get and set user configuration values", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", "fullName": "config get", "description": "Display user configuration value", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...]
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java
index bee35db959b9..a15c7aa33557 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java
@@ -400,7 +400,12 @@ public class Debug extends Run {
             pb.inheritIO();
             Process p = pb.start();
 
-            if (p.waitFor(120, TimeUnit.SECONDS)) {
+            boolean built = p.waitFor(120, TimeUnit.SECONDS);
+            if (built && p.exitValue() != 0) {
+                printer().printErr("Building Camel Spring Boot for debugging failed (exit code " + p.exitValue() + ")");
+                return CliExitCode.FAILURE;
+            }
+            if (built) {
                 AtomicReference<Process> processRef = new AtomicReference<>();
                 Runtime.getRuntime().addShutdownHook(new Thread(() -> {
                     try {
@@ -526,7 +531,12 @@ public class Debug extends Run {
             pb.inheritIO();
             Process p = pb.start();
 
-            if (p.waitFor(120, TimeUnit.SECONDS)) {
+            boolean built = p.waitFor(120, TimeUnit.SECONDS);
+            if (built && p.exitValue() != 0) {
+                printer().printErr("Building Camel Quarkus for debugging failed (exit code " + p.exitValue() + ")");
+                return CliExitCode.FAILURE;
+            }
+            if (built) {
                 AtomicReference<Process> processRef = new AtomicReference<>();
                 Runtime.getRuntime().addShutdownHook(new Thread(() -> {
                     try {
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportBaseCommand.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportBaseCommand.java
index 7a3a7266cffc..637b941a8391 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportBaseCommand.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportBaseCommand.java
@@ -580,6 +580,16 @@ public abstract class ExportBaseCommand extends CamelCommand {
 
     protected abstract Integer export() throws Exception;
 
+    /**
+     * Whether the directory is the current directory, or one that contains it, such as <tt>./</tt>, <tt>$PWD</tt> or
+     * <tt>..</tt>: cleaning it before the export deletes the files of the project, so it is only cleaned with
+     * --clean-dir.
+     */
+    static boolean isCurrentDirOrParent(String dir) {
+        Path path = Path.of(dir).toAbsolutePath().normalize();
+        return Path.of("").toAbsolutePath().normalize().startsWith(path);
+    }
+
     protected Integer runSilently(boolean ignoreLoadingError, boolean lazyBean, boolean verbose) throws Exception {
         if (kameletsVersion == null) {
             kameletsVersion = VersionHelper.extractKameletsVersion();
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportCamelMain.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportCamelMain.java
index a9cf94801f7f..e6df6dd6ecc3 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportCamelMain.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportCamelMain.java
@@ -196,7 +196,7 @@ class ExportCamelMain extends Export {
         }
         String appJar = Paths.get("target", ids[1] + "-" + ids[2] + ".jar").toString();
         copyReadme(BUILD_DIR, appJar);
-        if (cleanExportDir || !exportDir.equals(".")) {
+        if (cleanExportDir || !isCurrentDirOrParent(exportDir)) {
             // cleaning current dir can be a bit dangerous so only clean if explicit enabled
             // otherwise always clean export-dir to avoid stale data
             if (cleanExportDir) {
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportQuarkus.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportQuarkus.java
index 26f0357e24b8..7e141adb1fa2 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportQuarkus.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportQuarkus.java
@@ -187,7 +187,7 @@ class ExportQuarkus extends Export {
             copyCamelWrapper();
         }
 
-        if (cleanExportDir || !exportDir.equals(".")) {
+        if (cleanExportDir || !isCurrentDirOrParent(exportDir)) {
             // cleaning current dir can be a bit dangerous so only clean if explicit enabled
             // otherwise always clean export-dir to avoid stale data
             if (cleanExportDir) {
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportSpringBoot.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportSpringBoot.java
index 305b020a6fea..0ba2377713be 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportSpringBoot.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportSpringBoot.java
@@ -193,7 +193,7 @@ class ExportSpringBoot extends Export {
         }
         String appJar = "target" + File.separator + ids[1] + "-" + ids[2] + ".jar";
         copyReadme(BUILD_DIR, appJar);
-        if (cleanExportDir || !exportDir.equals(".")) {
+        if (cleanExportDir || !isCurrentDirOrParent(exportDir)) {
             // cleaning current dir can be a bit dangerous so only clean if explicit enabled
             // otherwise always clean export-dir to avoid stale data
             if (cleanExportDir) {
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Harden.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Harden.java
index f21b16500bf7..7a27199e3bcb 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Harden.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Harden.java
@@ -257,8 +257,11 @@ public class Harden extends CamelCommand {
     }
 
     private String detectEndpoint() {
-        return tryExplicitUrl()
-                .or(this::tryInfraOllama)
+        if (url != null && !url.isBlank()) {
+            // the LLM asked for, or none: the route is not sent to another one
+            return tryExplicitUrl().orElse(null);
+        }
+        return tryInfraOllama()
                 .or(this::tryDefaultOllama)
                 .orElse(null);
     }
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
index d0220fcc274a..0a4f08332fdb 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
@@ -2010,7 +2010,7 @@ public class Run extends CamelCommand {
         javaCmd.add(Paths.get(System.getProperty("java.home"), "bin", "java").toString());
         String runJvmArgs = mergeJvmArgs(jvmArgs, buildJfrJvmArgs());
         if (runJvmArgs != null && !runJvmArgs.isBlank()) {
-            javaCmd.addAll(Arrays.asList(runJvmArgs.trim().split("\\s+")));
+            javaCmd.addAll(CommandLineHelper.splitArguments(runJvmArgs));
         }
         if (jvmDebugPort > 0) {
             javaCmd.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:" + jvmDebugPort);
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/SBOMGenerator.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/SBOMGenerator.java
index 60ba2e839883..e3d654b6ae1b 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/SBOMGenerator.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/SBOMGenerator.java
@@ -25,6 +25,8 @@ import java.util.Properties;
 import java.util.concurrent.TimeUnit;
 
 import org.apache.camel.dsl.jbang.core.common.*;
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
 import org.apache.camel.util.CamelCaseOrderedProperties;
 import org.apache.camel.util.FileUtil;
 import picocli.CommandLine;
@@ -79,6 +81,12 @@ public class SBOMGenerator extends Export {
 
     @Override
     public Integer doCall() throws Exception {
+        if (!CYCLONEDX_FORMAT.equalsIgnoreCase(sbomFormat) && !SPDX_FORMAT.equalsIgnoreCase(sbomFormat)) {
+            throw CliException.usage("Unknown SBOM format: " + sbomFormat + " (use cyclonedx or spdx)");
+        }
+        if (!SBOM_JSON_FORMAT.equalsIgnoreCase(sbomOutputFormat) && !SBOM_XML_FORMAT.equalsIgnoreCase(sbomOutputFormat)) {
+            throw CliException.usage("Unknown SBOM output format: " + sbomOutputFormat + " (use json or xml)");
+        }
         this.quiet = true; // be quiet and generate from fresh data to ensure the output is up-to-date
         return super.doCall();
     }
@@ -127,7 +135,8 @@ public class SBOMGenerator extends Export {
                 done = p.waitFor(60, TimeUnit.SECONDS);
                 if (!done) {
                     p.destroyForcibly();
-                    answer = 1;
+                    printer().printErr("Maven did not generate the SBOM within 60 seconds");
+                    answer = CliExitCode.TIMEOUT;
                 } else if (p.exitValue() != 0) {
                     answer = p.exitValue();
                 }
@@ -156,7 +165,8 @@ public class SBOMGenerator extends Export {
                 done = p.waitFor(60, TimeUnit.SECONDS);
                 if (!done) {
                     p.destroyForcibly();
-                    answer = 1;
+                    printer().printErr("Maven did not generate the SBOM within 60 seconds");
+                    answer = CliExitCode.TIMEOUT;
                 } else if (p.exitValue() != 0) {
                     answer = p.exitValue();
                 }
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogBaseCommand.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogBaseCommand.java
index d5419617a91a..87f7955da7f8 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogBaseCommand.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogBaseCommand.java
@@ -224,14 +224,19 @@ public abstract class CatalogBaseCommand extends CamelCommand {
                 return o1.name.compareToIgnoreCase(o2.name) * negate;
             case "level":
             case "support-level":
-                return o1.level.compareToIgnoreCase(o2.level) * negate;
+                return compareIgnoreCase(o1.level, o2.level) * negate;
             case "description":
-                return o1.description.compareToIgnoreCase(o2.description) * negate;
+                return compareIgnoreCase(o1.description, o2.description) * negate;
             default:
                 return 0;
         }
     }
 
+    // a missing value sorts first
+    static int compareIgnoreCase(String s1, String s2) {
+        return (s1 != null ? s1 : "").compareToIgnoreCase(s2 != null ? s2 : "");
+    }
+
     String shortGav(Row r) {
         // only output artifact id
         return MavenGav.parseGav(r.gav).getArtifactId();
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 9c27755bb3a7..4149fcd2aec8 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
@@ -179,13 +179,6 @@ public class CatalogDoc extends CamelCommand {
             }
         }
 
-        if (prefix == null || "kamelet".equals(prefix)) {
-            KameletModel km = KameletCatalogHelper.loadKameletModel(name, kameletsVersion, mavenResolver.repos());
-            if (km != null) {
-                docKamelet(km);
-                return 0;
-            }
-        }
         if (prefix == null || "component".equals(prefix)) {
             ComponentModel cm = catalog.componentModel(name);
             if (cm != null) {
@@ -218,6 +211,14 @@ public class CatalogDoc extends CamelCommand {
                 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());
+            if (km != null) {
+                docKamelet(km);
+                return 0;
+            }
+        }
 
         if (prefix == null) {
             // guess if a kamelet
@@ -267,13 +268,6 @@ public class CatalogDoc extends CamelCommand {
         if ("main".equals(name) && catalog.mainModel() != null) {
             return CatalogDocs.catalogDoc(catalog, name, null, null, filter, "all", false, false, null);
         }
-        if (prefix == null || "kamelet".equals(prefix)) {
-            // the Kamelets of the version and repositories asked for, not only the ones of the CLI
-            KameletModel km = KameletCatalogHelper.loadKameletModel(name, kameletsVersion, mavenResolver.repos());
-            if (km != null) {
-                return CatalogDocs.kameletDoc(KameletDefinitions.of(km, "the Kamelet catalog " + kameletsVersion));
-            }
-        }
         if (prefix == null || !"kamelet".equals(prefix) && !"other".equals(prefix)) {
             JsonObject doc = CatalogDocs.catalogDoc(catalog, name, null, prefix, filter, "all", headers, false, null);
             if (doc.get("error") == null) {
@@ -289,6 +283,13 @@ public class CatalogDoc extends CamelCommand {
                 return doc;
             }
         }
+        if (prefix == null || "kamelet".equals(prefix)) {
+            // the Kamelets of the version and repositories asked for, not only the ones of the CLI
+            KameletModel km = KameletCatalogHelper.loadKameletModel(name, kameletsVersion, mavenResolver.repos());
+            if (km != null) {
+                return CatalogDocs.kameletDoc(KameletDefinitions.of(km, "the Kamelet catalog " + kameletsVersion));
+            }
+        }
         return null;
     }
 
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKamelet.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKamelet.java
index 18cae77dd45c..057025461fb4 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKamelet.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKamelet.java
@@ -16,7 +16,6 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.catalog;
 
-import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -29,12 +28,10 @@ import com.github.freva.asciitable.Column;
 import com.github.freva.asciitable.HorizontalAlign;
 import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
 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.CamelTableColumns;
 import org.apache.camel.dsl.jbang.core.common.TerminalWidthHelper;
 import org.apache.camel.dsl.jbang.core.common.VersionHelper;
-import org.apache.camel.main.download.DependencyDownloaderClassLoader;
-import org.apache.camel.main.download.MavenDependencyDownloader;
-import org.apache.camel.support.ObjectHelper;
 import org.apache.camel.util.StringHelper;
 import picocli.CommandLine;
 
@@ -63,6 +60,9 @@ public class CatalogKamelet extends CamelCommand {
             "--kamelets-version" }, description = "Apache Camel Kamelets version (auto-detected from classpath if not set)")
     String kameletsVersion;
 
+    @CommandLine.Mixin
+    MavenResolverMixin mavenResolver = new MavenResolverMixin();
+
     public CatalogKamelet(CamelJBangMain main) {
         super(main);
     }
@@ -76,23 +76,11 @@ public class CatalogKamelet extends CamelCommand {
         }
 
         Map<String, Object> kamelets;
-        var tccLoader = Thread.currentThread().getContextClassLoader();
-        try (MavenDependencyDownloader downloader = new MavenDependencyDownloader()) {
-            ClassLoader cl = createClassLoader();
-            downloader.setClassLoader(cl);
-            downloader.start();
-            downloader.downloadDependency("org.apache.camel.kamelets", "camel-kamelets-catalog", kameletsVersion);
-
-            Thread.currentThread().setContextClassLoader(cl);
-            Class<?> clazz = cl.loadClass("org.apache.camel.kamelets.catalog.KameletsCatalog");
-            Object catalog = clazz.getDeclaredConstructor().newInstance();
-            Method m = clazz.getMethod("getKamelets");
-            kamelets = (Map<String, Object>) ObjectHelper.invokeMethod(m, catalog);
+        try {
+            kamelets = KameletCatalogHelper.loadKamelets(kameletsVersion, mavenResolver.repos());
         } catch (Exception e) {
             printer().printErr("Cannot download camel-kamelets-catalog due to " + e.getMessage());
             return 1;
-        } finally {
-            Thread.currentThread().setContextClassLoader(tccLoader);
         }
 
         for (Object o : kamelets.values()) {
@@ -140,23 +128,19 @@ public class CatalogKamelet extends CamelCommand {
         switch (s) {
             case "name":
                 return o1.name.compareToIgnoreCase(o2.name) * negate;
+            // a Kamelet without the value sorts first
             case "type":
-                return o1.type.compareToIgnoreCase(o2.type) * negate;
+                return CatalogBaseCommand.compareIgnoreCase(o1.type, o2.type) * negate;
             case "level":
             case "support-level":
-                return o1.supportLevel.compareToIgnoreCase(o2.supportLevel) * negate;
+                return CatalogBaseCommand.compareIgnoreCase(o1.supportLevel, o2.supportLevel) * negate;
             case "description":
-                return o1.description.compareToIgnoreCase(o2.description) * negate;
+                return CatalogBaseCommand.compareIgnoreCase(o1.description, o2.description) * negate;
             default:
                 return 0;
         }
     }
 
-    private ClassLoader createClassLoader() {
-        ClassLoader parentCL = CatalogKamelet.class.getClassLoader();
-        return new DependencyDownloaderClassLoader(parentCL);
-    }
-
     private String getDescription(KameletModel r) {
         String d = r.description;
         if (d != null && d.contains(".")) {
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 937a5b785ff0..561680fda17f 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
@@ -28,6 +28,7 @@ import java.util.stream.Stream;
 
 import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
 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.RuntimeType;
 import org.apache.camel.main.download.DownloadException;
@@ -116,25 +117,25 @@ public class UpdateRun extends CamelCommand {
         Process p = pb.redirectErrorStream(true)
                 .start();
 
-        try (BufferedReader stdInput = new BufferedReader(new InputStreamReader(p.getInputStream()));
-             BufferedReader stdError = new BufferedReader(new InputStreamReader(p.getErrorStream()))) {
-
-            String line;
-            while ((line = stdInput.readLine()) != null || (line = stdError.readLine()) != null) {
-                printer().println(line);
-            }
-
-            if (!p.waitFor(updateMixin.upgradeTimeout, TimeUnit.SECONDS)) {
-                p.destroyForcibly();
-                printer().printErr("Update execution timed out");
-
-                return CliExitCode.TIMEOUT;
+        // print the output while the command runs, so the timeout applies to the command
+        Thread reader = new Thread(() -> {
+            try (BufferedReader stdInput = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
+                String line;
+                while ((line = stdInput.readLine()) != null) {
+                    printer().println(line);
+                }
+            } catch (IOException e) {
+                // the command was destroyed
             }
+        }, "camel-update-output");
+        reader.setDaemon(true);
+        reader.start();
 
-            int exitCode = p.exitValue();
-
-            return exitCode;
-
+        if (!p.waitFor(updateMixin.upgradeTimeout, TimeUnit.SECONDS)) {
+            p.destroyForcibly();
+            throw CliException.timeout("Update did not finish within " + updateMixin.upgradeTimeout + " seconds");
         }
+        reader.join(TimeUnit.SECONDS.toMillis(5));
+        return p.exitValue();
     }
 }
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionGet.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionGet.java
index 89a59ba68bf0..b7d95d56885a 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionGet.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionGet.java
@@ -54,7 +54,7 @@ public class VersionGet extends CamelCommand {
             String kv = properties.getProperty("kamelets-version");
             String repos = properties.getProperty("repos");
             String runtime = properties.getProperty("runtime");
-            if (uv != null || repos != null || runtime != null) {
+            if (uv != null || kv != null || repos != null || runtime != null) {
                 printer().println("User configuration:");
                 if (uv != null) {
                     printer().println("    camel-version = " + uv);
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionList.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionList.java
index 63b5137c219a..d461580807fb 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionList.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionList.java
@@ -52,6 +52,7 @@ import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
 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.commands.QuarkusExtensionRegistryMixin;
+import org.apache.camel.dsl.jbang.core.common.CliException;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.dsl.jbang.core.common.QuarkusHelper;
 import org.apache.camel.dsl.jbang.core.common.RuntimeCompletionCandidates;
@@ -403,10 +404,8 @@ public class VersionList extends CamelCommand {
             return versions;
 
         } catch (Exception e) {
-            printer().printErr("Error downloading available Camel versions due to: " + e.getMessage());
+            throw CliException.failure("Error downloading available Camel versions due to: " + e.getMessage());
         }
-
-        return Stream.of();
     }
 
     private Stream<Row> filterVersions(Stream<CamelAndRuntimeVersions> versions, List<ReleaseModel> releases) throws Exception {
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionSet.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionSet.java
index 04d2659112f8..a6c2833004b4 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionSet.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/version/VersionSet.java
@@ -64,6 +64,7 @@ public class VersionSet extends CamelCommand {
         CommandLineHelper.loadProperties(properties -> {
             if (reset) {
                 properties.remove("camel-version");
+                properties.remove("kamelets-version");
                 properties.remove("repos");
                 properties.remove("runtime");
             } else {
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CommandLineHelper.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CommandLineHelper.java
index e14d9e23d914..1327ea893ace 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CommandLineHelper.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CommandLineHelper.java
@@ -24,6 +24,8 @@ import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Properties;
 import java.util.function.Consumer;
 
@@ -205,6 +207,40 @@ public final class CommandLineHelper {
         return homeDir;
     }
 
+    /**
+     * The arguments of a command line typed as one text, such as <tt>--jvm-args</tt>: split at whitespace outside
+     * double or single quotes, which are removed, and a backslash escapes the next character (outside single quotes).
+     */
+    public static List<String> splitArguments(String line) {
+        List<String> answer = new ArrayList<>();
+        StringBuilder current = new StringBuilder();
+        boolean token = false;
+        char quote = 0;
+        for (int i = 0; i < line.length(); i++) {
+            char c = line.charAt(i);
+            if (quote == 0 && Character.isWhitespace(c)) {
+                if (token) {
+                    answer.add(current.toString());
+                    current.setLength(0);
+                    token = false;
+                }
+            } else if (c == '\\' && quote != '\'' && i + 1 < line.length()) {
+                current.append(line.charAt(++i));
+                token = true;
+            } else if ((c == '"' || c == '\'') && (quote == 0 || quote == c)) {
+                quote = quote == 0 ? c : 0;
+                token = true;
+            } else {
+                current.append(c);
+                token = true;
+            }
+        }
+        if (token) {
+            answer.add(current.toString());
+        }
+        return answer;
+    }
+
     /**
      * Adjust basic home directory where user properties and other settings will be stored. Unit tests may set this in
      * order to create stable and independent tests.
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CommandInputErrorsTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CommandInputErrorsTest.java
index 4ce505026a6a..180f7a8b5f49 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CommandInputErrorsTest.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CommandInputErrorsTest.java
@@ -173,6 +173,36 @@ class CommandInputErrorsTest {
                 .extracting(e -> ((CliException) e).getExitCode()).isEqualTo(CliExitCode.USAGE);
     }
 
+    @Test
+    void sbomWithAnUnknownFormat() throws Exception {
+        Path file = Files.writeString(dir.resolve("orders.yaml"), "- from:\n    uri: timer:x\n");
+
+        assertThatThrownBy(() -> call(new SBOMGenerator(main), file.toString(), "--sbom-format=bogus"))
+                .isInstanceOf(CliException.class)
+                .hasMessage("Unknown SBOM format: bogus (use cyclonedx or spdx)")
+                .extracting(e -> ((CliException) e).getExitCode()).isEqualTo(CliExitCode.USAGE);
+    }
+
+    @Test
+    void hardenWithAnLlmThatCannotBeReached() throws Exception {
+        Path file = Files.writeString(dir.resolve("orders.yaml"), "- from:\n    uri: timer:x\n");
+
+        // the route is not sent to another LLM, such as a local Ollama
+        assertThat(call(new Harden(main), file.toString(), "--url=http://127.0.0.1:9")).isEqualTo(CliExitCode.FAILURE);
+        assertThat(printer.getStderr()).startsWith("ERROR: Cannot connect to LLM service at: http://127.0.0.1:9");
+        assertThat(printer.getStdout()).doesNotContain("LLM Configuration");
+    }
+
+    @Test
+    void exportDoesNotCleanTheCurrentDirectory() {
+        assertThat(ExportBaseCommand.isCurrentDirOrParent(".")).isTrue();
+        assertThat(ExportBaseCommand.isCurrentDirOrParent("./")).isTrue();
+        assertThat(ExportBaseCommand.isCurrentDirOrParent(Path.of("").toAbsolutePath().toString())).isTrue();
+        assertThat(ExportBaseCommand.isCurrentDirOrParent("..")).isTrue();
+        assertThat(ExportBaseCommand.isCurrentDirOrParent("out")).isFalse();
+        assertThat(ExportBaseCommand.isCurrentDirOrParent(dir.resolve("out").toString())).isFalse();
+    }
+
     @Test
     void runQuarkusInTheBackground() throws Exception {
         Path file = Files.writeString(dir.resolve("orders.yaml"), "- from:\n    uri: timer:x\n");
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 b62adc4a3695..209321c9260d 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
@@ -28,6 +28,18 @@ import org.junit.jupiter.api.Test;
 
 class CatalogDocTest extends CamelCommandBaseTestSupport {
 
+    @Test
+    public void shouldDocumentAComponentWithoutTheKameletCatalog() throws Exception {
+        CatalogDoc command = createCommand("timer", false);
+        // a Kamelet catalog that cannot be downloaded: the component is found before it is needed
+        command.kameletsVersion = "0.0.0-no-such-version";
+
+        int exit = command.doCall();
+
+        Assertions.assertEquals(0, exit);
+        Assertions.assertTrue(printer.getOutput().contains("timer"), "Should document the timer component");
+    }
+
     @Test
     public void shouldPrintExampleForConsumerComponent() throws Exception {
         CatalogDoc command = createCommand("timer", true);
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKameletTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKameletTest.java
index 9fc4d5dda497..0e3e239ac85b 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKameletTest.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKameletTest.java
@@ -71,6 +71,18 @@ class CatalogKameletTest extends CamelCommandBaseTestSupport {
         assertEquals(List.of("Aws-S3-sink", "kafka-source", "timer-source"), names(rows));
     }
 
+    @Test
+    void shouldSortAKameletWithoutDescriptionFirst() {
+        CatalogKamelet cmd = command();
+        cmd.sort = "description";
+
+        List<KameletModel> rows = sampleRows();
+        rows.add(model("no-description-sink", "sink", null, null));
+        rows.sort(cmd::sortRow);
+
+        assertEquals(List.of("no-description-sink", "timer-source", "kafka-source", "Aws-S3-sink"), names(rows));
+    }
+
     @Test
     void shouldReverseOrderWhenSortKeyHasLeadingMinus() {
         CatalogKamelet cmd = command();
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/version/VersionGetTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/version/VersionGetTest.java
index 273841f6ae85..f916ede1081f 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/version/VersionGetTest.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/version/VersionGetTest.java
@@ -65,6 +65,21 @@ class VersionGetTest extends CamelCommandBaseTestSupport {
         Assertions.assertEquals("kamelets-version = greatest", lines.get(4));
     }
 
+    @Test
+    public void shouldPrintOnlyTheKameletsVersion() throws Exception {
+        UserConfigHelper.createUserConfig("""
+                kamelets-version=4.18.0
+                """);
+        createJBangVersionFile("0.101");
+
+        VersionGet command = createCommand();
+        command.doCall();
+
+        List<String> lines = printer.getLines();
+        Assertions.assertEquals("User configuration:", lines.get(2));
+        Assertions.assertEquals("kamelets-version = 4.18.0", lines.get(3));
+    }
+
     private void createJBangVersionFile(String version) throws IOException {
         Path jBangDir = Paths.get("target", ".jbang", "cache");
         if (!jBangDir.toFile().exists()) {
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/version/VersionSetTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/version/VersionSetTest.java
index 23fc0f5a171f..c6c5463113a2 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/version/VersionSetTest.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/version/VersionSetTest.java
@@ -90,6 +90,7 @@ class VersionSetTest extends BaseConfigTestSupport {
     void shouldRemoveAllSettingsOnReset() throws Exception {
         UserConfigHelper.createUserConfig("""
                 camel-version=4.9.0
+                kamelets-version=4.9.0
                 repos=https://repo.example.org/maven
                 runtime=spring-boot
                 other=keep-me
@@ -101,9 +102,10 @@ class VersionSetTest extends BaseConfigTestSupport {
 
         CommandLineHelper.loadProperties(properties -> {
             assertFalse(properties.containsKey("camel-version"), "camel-version must be removed on reset");
+            assertFalse(properties.containsKey("kamelets-version"), "kamelets-version must be removed on reset");
             assertFalse(properties.containsKey("repos"), "repos must be removed on reset");
             assertFalse(properties.containsKey("runtime"), "runtime must be removed on reset");
-            // unrelated settings are preserved: reset only clears the three version keys
+            // unrelated settings are preserved: reset only clears the version keys
             assertEquals("keep-me", properties.get("other"));
         });
     }
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/CommandLineArgumentsTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/CommandLineArgumentsTest.java
new file mode 100644
index 000000000000..42d56f85bd29
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/CommandLineArgumentsTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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.common;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The arguments of a command line typed as one text, such as --jvm-args: a quoted value with spaces is one argument.
+ */
+class CommandLineArgumentsTest {
+
+    @Test
+    void splitAtWhitespace() {
+        assertThat(CommandLineHelper.splitArguments(" -Xmx1g\t-Dfoo=bar  ")).containsExactly("-Xmx1g", "-Dfoo=bar");
+        assertThat(CommandLineHelper.splitArguments("")).isEmpty();
+    }
+
+    @Test
+    void quotedValueIsOneArgument() {
+        assertThat(CommandLineHelper.splitArguments("-Xmx1g -Dgreeting=\"hello world\""))
+                .containsExactly("-Xmx1g", "-Dgreeting=hello world");
+        assertThat(CommandLineHelper.splitArguments("-Dgreeting='hello world' -Dempty=''"))
+                .containsExactly("-Dgreeting=hello world", "-Dempty=");
+        // a quote of the other kind inside quotes is kept
+        assertThat(CommandLineHelper.splitArguments("-Dname=\"O'Brien\"")).containsExactly("-Dname=O'Brien");
+    }
+
+    @Test
+    void backslashEscapes() {
+        assertThat(CommandLineHelper.splitArguments("-Dpath=a\\ b")).containsExactly("-Dpath=a b");
+        // not inside single quotes
+        assertThat(CommandLineHelper.splitArguments("'a\\b'")).containsExactly("a\\b");
+    }
+}
diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesPodLogs.java b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesPodLogs.java
index 97f3614842d9..5a88a88ff156 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesPodLogs.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesPodLogs.java
@@ -25,6 +25,8 @@ import io.fabric8.kubernetes.client.dsl.LogWatch;
 import io.fabric8.kubernetes.client.dsl.PodResource;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.commands.kubernetes.traits.BaseTrait;
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
 import org.apache.camel.util.ObjectHelper;
 import picocli.CommandLine;
 import picocli.CommandLine.Command;
@@ -59,6 +61,9 @@ public class KubernetesPodLogs extends KubernetesBaseCommand {
     // used for testing
     long maxMessageCount = -1;
     long messageCount = 0;
+    // whether a pod with the label showed up, and whether reading its log failed the last time
+    private boolean podFound;
+    private boolean readFailed;
 
     public KubernetesPodLogs(CamelJBangMain main) {
         super(main);
@@ -74,8 +79,7 @@ public class KubernetesPodLogs extends KubernetesBaseCommand {
 
         String[] parts = label.split("=", 2);
         if (parts.length != 2) {
-            printer().printErr("--label selector must be in syntax: key=value");
-            return 1;
+            throw CliException.usage("--label selector must be in syntax: key=value");
         }
 
         var retry = watchLogs();
@@ -86,7 +90,10 @@ public class KubernetesPodLogs extends KubernetesBaseCommand {
         }
 
         printer().println("Stopped pod logging!");
-        return 0;
+        if (!podFound) {
+            throw CliException.notFound("No pod with label " + label);
+        }
+        return readFailed ? CliExitCode.FAILURE : CliExitCode.OK;
     }
 
     // Returns true if a retry should be attempted
@@ -100,6 +107,7 @@ public class KubernetesPodLogs extends KubernetesBaseCommand {
             printer().printf("Pod for label %s not available%n", label);
             return true;
         }
+        podFound = true;
 
         var terminated = isPodTerminated(podRes);
         if (!terminated) {
@@ -128,8 +136,10 @@ public class KubernetesPodLogs extends KubernetesBaseCommand {
                     }
                     retryCount = 0;
                 }
+                readFailed = false;
             } catch (IOException e) {
-                printer().println("Failed to read pod logs - " + e.getMessage());
+                printer().printErr("Failed to read pod logs - " + e.getMessage());
+                readFailed = true;
             }
 
             terminated = isPodTerminated(podRes);
diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesRun.java b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesRun.java
index 64785c15f45b..4f06110304af 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesRun.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/main/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesRun.java
@@ -36,6 +36,7 @@ import java.util.concurrent.TimeUnit;
 import io.fabric8.kubernetes.api.model.Pod;
 import io.fabric8.kubernetes.client.KubernetesClient;
 import io.fabric8.kubernetes.client.KubernetesClientBuilder;
+import io.fabric8.kubernetes.client.KubernetesClientTimeoutException;
 import io.fabric8.kubernetes.client.vertx5.Vertx5HttpClientFactory;
 import io.vertx.core.Vertx;
 import io.vertx.core.VertxOptions;
@@ -50,6 +51,7 @@ import org.apache.camel.dsl.jbang.core.commands.kubernetes.traits.BaseTrait;
 import org.apache.camel.dsl.jbang.core.commands.kubernetes.traits.MountTrait;
 import org.apache.camel.dsl.jbang.core.commands.kubernetes.traits.TraitHelper;
 import org.apache.camel.dsl.jbang.core.commands.kubernetes.traits.model.Traits;
+import org.apache.camel.dsl.jbang.core.common.CliException;
 import org.apache.camel.dsl.jbang.core.common.Printer;
 import org.apache.camel.dsl.jbang.core.common.QuarkusHelper.QuarkusPlatformBom;
 import org.apache.camel.dsl.jbang.core.common.RuntimeCompletionCandidates;
@@ -627,8 +629,13 @@ public class KubernetesRun extends KubernetesBaseCommand {
             }
             printer().println("Run: " + kubectlCmd);
         }
-        var pod = client(Pod.class).withLabel(BaseTrait.KUBERNETES_LABEL_NAME, projectName)
-                .waitUntilCondition(it -> "Running".equals(getPodPhase(it)), 10, TimeUnit.MINUTES);
+        Pod pod;
+        try {
+            pod = client(Pod.class).withLabel(BaseTrait.KUBERNETES_LABEL_NAME, projectName)
+                    .waitUntilCondition(it -> "Running".equals(getPodPhase(it)), 10, TimeUnit.MINUTES);
+        } catch (KubernetesClientTimeoutException e) {
+            throw CliException.timeout("The pod of " + projectName + " is not running after 10 minutes");
+        }
         printer().println(String.format("Pod '%s' in phase %s", pod.getMetadata().getName(), getPodPhase(pod)));
     }
 
@@ -719,22 +726,20 @@ public class KubernetesRun extends KubernetesBaseCommand {
 
         pb.command(args.toArray(String[]::new));
 
-        if (quiet || !verbose) {
-            pb.redirectErrorStream(true);
-            pb.redirectOutput(ProcessBuilder.Redirect.DISCARD);
-        } else {
-            pb.inheritIO(); // run in foreground (with IO so logs are visible)
-        }
-
-        Process p = pb.start();
-        // wait for that process to exit as we run in foreground
-        int exit = p.waitFor();
-        if (exit != 0) {
-            printer().printErr("Build failed!");
-            return exit;
+        Path log = mavenOutput(pb);
+        try {
+            Process p = pb.start();
+            // wait for that process to exit as we run in foreground
+            int exit = p.waitFor();
+            if (exit != 0) {
+                printer().printErr("Build failed!");
+                printMavenOutput(log);
+                return exit;
+            }
+            return 0;
+        } finally {
+            removeMavenOutput(log);
         }
-
-        return 0;
     }
 
     private Integer deployProject(String workingDir, boolean reload) throws Exception {
@@ -801,26 +806,66 @@ public class KubernetesRun extends KubernetesBaseCommand {
 
         pb.command(args.toArray(String[]::new));
 
-        if (quiet || !verbose) {
-            pb.redirectErrorStream(true);
-            pb.redirectOutput(ProcessBuilder.Redirect.DISCARD);
-        } else {
+        Path log = mavenOutput(pb);
+        try {
+            Process p = pb.start();
+            // wait for that process to exit as we run in foreground
+            int exit = p.waitFor();
+            if (exit != 0) {
+                printer().printErr("Deployment to %s failed!".formatted(clusterType));
+                printMavenOutput(log);
+                return exit;
+            }
+            return 0;
+        } finally {
+            removeMavenOutput(log);
+        }
+    }
+
+    /**
+     * Shows the output of Maven with --verbose, and keeps it in a log file otherwise, for when it fails.
+     *
+     * @return the log file, or <tt>null</tt> with --verbose
+     */
+    private Path mavenOutput(ProcessBuilder pb) throws IOException {
+        if (!quiet && verbose) {
             pb.inheritIO(); // run in foreground (with IO so logs are visible)
+            return null;
         }
+        Path log = Files.createTempFile("camel-kubernetes-", ".log");
+        log.toFile().deleteOnExit();
+        pb.redirectErrorStream(true);
+        pb.redirectOutput(log.toFile());
+        return log;
+    }
 
-        Process p = pb.start();
-        // wait for that process to exit as we run in foreground
-        int exit = p.waitFor();
-        if (exit != 0) {
-            String msg = "Deployment to %s failed!";
-            if (!verbose) {
-                msg += " (use --verbose for more details)";
+    /**
+     * Removes the log of Maven once it has ended: camel kubernetes run --dev builds and deploys again on every change.
+     */
+    private static void removeMavenOutput(Path log) {
+        if (log != null) {
+            try {
+                Files.deleteIfExists(log);
+            } catch (IOException e) {
+                // removed when the JVM exits
             }
-            printer().printErr(msg.formatted(clusterType));
-            return exit;
         }
+    }
 
-        return 0;
+    /**
+     * Prints the end of the output of Maven when it failed (it was hidden, as no --verbose was given).
+     */
+    private void printMavenOutput(Path log) {
+        if (log == null) {
+            return;
+        }
+        try {
+            List<String> lines = Files.readAllLines(log);
+            printer().printDiagnostic(String.join(System.lineSeparator(),
+                    lines.subList(Math.max(0, lines.size() - 30), lines.size())));
+        } catch (IOException e) {
+            // no output to show
+        }
     }
 
     void detectCluster() {
diff --git a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesPodLogsTest.java b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesPodLogsTest.java
index b422d8e0dbaf..b7b8840baf2e 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesPodLogsTest.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-kubernetes/src/test/java/org/apache/camel/dsl/jbang/core/commands/kubernetes/KubernetesPodLogsTest.java
@@ -23,6 +23,8 @@ import io.fabric8.kubernetes.api.model.Pod;
 import io.fabric8.kubernetes.api.model.PodBuilder;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.commands.kubernetes.traits.BaseTrait;
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
@@ -36,8 +38,9 @@ class KubernetesPodLogsTest extends KubernetesBaseTestSupport {
         KubernetesPodLogs command = createCommand();
         command.name = "mickey-mouse";
         command.maxRetryAttempts = 2; // total timeout of 4 seconds
-        int exit = command.doCall();
-        Assertions.assertEquals(0, exit);
+        CliException e = Assertions.assertThrows(CliException.class, command::doCall);
+        Assertions.assertEquals(CliExitCode.NOT_FOUND, e.getExitCode());
+        Assertions.assertEquals("No pod with label app.kubernetes.io/name=mickey-mouse", e.getMessage());
 
         Assertions.assertTrue(
                 printer.getOutput().contains("Pod for label app.kubernetes.io/name=mickey-mouse not available"));
diff --git a/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/PropertiesValidateCommand.java b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/PropertiesValidateCommand.java
index 0f71f20448fd..0b5ae84b2e0b 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/PropertiesValidateCommand.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/PropertiesValidateCommand.java
@@ -30,6 +30,7 @@ import org.apache.camel.catalog.DefaultCamelCatalog;
 import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.commands.ai.SourceValidator;
+import org.apache.camel.dsl.jbang.core.common.CliException;
 import org.apache.camel.dsl.jbang.core.common.JsonOutput;
 import org.apache.camel.util.FileUtil;
 import picocli.CommandLine;
@@ -70,6 +71,9 @@ public class PropertiesValidateCommand extends CamelCommand {
             }
         }
 
+        if (reports.isEmpty()) {
+            throw CliException.notFound("No properties files to validate");
+        }
         int count = reports.values().stream().mapToInt(List::size).sum();
         if (jsonOutput) {
             JsonOutput.printList(printer(),
diff --git a/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlValidateCommand.java b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlValidateCommand.java
index 246a273caa6b..84ec7c2cd711 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlValidateCommand.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlValidateCommand.java
@@ -32,6 +32,7 @@ import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.commands.ai.SourceValidator;
+import org.apache.camel.dsl.jbang.core.common.CliException;
 import org.apache.camel.dsl.jbang.core.common.JsonOutput;
 import org.apache.camel.dsl.yaml.validator.YamlValidator;
 import org.apache.camel.util.FileUtil;
@@ -109,6 +110,9 @@ public class YamlValidateCommand extends CamelCommand {
             }
         }
 
+        if (reports.isEmpty()) {
+            throw CliException.notFound("No YAML files to validate");
+        }
         int count = errorCounts(reports);
         if (jsonOutput) {
             List<JsonObject> rows = new ArrayList<>();
diff --git a/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/java/org/apache/camel/dsl/jbang/core/commands/validate/ValidateJsonTest.java b/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/java/org/apache/camel/dsl/jbang/core/commands/validate/ValidateJsonTest.java
index 7113fd943a2c..79323113e0f7 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/java/org/apache/camel/dsl/jbang/core/commands/validate/ValidateJsonTest.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/java/org/apache/camel/dsl/jbang/core/commands/validate/ValidateJsonTest.java
@@ -21,6 +21,8 @@ import java.nio.file.Path;
 
 import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
 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.StringPrinter;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
@@ -30,6 +32,7 @@ import org.junit.jupiter.api.io.TempDir;
 import picocli.CommandLine;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 /**
  * camel validate with --json: one JSON array on stdout with valid, file and errors of each file, as the MCP tool
@@ -100,12 +103,24 @@ class ValidateJsonTest {
         Path text = dir.resolve("notes.txt");
         Files.writeString(text, "not a route");
 
-        assertThat(run(new YamlValidateCommand(main()), "--json", text.toString())).isZero();
-
-        assertThat(printer.getStdout()).isEqualTo("[]");
+        // nothing is left to validate, which is not found (the error document is printed by the error handler)
+        CliException e = assertThrows(CliException.class,
+                () -> run(new YamlValidateCommand(main()), "--json", text.toString()));
+        assertThat(e.getExitCode()).isEqualTo(CliExitCode.NOT_FOUND);
         assertThat(printer.getStderr()).startsWith("WARN: Skipping non-YAML file");
     }
 
+    @Test
+    void nothingToValidateIsNotFound() throws Exception {
+        Path text = Files.writeString(dir.resolve("notes.txt"), "not a route");
+
+        CliException e = assertThrows(CliException.class, () -> run(new YamlValidateCommand(main()), text.toString()));
+        assertThat(e.getExitCode()).isEqualTo(CliExitCode.NOT_FOUND);
+        assertThat(e.getMessage()).isEqualTo("No YAML files to validate");
+        e = assertThrows(CliException.class, () -> run(new PropertiesValidateCommand(main()), text.toString()));
+        assertThat(e.getMessage()).isEqualTo("No properties files to validate");
+    }
+
     private CamelJBangMain main() {
         printer = new StringPrinter();
         return new CamelJBangMain().withPrinter(printer);