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 6b7aff558d4a CAMEL-24727: camel-jbang-export - an export keeps to its 
request (work dir, logging, system properties, repositories, leak) (#28688)
6b7aff558d4a is described below

commit 6b7aff558d4a881e0048830bf8f0c3209ab9ad44
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 30 08:19:23 2026 +0100

    CAMEL-24727: camel-jbang-export - an export keeps to its request (work dir, 
logging, system properties, repositories, leak) (#28688)
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../modules/ROOT/pages/camel-jbang-projects.adoc   |  16 +++
 .../camel/dsl/jbang/core/commands/Export.java      |   1 +
 .../dsl/jbang/core/commands/ExportBaseCommand.java |  19 +++
 .../jbang/core/commands/MavenResolverMixin.java    |  11 +-
 .../commands/QuarkusExtensionRegistryMixin.java    |  20 ++-
 .../jbang/core/commands/QuarkusPlatformMixin.java  |   5 +-
 .../apache/camel/dsl/jbang/core/commands/Run.java  |  62 ++++----
 .../jbang/core/commands/RunDependencyResolver.java |   1 +
 .../dsl/jbang/core/commands/ExportApiTest.java     | 129 +++++++++++++++++
 .../camel/dsl/jbang/export/CamelMainExporter.java  |  14 +-
 .../camel/dsl/jbang/export/DependencyResolver.java |   5 +-
 .../camel/dsl/jbang/export/ExportRequest.java      |  10 ++
 .../apache/camel/dsl/jbang/export/Exporter.java    |  52 +++----
 .../camel/dsl/jbang/export/ProjectExporter.java    |  18 +--
 .../camel/dsl/jbang/export/QuarkusExporter.java    |  10 +-
 .../camel/dsl/jbang/export/SpringBootExporter.java |  11 +-
 .../jbang/export/catalog/KameletCatalogHelper.java |   7 +-
 .../dsl/jbang/export/common/CatalogLoader.java     |  26 +++-
 .../dsl/jbang/export/common/MavenResolver.java     |  27 +++-
 .../export/common/MavenResolverMixinSpec.java      |   8 ++
 .../dsl/jbang/export/common/QuarkusPlatform.java   |  14 +-
 .../dsl/jbang/export/ProjectExporterTest.java      | 158 ++++++++++++++++++---
 .../java/org/apache/camel/main/KameletMain.java    |  17 ++-
 .../main/download/MavenDependencyDownloader.java   |  16 +++
 .../camel/main/download/TransactedDownloader.java  |   9 +-
 .../camel/tooling/maven/MavenDownloader.java       |  29 ++++
 .../camel/tooling/maven/MavenDownloaderImpl.java   |  30 ++--
 .../tooling/maven/MavenDownloaderImplTest.java     |  35 +++++
 28 files changed, 622 insertions(+), 138 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-projects.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-projects.adoc
index 1503455cf940..0646f15651bd 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-projects.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-projects.adoc
@@ -212,6 +212,22 @@ int exit = new ProjectExporter(context).export(request);
 To find the dependencies of the integration, the export boots it silently, the 
same way as `camel run` does.
 This is done by a `DependencyResolver`, which `camel-jbang-core` provides, so 
it must be on the classpath as well (or you set your own resolver on the 
`ExportContext`).
 
+The export keeps to the request and leaves the JVM as it is, so an application 
can export many times, and several exports can run at the same time:
+
+* The run settings of the silent boot and the project being built are kept in 
the work directory of the request (`request.workDir`).
+It is the `.camel-jbang` directory of the current directory by default, as 
with `camel export`, and it is deleted and created again on each export: give 
each export that runs at the same time a directory of its own.
+* The export does not configure logging: it logs as the application has 
configured.
+* The export reads no system properties for its options.
+`camel export` sets what its system properties configure (such as 
`camel.jbang.camelSpringBootVersion` or `camel.jbang.repos`) on the request 
before it exports.
+* The extra default Maven repositories are an option of the request, used for 
the downloads and in the `pom.xml` of the project.
+When they are not set, they are the ones of the `camel.extra.repos` system 
property, as with the other commands:
++
+[source,java]
+----
+// download from, and add to the pom.xml, this repository (and none of 
camel.extra.repos)
+request.mavenResolver = new MavenResolver(true, null, false, 
"acme=https://repo.acme.com/maven2";);
+----
+
 == Gathering list of dependencies
 
 List the Camel components and JARs required to run your integration:
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Export.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Export.java
index 91d04b62291e..ba4e50aedd12 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Export.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Export.java
@@ -56,6 +56,7 @@ public class Export extends ExportBaseCommand {
         ExportRequest request = toRequest();
         ProjectExporter exporter = new ProjectExporter(exportContext());
         Path baseDir = exporter.prepare(request);
+        applySystemProperties(request);
         // the options from application.properties, the profile and the system 
properties
         fromRequest(request);
 
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 d7bfd1f8a58f..58fa300b7443 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
@@ -40,6 +40,7 @@ import org.apache.camel.dsl.jbang.export.ProjectExporter;
 import org.apache.camel.dsl.jbang.export.common.HawtioVersion;
 import org.apache.camel.dsl.jbang.export.common.MavenResolver;
 import org.apache.camel.dsl.jbang.export.common.Printer;
+import org.apache.camel.dsl.jbang.export.common.PropertyResolver;
 import org.apache.camel.dsl.jbang.export.common.QuarkusPlatform;
 import org.apache.camel.dsl.jbang.export.common.RuntimeType;
 import org.apache.camel.dsl.jbang.export.common.RuntimeUtil;
@@ -382,6 +383,7 @@ public abstract class ExportBaseCommand extends 
CamelCommand {
     protected Integer exportRuntime(Path exportBaseDir) throws Exception {
         ExportRequest request = toRequest();
         request.exportBaseDir = exportBaseDir;
+        applySystemProperties(request);
         Exporter exporter = new 
ProjectExporter(exportContext()).createExporter(request);
         if (exporter == null) {
             printer().printErr("Unknown runtime: " + runtime);
@@ -390,6 +392,23 @@ public abstract class ExportBaseCommand extends 
CamelCommand {
         return exporter.export();
     }
 
+    /**
+     * Sets what the system properties of the CLI configure on the request, as 
the export engine reads no system
+     * properties: the Quarkus platform, the Camel Spring Boot version, and 
the repositories (camel.jbang.repos) when
+     * none are configured. The extra default repositories of the 
camel.extra.repos system property are left to the
+     * Maven downloader.
+     */
+    static void applySystemProperties(ExportRequest request) {
+        request.quarkusPlatform = QuarkusPlatform.of(System.getProperties(), 
request.quarkusPlatform);
+        request.camelSpringBootVersion
+                = 
PropertyResolver.fromSystemProperty(CAMEL_SPRING_BOOT_VERSION, () -> 
request.camelSpringBootVersion);
+        MavenResolver resolver = request.mavenResolver;
+        String repos = System.getProperty(REPOS);
+        if (resolver.repos() == null && repos != null) {
+            request.mavenResolver = new MavenResolver(resolver.download(), 
repos, resolver.fresh(), resolver.extraRepos());
+        }
+    }
+
     protected void addDependencies(String... deps) {
         var depsArray = Optional.ofNullable(deps).orElse(new String[0]);
         dependencies.addAll(Arrays.asList(depsArray));
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/MavenResolverMixin.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/MavenResolverMixin.java
index 864e21a187ac..fc42097e5ff1 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/MavenResolverMixin.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/MavenResolverMixin.java
@@ -43,6 +43,9 @@ public class MavenResolverMixin implements 
MavenResolverMixinSpec {
                         defaultValue = "false")
     boolean fresh;
 
+    // not an option: set by an export through the Java API, else the 
camel.extra.repos system property is in use
+    String extraRepos;
+
     private volatile MavenResolver resolver;
     private final Object resolverLock = new Object();
 
@@ -52,6 +55,7 @@ public class MavenResolverMixin implements 
MavenResolverMixinSpec {
                 = 
Boolean.parseBoolean(props.getProperty(CamelJBangConstants.DOWNLOAD, 
String.valueOf(fallback.download())));
         result.repos = props.getProperty(CamelJBangConstants.REPOS, 
fallback.repos());
         result.fresh = 
Boolean.parseBoolean(props.getProperty(CamelJBangConstants.FRESH, 
String.valueOf(fallback.fresh())));
+        result.extraRepos = fallback.extraRepos();
         return result;
     }
 
@@ -80,7 +84,7 @@ public class MavenResolverMixin implements 
MavenResolverMixinSpec {
         if ((r = resolver) == null) {
             synchronized (resolverLock) {
                 if ((r = resolver) == null) {
-                    r = new MavenResolver(download, repos, fresh);
+                    r = new MavenResolver(download, repos, fresh, extraRepos);
                     resolver = r;
                 }
             }
@@ -92,6 +96,11 @@ public class MavenResolverMixin implements 
MavenResolverMixinSpec {
         return fresh;
     }
 
+    @Override
+    public String extraRepos() {
+        return extraRepos;
+    }
+
     /**
      * Put {@link #download}, {@link #repos} and {@link fresh} to the given 
{@link Properties}.
      *
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/QuarkusExtensionRegistryMixin.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/QuarkusExtensionRegistryMixin.java
index 981f0714bf02..2db7cd31083b 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/QuarkusExtensionRegistryMixin.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/QuarkusExtensionRegistryMixin.java
@@ -18,6 +18,7 @@ package org.apache.camel.dsl.jbang.core.commands;
 
 import java.nio.file.Path;
 
+import org.apache.camel.dsl.jbang.export.common.QuarkusHelper;
 import org.apache.camel.dsl.jbang.export.common.QuarkusPlatform;
 import picocli.CommandLine;
 
@@ -42,7 +43,24 @@ public class QuarkusExtensionRegistryMixin {
     }
 
     public String quarkusExtensionRegistryBaseUri() {
-        return 
QuarkusPlatform.resolveExtensionRegistryBaseUri(quarkusExtensionRegistryBaseUri);
+        return 
resolveExtensionRegistryBaseUri(quarkusExtensionRegistryBaseUri);
+    }
+
+    /**
+     * The base URI of the Quarkus Extension Registry: the configured one, 
else the one of the
+     * camel.jbang.quarkus.platform.url system property (without its 
"/client/platforms" suffix), else the default
+     * registry; without a trailing slash.
+     */
+    public static String resolveExtensionRegistryBaseUri(String 
quarkusExtensionRegistryBaseUri) {
+        String result = quarkusExtensionRegistryBaseUri;
+        if (result == null) {
+            result = 
System.getProperty(QuarkusHelper.QUARKUS_PLATFORM_URL_PROPERTY);
+            final String suffix = "/client/platforms";
+            if (result != null && result.endsWith(suffix)) {
+                result = result.substring(0, result.length() - 
suffix.length());
+            }
+        }
+        return QuarkusPlatform.extensionRegistryBaseUri(result);
     }
 
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/QuarkusPlatformMixin.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/QuarkusPlatformMixin.java
index ad0abf76ab53..6a8a50b1efd8 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/QuarkusPlatformMixin.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/QuarkusPlatformMixin.java
@@ -68,10 +68,11 @@ public class QuarkusPlatformMixin extends 
QuarkusExtensionRegistryMixin implemen
     }
 
     /**
-     * @return the {@link QuarkusPlatform} with the options of this mixin
+     * @return the {@link QuarkusPlatform} with the options of this mixin, and 
the base URI of the Quarkus Extension
+     *         Registry of the camel.jbang.quarkus.platform.url system 
property when it is not configured
      */
     public QuarkusPlatform toPlatform() {
-        return new QuarkusPlatform(quarkusGroupId, quarkusVersion, 
quarkusExtensionRegistryBaseUri);
+        return new QuarkusPlatform(quarkusGroupId, quarkusVersion, 
quarkusExtensionRegistryBaseUri());
     }
 
     /**
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 f140bdbe1f6c..d6fc24f850c4 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
@@ -77,9 +77,7 @@ import org.apache.camel.dsl.jbang.export.common.ExportHelper;
 import org.apache.camel.dsl.jbang.export.common.GenAiDependencyHelper;
 import org.apache.camel.dsl.jbang.export.common.PluginExporter;
 import org.apache.camel.dsl.jbang.export.common.Printer;
-import org.apache.camel.dsl.jbang.export.common.PropertyResolver;
 import 
org.apache.camel.dsl.jbang.export.common.QuarkusHelper.QuarkusPlatformBom;
-import org.apache.camel.dsl.jbang.export.common.QuarkusPlatform;
 import org.apache.camel.dsl.jbang.export.common.RuntimeType;
 import org.apache.camel.dsl.jbang.export.common.RuntimeUtil;
 import org.apache.camel.dsl.jbang.export.common.Source;
@@ -90,6 +88,7 @@ import org.apache.camel.dsl.jbang.export.common.VersionHelper;
 import org.apache.camel.main.BaseMainSupport;
 import org.apache.camel.main.KameletMain;
 import org.apache.camel.main.MainListenerSupport;
+import org.apache.camel.main.SimpleMainShutdownStrategy;
 import org.apache.camel.main.download.DownloadListener;
 import org.apache.camel.main.util.SuggestSimilarHelper;
 import org.apache.camel.spi.BacklogDebugger;
@@ -165,8 +164,8 @@ public class Run extends CamelCommand {
     private static final Set<String> ACCEPTED_YAML_ROOT_ELEMENTS
             = new HashSet<>(Arrays.asList(ACCEPTED_YAML_ROOT_ELEMENT_NAMES));
 
-    private static final String OPENAPI_GENERATED_FILE = 
CommandLineHelper.CAMEL_JBANG_WORK_DIR + "/generated-openapi.yaml";
-    private static final String CLIPBOARD_GENERATED_FILE = 
CommandLineHelper.CAMEL_JBANG_WORK_DIR + "/generated-clipboard";
+    private static final String OPENAPI_GENERATED_FILE = 
"generated-openapi.yaml";
+    private static final String CLIPBOARD_GENERATED_FILE = 
"generated-clipboard";
     // the same layout as the camel-jbang console logging, so the log file 
reads the same for every runtime
     static final String QUARKUS_LOG_FILE_FORMAT
             = "%d{yyyy-MM-dd HH:mm:ss.SSS} %5p %i --- [%15.15t] %-40.40c{3.} : 
%s%e%n";
@@ -181,6 +180,8 @@ public class Run extends CamelCommand {
 
     public boolean exportRun;
     protected Path exportBaseDir;
+    // the working directory, which is deleted and created again when running
+    Path workDir = CommandLineHelper.getWorkDir();
     boolean scriptRun;
     boolean transformRun;
     boolean transformMessageRun;
@@ -861,13 +862,12 @@ public class Run extends CamelCommand {
             }
         }
 
-        Path work = CommandLineHelper.getWorkDir();
-        removeDir(work);
-        if (!Files.exists(work)) {
+        removeDir(workDir);
+        if (!Files.exists(workDir)) {
             try {
-                Files.createDirectories(work);
+                Files.createDirectories(workDir);
             } catch (IOException e) {
-                printer().printWarn("Failed to create working directory: " + 
work.toAbsolutePath());
+                printer().printWarn("Failed to create working directory: " + 
workDir.toAbsolutePath());
             }
         }
 
@@ -947,10 +947,16 @@ public class Run extends CamelCommand {
         }
 
         final KameletMain main = createMainInstance();
+        if (exportRun) {
+            // the silent run of an export boots and stops in this JVM, which 
may run many exports (camel export
+            // through the Java API): the default strategy adds a JVM shutdown 
hook that keeps the run until the JVM exits
+            main.setShutdownStrategy(new SimpleMainShutdownStrategy());
+        }
         main.setProfile(profile);
         if (mavenResolver.repos() != null && !mavenResolver.repos().isBlank()) 
{
             main.setRepositories(mavenResolver.repos());
         }
+        main.setExtraRepositories(mavenResolver.extraRepos());
         main.setDownload(mavenResolver.download());
         main.setPackageScanJars(packageScanJars);
         main.setFresh(mavenResolver.fresh());
@@ -1018,8 +1024,7 @@ public class Run extends CamelCommand {
         if (prompt) {
             writeSetting(main, profileProperties, PROMPT, "true");
         }
-        writeSetting(main, profileProperties, COMPILE_WORK_DIR,
-                Paths.get(CommandLineHelper.CAMEL_JBANG_WORK_DIR, 
"compile").toString());
+        writeSetting(main, profileProperties, COMPILE_WORK_DIR, 
workDir.resolve("compile").toString());
 
         if (gav != null) {
             writeSetting(main, profileProperties, GAV, gav);
@@ -1142,7 +1147,7 @@ public class Run extends CamelCommand {
 
         // include generated openapi to files to run
         if (openapi != null) {
-            files.add(OPENAPI_GENERATED_FILE);
+            files.add(openApiGeneratedFile());
         }
 
         // if we only run pom.xml then auto discover from the Maven based 
project
@@ -1736,7 +1741,7 @@ public class Run extends CamelCommand {
         eq.javaLiveReload = this.dev;
         eq.symbolicLink = this.dev;
         eq.mavenWrapper = true;
-        eq.quarkusPlatform = QuarkusPlatform.of(System.getProperties(), 
this.quarkusPlatform);
+        eq.quarkusPlatform = this.quarkusPlatform.toPlatform();
         eq.javaVersion = javaVersion;
         eq.camelVersion = this.camelVersion;
         eq.javaVersion = this.javaVersion;
@@ -1779,6 +1784,7 @@ public class Run extends CamelCommand {
         printer().println("Running using Quarkus (preparing and downloading 
files)");
 
         // run export
+        ExportBaseCommand.applySystemProperties(eq);
         int exit = new QuarkusExporter(eq, CliExportContext.create(getMain(), 
getMain().getOut(), eq.yes)).export();
         if (exit != 0) {
             return exit;
@@ -1940,6 +1946,7 @@ public class Run extends CamelCommand {
         printer().println("Running using Camel Main (preparing and downloading 
files)");
 
         // run export
+        ExportBaseCommand.applySystemProperties(eq);
         int exit = new CamelMainExporter(eq, 
CliExportContext.create(getMain(), getMain().getOut(), eq.yes)).export();
         if (exit != 0) {
             return exit;
@@ -2547,8 +2554,7 @@ public class Run extends CamelCommand {
         eq.springBootVersion = this.springBootVersion;
         eq.javaVersion = this.javaVersion;
         eq.camelVersion = this.camelVersion;
-        eq.camelSpringBootVersion = 
PropertyResolver.fromSystemProperty(CAMEL_SPRING_BOOT_VERSION,
-                () -> this.camelSpringBootVersion != null ? 
this.camelSpringBootVersion : this.camelVersion);
+        eq.camelSpringBootVersion = this.camelSpringBootVersion != null ? 
this.camelSpringBootVersion : this.camelVersion;
         eq.javaVersion = this.javaVersion;
         eq.kameletsVersion = this.kameletsVersion;
         eq.exportDir = runDirPath.toString();
@@ -2593,6 +2599,7 @@ public class Run extends CamelCommand {
         printer().println("Running using Spring Boot (preparing and 
downloading files)");
 
         // run export
+        ExportBaseCommand.applySystemProperties(eq);
         int exit = new SpringBootExporter(eq, 
CliExportContext.create(getMain(), getMain().getOut(), eq.yes)).export();
         if (exit != 0) {
             return exit;
@@ -3429,7 +3436,7 @@ public class Run extends CamelCommand {
         Clipboard c = Toolkit.getDefaultToolkit().getSystemClipboard();
         Object t = c.getData(DataFlavor.stringFlavor);
         if (t != null) {
-            String fn = CLIPBOARD_GENERATED_FILE + "." + ext;
+            String fn = workDir + "/" + CLIPBOARD_GENERATED_FILE + "." + ext;
             if ("java".equals(ext)) {
                 String fqn = determineClassName(t.toString());
                 if (fqn == null) {
@@ -3485,6 +3492,8 @@ public class Run extends CamelCommand {
     }
 
     private void configureLogging(Path baseDir, Properties profileProperties) 
throws Exception {
+        // the silent run of an export only writes the logging settings: it 
runs inside the export, which logs as its
+        // caller has configured (camel export configures logging before it 
exports)
         if (loggingOptions.logging) {
             // allow to configure individual logging levels in 
application.properties
             Properties prop = loadProfileProperties(baseDir);
@@ -3522,13 +3531,15 @@ public class Run extends CamelCommand {
                     }
                 }
             }
-            RuntimeUtil.configureLog(loggingOptions.loggingLevel, 
loggingOptions.loggingColor,
-                    loggingOptions.loggingJson, scriptRun, false, 
loggingOptions.loggingConfigPath,
-                    loggingOptions.loggingCategory, mcpStdioMode);
+            if (!exportRun) {
+                RuntimeUtil.configureLog(loggingOptions.loggingLevel, 
loggingOptions.loggingColor,
+                        loggingOptions.loggingJson, scriptRun, false, 
loggingOptions.loggingConfigPath,
+                        loggingOptions.loggingCategory, mcpStdioMode);
+            }
             writeSettings("loggingLevel", loggingOptions.loggingLevel);
             writeSettings("loggingColor", loggingOptions.loggingColor ? "true" 
: "false");
             writeSettings("loggingJson", loggingOptions.loggingJson ? "true" : 
"false");
-            if (!scriptRun) {
+            if (!scriptRun && !exportRun) {
                 // remember log file
                 String name = RuntimeUtil.getPid() + ".log";
                 final Path logDir = CommandLineHelper.getCamelDir();
@@ -3544,7 +3555,6 @@ public class Run extends CamelCommand {
             }
         } else {
             if (exportRun) {
-                RuntimeUtil.configureLog(loggingOptions.loggingLevel, false, 
false, false, true, null, null);
                 writeSettings("loggingLevel", loggingOptions.loggingLevel);
             } else {
                 RuntimeUtil.configureLog("off", false, false, false, false, 
null, null);
@@ -3562,7 +3572,7 @@ public class Run extends CamelCommand {
         Map<String, Object> model = new HashMap<>();
         model.put("Spec", filePath.toString());
         String content = TemplateHelper.processTemplate("rest-dsl.yaml.ftl", 
model);
-        Files.writeString(Paths.get(OPENAPI_GENERATED_FILE), content);
+        Files.writeString(Paths.get(openApiGeneratedFile()), content);
 
         // we need to include the spec on the classpath
         files.add(openapi);
@@ -3615,7 +3625,7 @@ public class Run extends CamelCommand {
         // flatten file
         name = FileUtil.stripPath(name);
 
-        if (OPENAPI_GENERATED_FILE.equals(name)) {
+        if (openApiGeneratedFile().equals(name)) {
             return false;
         }
         if ("pom.xml".equalsIgnoreCase(name)) {
@@ -3706,6 +3716,10 @@ public class Run extends CamelCommand {
         return ExportHelper.walkResourceDir(dirPath);
     }
 
+    private String openApiGeneratedFile() {
+        return workDir + "/" + OPENAPI_GENERATED_FILE;
+    }
+
     private void writeSettings(String key, String value) {
         try {
             // use java.util.Properties to ensure the value is escaped 
correctly
@@ -3714,7 +3728,7 @@ public class Run extends CamelCommand {
             StringWriter sw = new StringWriter();
             prop.store(sw, null);
 
-            Path runSettingsPath = 
CommandLineHelper.getWorkDir().resolve(RUN_SETTINGS_FILE);
+            Path runSettingsPath = workDir.resolve(RUN_SETTINGS_FILE);
 
             StringBuilder content = new StringBuilder();
             String[] lines = sw.toString().split(System.lineSeparator());
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/RunDependencyResolver.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/RunDependencyResolver.java
index 28adbc059961..a9756284e456 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/RunDependencyResolver.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/RunDependencyResolver.java
@@ -42,6 +42,7 @@ public class RunDependencyResolver implements 
DependencyResolver {
         Run run = new Run(main);
         // need to declare the profile to use for run
         run.exportBaseDir = request.exportBaseDir;
+        run.workDir = request.workDir;
         run.dependencies = request.dependencies;
         run.files = request.files;
         run.name = request.name;
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ExportApiTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ExportApiTest.java
new file mode 100644
index 000000000000..a2b7e4140914
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ExportApiTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.lang.ref.WeakReference;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.dsl.jbang.export.ExportContext;
+import org.apache.camel.dsl.jbang.export.ExportRequest;
+import org.apache.camel.dsl.jbang.export.ProjectExporter;
+import org.apache.camel.dsl.jbang.export.common.ExportHelper;
+import org.apache.camel.dsl.jbang.export.common.RuntimeType;
+import org.apache.camel.dsl.jbang.export.common.StringPrinter;
+import org.apache.camel.spi.CamelContextTracker;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+/**
+ * Exports through the Java API with the dependency resolver of 
camel-jbang-core (the silent run), as an application
+ * that embeds the export does: the export keeps to its work directory, leaves 
the logging and the system properties of
+ * the JVM as they are, and releases its silent run.
+ */
+class ExportApiTest {
+
+    @TempDir
+    Path dir;
+
+    private Path route;
+
+    @BeforeEach
+    void copyRoute() throws Exception {
+        route = dir.resolve("hello.camel.yaml");
+        Files.copy(Path.of("src/test/resources/hello.camel.yaml"), route);
+    }
+
+    @Test
+    void exportsWithoutSideEffects() throws Exception {
+        ExportRequest request = request(0);
+
+        Configuration logging = 
LoggerContext.getContext(false).getConfiguration();
+        Map<Object, Object> properties = new HashMap<>(System.getProperties());
+
+        StringPrinter printer = new StringPrinter();
+        int exit = new ProjectExporter(new 
ExportContext().withPrinter(printer)).export(request);
+
+        assertThat(exit).as(printer.getOutput()).isZero();
+        
assertThat(Files.readString(dir.resolve("export0/pom.xml"))).contains("<artifactId>camel-timer</artifactId>");
+        // the silent run writes its settings to the work directory of the 
request (the export tests in
+        // camel-jbang-export check that the current directory is left alone, 
which the forks of this module share)
+        
assertThat(request.workDir.resolve(ExportHelper.RUN_SETTINGS_FILE)).content()
+                .contains("camel.main.routesIncludePattern=", 
"hello.camel.yaml");
+
+        
assertThat(LoggerContext.getContext(false).getConfiguration()).isSameAs(logging);
+        assertThat(new 
HashMap<>(System.getProperties())).isEqualTo(properties);
+    }
+
+    @Test
+    void repeatedExportsReleaseTheirSilentRuns() throws Exception {
+        // the first export starts what stays for the JVM, such as the shared 
pools of the Maven resolver
+        export(0);
+        Set<Thread> threads = Thread.getAllStackTraces().keySet();
+
+        List<WeakReference<CamelContext>> contexts = new 
CopyOnWriteArrayList<>();
+        try (CamelContextTracker tracker = new CamelContextTracker() {
+            @Override
+            public void contextCreated(CamelContext camelContext) {
+                contexts.add(new WeakReference<>(camelContext));
+            }
+        }) {
+            tracker.open();
+            for (int i = 1; i <= 20; i++) {
+                export(i);
+            }
+        }
+
+        assertThat(contexts).hasSizeGreaterThanOrEqualTo(20);
+        // nothing holds on to the CamelContext of a silent run once its 
export is done
+        await().atMost(20, TimeUnit.SECONDS).untilAsserted(() -> {
+            System.gc();
+            assertThat(contexts).allSatisfy(ref -> 
assertThat(ref.get()).isNull());
+        });
+        assertThat(Thread.getAllStackTraces().keySet()).allSatisfy(t -> 
assertThat(threads).contains(t));
+    }
+
+    private ExportRequest request(int i) {
+        ExportRequest request = new ExportRequest();
+        request.files.add(route.toString());
+        request.runtime = RuntimeType.main;
+        request.exportDir = dir.resolve("export" + i).toString();
+        request.workDir = dir.resolve("work" + i);
+        request.mavenWrapper = false;
+        request.quiet = true;
+        return request;
+    }
+
+    private void export(int i) throws Exception {
+        StringPrinter printer = new StringPrinter();
+        assertThat(new ProjectExporter(new 
ExportContext().withPrinter(printer)).export(request(i)))
+                .as(printer.getOutput()).isZero();
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/CamelMainExporter.java
 
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/CamelMainExporter.java
index 6d4e1faed570..e2c964c235c8 100644
--- 
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/CamelMainExporter.java
+++ 
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/CamelMainExporter.java
@@ -31,7 +31,6 @@ import java.util.Set;
 
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.catalog.DefaultCamelCatalog;
-import org.apache.camel.dsl.jbang.export.common.CamelHome;
 import org.apache.camel.dsl.jbang.export.common.CamelJBangConstants;
 import org.apache.camel.dsl.jbang.export.common.ExportHelper;
 import org.apache.camel.dsl.jbang.export.common.PathUtils;
@@ -49,8 +48,6 @@ import static 
org.apache.camel.dsl.jbang.export.common.ExportHelper.exportPackag
  */
 public class CamelMainExporter extends Exporter {
 
-    protected static final String GROOVY_COMPILE_DIR = 
CamelHome.CAMEL_JBANG_WORK_DIR + "/compile/groovy";
-
     public CamelMainExporter(ExportRequest request, ExportContext context) {
         super(request, context);
         if (pomTemplateName == null) {
@@ -77,7 +74,7 @@ public class CamelMainExporter extends Exporter {
         String[] ids = gav.split(":");
 
         // the settings file has information what to export
-        Path settings = 
CamelHome.getWorkDir().resolve(ExportHelper.RUN_SETTINGS_FILE);
+        Path settings = runSettingsFile();
         if (mavenResolver.fresh() || !files.isEmpty() || 
!Files.exists(settings)) {
             // allow to automatic build
             printer().println("Generating fresh run data");
@@ -95,7 +92,7 @@ public class CamelMainExporter extends Exporter {
         Path profile = exportBaseDir.resolve("application.properties");
 
         // use a temporary work dir
-        Path buildDir = Path.of(BUILD_DIR);
+        Path buildDir = buildDir();
         PathUtils.deleteDirectory(buildDir);
         Files.createDirectories(buildDir);
 
@@ -198,10 +195,10 @@ public class CamelMainExporter extends Exporter {
             copyCamelWrapper();
         }
         if (docker) {
-            copyDockerFiles(BUILD_DIR);
+            copyDockerFiles(buildDir.toString());
         }
         String appJar = Paths.get("target", ids[1] + "-" + ids[2] + 
".jar").toString();
-        copyReadme(BUILD_DIR, appJar);
+        copyReadme(buildDir.toString(), appJar);
         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
@@ -392,7 +389,8 @@ public class CamelMainExporter extends Exporter {
 
     protected void copyGroovyPrecompiled(Path srcResourcesDir) throws 
Exception {
         // are there any pre-compiled groovy code
-        File gc = new File(GROOVY_COMPILE_DIR);
+        // the silent run compiles the groovy code into the work directory
+        File gc = workDir.resolve("compile/groovy").toFile();
         if (gc.exists() && gc.isDirectory()) {
             File[] files = gc.listFiles();
             if (files != null) {
diff --git 
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/DependencyResolver.java
 
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/DependencyResolver.java
index f81f9602fc1c..77891dd3eac3 100644
--- 
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/DependencyResolver.java
+++ 
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/DependencyResolver.java
@@ -16,7 +16,6 @@
  */
 package org.apache.camel.dsl.jbang.export;
 
-import org.apache.camel.dsl.jbang.export.common.CamelHome;
 import org.apache.camel.dsl.jbang.export.common.ExportHelper;
 
 /**
@@ -24,8 +23,8 @@ import org.apache.camel.dsl.jbang.export.common.ExportHelper;
  * <p>
  * The export boots the integration silently to learn what it uses: the 
components, languages, data formats and other
  * dependencies, and how the source files are classified. The resolver writes 
these as run settings to
- * {@link ExportHelper#RUN_SETTINGS_FILE} in the work directory ({@link 
CamelHome#getWorkDir()}), which the exporter
- * then reads.
+ * {@link ExportHelper#RUN_SETTINGS_FILE} in the work directory of the request 
({@link ExportRequest#workDir}), which
+ * the exporter then reads.
  * <p>
  * camel-jbang-core provides the implementation that boots the integration the 
same way as camel run does, and registers
  * it with the {@link java.util.ServiceLoader}.
diff --git 
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/ExportRequest.java
 
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/ExportRequest.java
index bf30c49be942..93294008077e 100644
--- 
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/ExportRequest.java
+++ 
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/ExportRequest.java
@@ -20,6 +20,7 @@ import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
 
+import org.apache.camel.dsl.jbang.export.common.CamelHome;
 import org.apache.camel.dsl.jbang.export.common.MavenResolver;
 import org.apache.camel.dsl.jbang.export.common.QuarkusPlatform;
 import org.apache.camel.dsl.jbang.export.common.RuntimeType;
@@ -201,6 +202,14 @@ public class ExportRequest {
      */
     public String exportDir = ".";
 
+    /**
+     * The working directory of the export: the run settings of the dependency 
resolution and the project being built
+     * are kept there, and the directory is deleted and created again on each 
export. The default is the .camel-jbang
+     * directory in the current directory, as with camel export; use a 
directory of its own for each export that runs at
+     * the same time.
+     */
+    public Path workDir = CamelHome.getWorkDir();
+
     /**
      * If exporting to current directory (default) then all existing files are 
preserved. Enabling this option will
      * force cleaning current directory including all sub dirs (use this with 
care).
@@ -352,6 +361,7 @@ public class ExportRequest {
         this.consoleForRun = other.consoleForRun;
         this.console = other.console;
         this.exportDir = other.exportDir;
+        this.workDir = other.workDir;
         this.cleanExportDir = other.cleanExportDir;
         this.yes = other.yes;
         this.loggingLevel = other.loggingLevel;
diff --git 
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/Exporter.java
 
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/Exporter.java
index 4c39800acaa0..b00a60127630 100644
--- 
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/Exporter.java
+++ 
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/Exporter.java
@@ -52,7 +52,6 @@ import java.util.stream.Stream;
 
 import freemarker.template.TemplateNotFoundException;
 import org.apache.camel.dsl.jbang.export.catalog.KameletCatalogHelper;
-import org.apache.camel.dsl.jbang.export.common.CamelHome;
 import org.apache.camel.dsl.jbang.export.common.ExportHelper;
 import org.apache.camel.dsl.jbang.export.common.GenAiDependencyHelper;
 import org.apache.camel.dsl.jbang.export.common.PluginExporter;
@@ -78,8 +77,6 @@ import static 
org.apache.camel.dsl.jbang.export.common.CamelJBangConstants.*;
  */
 public abstract class Exporter extends ExportRequest {
 
-    protected static final String BUILD_DIR = CamelHome.CAMEL_JBANG_WORK_DIR + 
"/work";
-
     protected static final String[] SETTINGS_PROP_SOURCE_KEYS = new String[] {
             "camel.main.routesIncludePattern",
             "camel.component.properties.location",
@@ -96,13 +93,10 @@ public abstract class Exporter extends ExportRequest {
     private static final Pattern PACKAGE_PATTERN = Pattern.compile(
             "^\\s*package\\s+([a-zA-Z][.\\w]*)\\s*;.*$", Pattern.MULTILINE);
 
-    private static final String EXTRA_REPOS_PROPERTY = "camel.extra.repos";
-    private static final String EXTRA_REPOS_DEFAULT_VALUE_PROPERTY = 
"camel.default.extra.repos.default.value";
-
     private static final Set<String> EXCLUDED_GROUP_IDS = 
Set.of("org.fusesource.jansi", "org.apache.logging.log4j");
 
     private Printer quietPrinter;
-    // Map to store deferred symbolic links: target path in BUILD_DIR → 
original source path
+    // Map to store deferred symbolic links: target path in the build dir → 
original source path
     private Map<Path, Path> deferredSymlinks = new HashMap<>();
 
     protected final ExportContext exportContext;
@@ -112,6 +106,20 @@ public abstract class Exporter extends ExportRequest {
         this.exportContext = context;
     }
 
+    /**
+     * The directory in the work directory where the project is built, before 
it is copied to the export directory.
+     */
+    protected Path buildDir() {
+        return workDir.resolve("work");
+    }
+
+    /**
+     * The run settings in the work directory, which the dependency resolver 
writes.
+     */
+    protected Path runSettingsFile() {
+        return workDir.resolve(ExportHelper.RUN_SETTINGS_FILE);
+    }
+
     /**
      * Builds a list of repository data maps from a comma-separated repos 
string, for use with FreeMarker templates.
      */
@@ -974,11 +982,11 @@ public abstract class Exporter extends ExportRequest {
     }
 
     protected void copyMavenWrapper() throws Exception {
-        Path wrapperPath = Paths.get(BUILD_DIR, ".mvn/wrapper");
+        Path wrapperPath = buildDir().resolve(".mvn/wrapper");
         Files.createDirectories(wrapperPath);
         // copy files
-        Path mvnwPath = Paths.get(BUILD_DIR, "mvnw");
-        Path mvnwCmdPath = Paths.get(BUILD_DIR, "mvnw.cmd");
+        Path mvnwPath = buildDir().resolve("mvnw");
+        Path mvnwCmdPath = buildDir().resolve("mvnw.cmd");
         Path wrapperJarPath = wrapperPath.resolve("maven-wrapper.jar");
         Path wrapperPropsPath = 
wrapperPath.resolve("maven-wrapper.properties");
 
@@ -1002,7 +1010,7 @@ public abstract class Exporter extends ExportRequest {
     }
 
     protected void copyCamelWrapper() throws Exception {
-        Path camelDir = Paths.get(BUILD_DIR, ".camel");
+        Path camelDir = buildDir().resolve(".camel");
         Files.createDirectories(camelDir);
 
         // write camel-wrapper.properties with the export's camel version
@@ -1017,8 +1025,8 @@ public abstract class Exporter extends ExportRequest {
         Files.writeString(camelDir.resolve("camel-wrapper.properties"), 
content);
 
         // copy wrapper scripts
-        Path camelPath = Paths.get(BUILD_DIR, "camel");
-        Path camelCmdPath = Paths.get(BUILD_DIR, "camel.cmd");
+        Path camelPath = buildDir().resolve("camel");
+        Path camelCmdPath = buildDir().resolve("camel.cmd");
         try (InputStream is = 
Exporter.class.getClassLoader().getResourceAsStream("camel-wrapper/camelw")) {
             Files.copy(is, camelPath, StandardCopyOption.REPLACE_EXISTING);
         }
@@ -1047,10 +1055,6 @@ public abstract class Exporter extends ExportRequest {
         Set<String> answer = new LinkedHashSet<>();
 
         String propRepositories = prop.getProperty(REPOS);
-        if (propRepositories == null) {
-            // fallback to system property
-            propRepositories = System.getProperty(REPOS);
-        }
         if (propRepositories != null) {
             Collections.addAll(answer, propRepositories.split("\\s*,\\s*"));
         }
@@ -1073,9 +1077,9 @@ public abstract class Exporter extends ExportRequest {
             Collections.addAll(answer, 
this.mavenResolver.repos().split("\\s*,\\s*"));
         }
 
-        // include extra repos from system property
-        String extraRepos = System.getProperty(EXTRA_REPOS_PROPERTY,
-                System.getProperty(EXTRA_REPOS_DEFAULT_VALUE_PROPERTY));
+        // include the extra default repositories the downloads use (the 
camel.extra.repos system property unless the
+        // request sets them)
+        String extraRepos = mavenResolver.downloader().getExtraRepos();
         if (extraRepos != null && !extraRepos.isBlank()) {
             for (String r : extraRepos.split("\\s*,\\s*")) {
                 if (!r.isBlank()) {
@@ -1246,7 +1250,7 @@ public abstract class Exporter extends ExportRequest {
 
     /**
      * Creates symbolic links for files that were deferred during the copy 
phase. This method should be called after
-     * copying BUILD_DIR to exportDir and before deleting BUILD_DIR.
+     * copying the build dir to exportDir and before deleting the build dir.
      *
      * @param  buildDir  the temporary build directory
      * @param  exportDir the final export directory
@@ -1374,7 +1378,7 @@ public abstract class Exporter extends ExportRequest {
     protected void copyLocalLibDependencies(Set<String> deps) throws Exception 
{
         for (String d : deps) {
             if (d.startsWith("lib:")) {
-                Path libDirPath = Paths.get(BUILD_DIR, "lib");
+                Path libDirPath = buildDir().resolve("lib");
                 Files.createDirectories(libDirPath);
                 String n = d.substring(4);
                 Path sourcePath = Paths.get(n);
@@ -1387,7 +1391,7 @@ public abstract class Exporter extends ExportRequest {
     protected void copyAgentDependencies(Set<String> deps) throws Exception {
         for (String d : deps) {
             if (d.startsWith("agent:")) {
-                Path libDirPath = Paths.get(BUILD_DIR, "agent");
+                Path libDirPath = buildDir().resolve("agent");
                 Files.createDirectories(libDirPath);
                 String n = d.substring(6);
                 MavenGav gav = MavenGav.parseGav(n);
@@ -1401,7 +1405,7 @@ public abstract class Exporter extends ExportRequest {
             List<MavenArtifact> artifacts = 
mavenResolver.downloader().resolveArtifacts(
                     List.of(gav.toString()), Set.of(), true, 
gav.getVersion().contains("SNAPSHOT"));
             for (MavenArtifact artifact : artifacts) {
-                Path target = Paths.get(BUILD_DIR, "agent", 
artifact.getFile().getName());
+                Path target = 
buildDir().resolve("agent").resolve(artifact.getFile().getName());
                 if (Files.exists(target) || 
EXCLUDED_GROUP_IDS.contains(artifact.getGav().getGroupId())) {
                     continue;
                 }
diff --git 
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/ProjectExporter.java
 
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/ProjectExporter.java
index ae9a04623606..19eba96174b0 100644
--- 
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/ProjectExporter.java
+++ 
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/ProjectExporter.java
@@ -27,7 +27,6 @@ import 
org.apache.camel.dsl.jbang.export.common.CamelJBangConstants;
 import org.apache.camel.dsl.jbang.export.common.ExportHelper;
 import org.apache.camel.dsl.jbang.export.common.MavenResolver;
 import org.apache.camel.dsl.jbang.export.common.Printer;
-import org.apache.camel.dsl.jbang.export.common.PropertyResolver;
 import org.apache.camel.dsl.jbang.export.common.QuarkusPlatform;
 import org.apache.camel.dsl.jbang.export.common.RuntimeType;
 import org.apache.camel.dsl.jbang.export.common.RuntimeUtil;
@@ -111,8 +110,9 @@ public class ProjectExporter {
 
     /**
      * Prepares the request: a single directory as the file to export is the 
directory of the files to export, the
-     * application.properties (and the one of the profile) of that directory 
are included, and the options they (and the
-     * system properties) configure are set on the request.
+     * application.properties (and the one of the profile) of that directory 
are included, and the options they
+     * configure are set on the request. System properties are not read: the 
caller sets what they configure on the
+     * request (camel export does so after the preparation).
      *
      * @param  request the request, which is completed
      * @return         the base directory of the files to export
@@ -154,9 +154,6 @@ public class ProjectExporter {
             doLoadAndInitProfileProperties(request, 
baseDir.resolve("application-" + request.profile + ".properties"));
         }
 
-        // property overrides from system properties for supported properties
-        overrideFromSystemProperties(request);
-
         return baseDir;
     }
 
@@ -296,13 +293,4 @@ public class ProjectExporter {
             request.excludes = 
RuntimeUtil.getCommaSeparatedPropertyAsList(props, EXCLUDES, request.excludes);
         }
     }
-
-    /**
-     * For supported fields prefer the values from system properties if they 
are defined.
-     */
-    private static void overrideFromSystemProperties(ExportRequest request) {
-        request.quarkusPlatform = QuarkusPlatform.of(System.getProperties(), 
request.quarkusPlatform);
-        request.camelSpringBootVersion
-                = 
PropertyResolver.fromSystemProperty(CAMEL_SPRING_BOOT_VERSION, () -> 
request.camelSpringBootVersion);
-    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/QuarkusExporter.java
 
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/QuarkusExporter.java
index 5ae19d501748..a295d8d51452 100644
--- 
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/QuarkusExporter.java
+++ 
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/QuarkusExporter.java
@@ -46,7 +46,6 @@ import org.w3c.dom.Document;
 
 import org.xml.sax.SAXException;
 
-import org.apache.camel.dsl.jbang.export.common.CamelHome;
 import org.apache.camel.dsl.jbang.export.common.ExportHelper;
 import org.apache.camel.dsl.jbang.export.common.PathUtils;
 import org.apache.camel.dsl.jbang.export.common.QuarkusHelper;
@@ -100,7 +99,7 @@ public class QuarkusExporter extends Exporter {
         Path profile = exportBaseDir.resolve("application.properties");
 
         // the settings file has information what to export
-        Path settings = 
CamelHome.getWorkDir().resolve(ExportHelper.RUN_SETTINGS_FILE);
+        Path settings = runSettingsFile();
         if (mavenResolver.fresh() || !files.isEmpty() || 
!Files.exists(settings)) {
             // allow to automatic build
             printer().println("Generating fresh run data");
@@ -115,7 +114,7 @@ public class QuarkusExporter extends Exporter {
         printer().println("Exporting as Quarkus project to: " + exportDir);
 
         // use a temporary work dir
-        Path buildDir = Path.of(BUILD_DIR);
+        Path buildDir = buildDir();
         PathUtils.deleteDirectory(buildDir);
         Files.createDirectories(buildDir);
 
@@ -172,7 +171,7 @@ public class QuarkusExporter extends Exporter {
         writeDevProfileProperties(srcResourcesDir, devProfile);
         // copy docker files
         if (docker) {
-            copyDockerFiles(BUILD_DIR);
+            copyDockerFiles(buildDir.toString());
         }
         String appJar;
         if ("fast-jar".equals(quarkusPackageType)) {
@@ -180,7 +179,7 @@ public class QuarkusExporter extends Exporter {
         } else {
             appJar = "target" + File.separator + ids[1] + "-" + ids[2] + 
".jar";
         }
-        copyReadme(BUILD_DIR, appJar);
+        copyReadme(buildDir.toString(), appJar);
         // gather dependencies
         Set<String> deps = resolveDependencies(settings, profile);
         // copy local lib JARs
@@ -295,7 +294,6 @@ public class QuarkusExporter extends Exporter {
             fileName = StringHelper.after(fileName, ":");
         }
         fileName = FileUtil.stripPath(fileName);
-        fileName = fileName.replace(CamelHome.CAMEL_JBANG_WORK_DIR + "/", "");
         return fileName;
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/SpringBootExporter.java
 
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/SpringBootExporter.java
index d0439d8ec325..1a354cf9209a 100644
--- 
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/SpringBootExporter.java
+++ 
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/SpringBootExporter.java
@@ -35,7 +35,6 @@ import java.util.Properties;
 import java.util.Set;
 
 import org.apache.camel.catalog.CamelCatalog;
-import org.apache.camel.dsl.jbang.export.common.CamelHome;
 import org.apache.camel.dsl.jbang.export.common.CamelJBangConstants;
 import org.apache.camel.dsl.jbang.export.common.CatalogLoader;
 import org.apache.camel.dsl.jbang.export.common.ExportHelper;
@@ -86,7 +85,7 @@ public class SpringBootExporter extends Exporter {
         Path profile = exportBaseDir.resolve("application.properties");
 
         // the settings file has information what to export
-        Path settings = 
CamelHome.getWorkDir().resolve(ExportHelper.RUN_SETTINGS_FILE);
+        Path settings = runSettingsFile();
         if (mavenResolver.fresh() || !files.isEmpty() || 
!Files.exists(settings)) {
             // allow to automatic build
             printer().println("Generating fresh run data");
@@ -101,7 +100,7 @@ public class SpringBootExporter extends Exporter {
         printer().println("Exporting as Spring Boot project to: " + exportDir);
 
         // use a temporary work dir
-        Path buildDir = Path.of(BUILD_DIR);
+        Path buildDir = buildDir();
         PathUtils.deleteDirectory(buildDir);
         Files.createDirectories(buildDir);
 
@@ -195,10 +194,10 @@ public class SpringBootExporter extends Exporter {
             copyCamelWrapper();
         }
         if (docker) {
-            copyDockerFiles(BUILD_DIR);
+            copyDockerFiles(buildDir.toString());
         }
         String appJar = "target" + File.separator + ids[1] + "-" + ids[2] + 
".jar";
-        copyReadme(BUILD_DIR, appJar);
+        copyReadme(buildDir.toString(), appJar);
         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
@@ -227,7 +226,7 @@ public class SpringBootExporter extends Exporter {
                 ? camelSpringBootVersion : (camelVersion == null ? 
RuntimeType.main.version() : camelVersion);
         String repos = getMavenRepositories(settings, prop, sbVersion);
 
-        CamelCatalog catalog = CatalogLoader.loadSpringBootCatalog(repos, 
sbVersion, mavenResolver.download());
+        CamelCatalog catalog = CatalogLoader.loadSpringBootCatalog(repos, 
sbVersion, sbVersion, null, mavenResolver.download());
         if (ObjectHelper.isEmpty(camelVersion)) {
             camelVersion = catalog.getLoadedVersion();
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/catalog/KameletCatalogHelper.java
 
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/catalog/KameletCatalogHelper.java
index 07cafb09bcb9..f94fe9c35e0f 100644
--- 
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/catalog/KameletCatalogHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/catalog/KameletCatalogHelper.java
@@ -187,7 +187,12 @@ public final class KameletCatalogHelper {
             Map<String, Object> answer = (Map<String, Object>) 
ObjectHelper.invokeMethod(m, catalog);
             if (release && answer != null) {
                 answer = Collections.unmodifiableMap(answer);
-                KAMELETS.put(key, answer);
+                // another thread may have loaded it at the same time (such as 
exports that run in parallel): the
+                // callers share the first one
+                Map<String, Object> existing = KAMELETS.putIfAbsent(key, 
answer);
+                if (existing != null) {
+                    answer = existing;
+                }
             }
             return answer;
         } finally {
diff --git 
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/common/CatalogLoader.java
 
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/common/CatalogLoader.java
index a0ec03b078ee..489b6be00c01 100644
--- 
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/common/CatalogLoader.java
+++ 
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/common/CatalogLoader.java
@@ -112,6 +112,25 @@ public final class CatalogLoader {
     public static CamelCatalog loadSpringBootCatalog(
             String repos, String version, String springBootGroupId, boolean 
download)
             throws Exception {
+        final String camelVersion = version;
+        return loadSpringBootCatalog(repos, version,
+                
PropertyResolver.fromSystemProperty(CamelJBangConstants.CAMEL_SPRING_BOOT_VERSION,
 () -> camelVersion),
+                springBootGroupId, download);
+    }
+
+    /**
+     * Loads the catalog of Camel Spring Boot.
+     *
+     * @param repos             additional Maven repositories
+     * @param version           the version of the Camel catalog (the default 
when null)
+     * @param providerVersion   the version of 
camel-catalog-provider-springboot (the version of the Camel catalog when
+     *                          null)
+     * @param springBootGroupId the groupId of 
camel-catalog-provider-springboot (org.apache.camel.springboot when null)
+     * @param download          whether to download from remote repositories
+     */
+    public static CamelCatalog loadSpringBootCatalog(
+            String repos, String version, String providerVersion, String 
springBootGroupId, boolean download)
+            throws Exception {
         CamelCatalog answer = new DefaultCamelCatalog();
         if (version == null) {
             version = answer.getCatalogVersion();
@@ -144,9 +163,8 @@ public final class CatalogLoader {
                 throw new IOException("Cannot download 
org.apache.camel:camel-catalog:" + camelCatalogVersion);
             }
 
-            final String camelVersion = version;
-            ma = downloader.downloadArtifact(springBootGroupId, 
"camel-catalog-provider-springboot",
-                    
PropertyResolver.fromSystemProperty(CamelJBangConstants.CAMEL_SPRING_BOOT_VERSION,
 () -> camelVersion));
+            final String sbVersion = providerVersion != null ? providerVersion 
: version;
+            ma = downloader.downloadArtifact(springBootGroupId, 
"camel-catalog-provider-springboot", sbVersion);
             if (ma != null) {
                 cl.addFile(ma.getFile());
             } else {
@@ -158,8 +176,6 @@ public final class CatalogLoader {
             try {
                 clazz = (Class<RuntimeProvider>) 
cl.loadClass(SPRING_BOOT_CATALOG_PROVIDER);
             } catch (UnsupportedClassVersionError e) {
-                String sbVersion = PropertyResolver.fromSystemProperty(
-                        CamelJBangConstants.CAMEL_SPRING_BOOT_VERSION, () -> 
camelVersion);
                 throw new IOException(
                         "Cannot load camel-catalog-provider-springboot:" + 
sbVersion
                                       + " on Java " + Runtime.version()
diff --git 
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/common/MavenResolver.java
 
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/common/MavenResolver.java
index 14365abe4380..923c9bbdb471 100644
--- 
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/common/MavenResolver.java
+++ 
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/common/MavenResolver.java
@@ -23,14 +23,15 @@ import org.apache.camel.tooling.maven.MavenDownloader;
 import org.apache.camel.tooling.maven.MavenDownloaderImpl;
 
 /**
- * How Maven artifacts are downloaded: whether downloading is allowed, the 
additional repositories, and whether to use
- * fresh (non-cached) resources.
+ * How Maven artifacts are downloaded: whether downloading is allowed, the 
additional repositories, the extra default
+ * repositories, and whether to use fresh (non-cached) resources.
  */
 public class MavenResolver implements MavenResolverMixinSpec {
 
     private final boolean download;
     private final String repos;
     private final boolean fresh;
+    private final String extraRepos;
 
     private volatile MavenDownloader downloader;
     private final Object downloaderLock = new Object();
@@ -40,9 +41,23 @@ public class MavenResolver implements MavenResolverMixinSpec 
{
     }
 
     public MavenResolver(boolean download, String repos, boolean fresh) {
+        this(download, repos, fresh, null);
+    }
+
+    /**
+     * @param download   whether downloading is allowed
+     * @param repos      the additional repositories
+     * @param fresh      whether to use fresh (non-cached) resources
+     * @param extraRepos the extra default repositories (comma-separated URLs 
or id=url pairs, empty for none), used for
+     *                   the downloads and in the pom.xml of the exported 
project; null for the ones of the
+     *                   camel.extra.repos system property, else the 
camel.default.extra.repos.default.value system
+     *                   property
+     */
+    public MavenResolver(boolean download, String repos, boolean fresh, String 
extraRepos) {
         this.download = download;
         this.repos = repos;
         this.fresh = fresh;
+        this.extraRepos = extraRepos;
     }
 
     public static MavenResolver of(Properties props, MavenResolverMixinSpec 
fallback) {
@@ -50,7 +65,7 @@ public class MavenResolver implements MavenResolverMixinSpec {
                 = 
Boolean.parseBoolean(props.getProperty(CamelJBangConstants.DOWNLOAD, 
String.valueOf(fallback.download())));
         String repos = props.getProperty(CamelJBangConstants.REPOS, 
fallback.repos());
         boolean fresh = 
Boolean.parseBoolean(props.getProperty(CamelJBangConstants.FRESH, 
String.valueOf(fallback.fresh())));
-        return new MavenResolver(download, repos, fresh);
+        return new MavenResolver(download, repos, fresh, 
fallback.extraRepos());
     }
 
     /** You would typically want to use the properly configured {@link 
#downloader()} instead of this value */
@@ -70,6 +85,11 @@ public class MavenResolver implements MavenResolverMixinSpec 
{
         return fresh;
     }
 
+    @Override
+    public String extraRepos() {
+        return extraRepos;
+    }
+
     /**
      * @return a lazily and thread-safe initialized {@link MavenDownloader} 
configured with {@link #repos}
      */
@@ -82,6 +102,7 @@ public class MavenResolver implements MavenResolverMixinSpec 
{
                     if (repos != null) {
                         d.setRepos(repos);
                     }
+                    d.setExtraRepos(extraRepos);
                     d.setOffline(!download);
                     d.setFresh(fresh);
                     d.build();
diff --git 
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/common/MavenResolverMixinSpec.java
 
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/common/MavenResolverMixinSpec.java
index 69a63cdbca21..9a730762aef2 100644
--- 
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/common/MavenResolverMixinSpec.java
+++ 
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/common/MavenResolverMixinSpec.java
@@ -22,4 +22,12 @@ public interface MavenResolverMixinSpec {
     boolean download();
 
     boolean fresh();
+
+    /**
+     * The extra default repositories (comma-separated URLs or id=url pairs, 
empty for none), or null for the ones of
+     * the camel.extra.repos system property.
+     */
+    default String extraRepos() {
+        return null;
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/common/QuarkusPlatform.java
 
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/common/QuarkusPlatform.java
index 29b3ed652527..fe54fdc1e1f6 100644
--- 
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/common/QuarkusPlatform.java
+++ 
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/common/QuarkusPlatform.java
@@ -120,19 +120,11 @@ public class QuarkusPlatform implements 
QuarkusPlatformMixinSpec {
     }
 
     /**
-     * The base URI of the Quarkus Extension Registry: the configured one, 
else the one of the
-     * camel.jbang.quarkus.platform.url system property (without its 
"/client/platforms" suffix), else
+     * The base URI of the Quarkus Extension Registry: the given one, else
      * {@link RuntimeType#QUARKUS_EXTENSION_REGISTRY_BASE_URL}; without a 
trailing slash.
      */
-    public static String resolveExtensionRegistryBaseUri(String 
quarkusExtensionRegistryBaseUri) {
+    public static String extensionRegistryBaseUri(String 
quarkusExtensionRegistryBaseUri) {
         String result = quarkusExtensionRegistryBaseUri;
-        if (result == null) {
-            result = 
System.getProperty(QuarkusHelper.QUARKUS_PLATFORM_URL_PROPERTY);
-            final String suffix = "/client/platforms";
-            if (result != null && result.endsWith(suffix)) {
-                result = result.substring(0, result.length() - 
suffix.length());
-            }
-        }
         if (result == null) {
             result = RuntimeType.QUARKUS_EXTENSION_REGISTRY_BASE_URL;
         }
@@ -166,6 +158,6 @@ public class QuarkusPlatform implements 
QuarkusPlatformMixinSpec {
 
     @Override
     public String quarkusExtensionRegistryBaseUri() {
-        return 
resolveExtensionRegistryBaseUri(quarkusExtensionRegistryBaseUri);
+        return extensionRegistryBaseUri(quarkusExtensionRegistryBaseUri);
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-export/src/test/java/org/apache/camel/dsl/jbang/export/ProjectExporterTest.java
 
b/dsl/camel-jbang/camel-jbang-export/src/test/java/org/apache/camel/dsl/jbang/export/ProjectExporterTest.java
index 52ab0a150872..5771d3cabb4d 100644
--- 
a/dsl/camel-jbang/camel-jbang-export/src/test/java/org/apache/camel/dsl/jbang/export/ProjectExporterTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-export/src/test/java/org/apache/camel/dsl/jbang/export/ProjectExporterTest.java
@@ -19,14 +19,23 @@ package org.apache.camel.dsl.jbang.export;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.stream.Stream;
 
 import org.apache.camel.dsl.jbang.export.common.CamelHome;
 import org.apache.camel.dsl.jbang.export.common.ExportHelper;
+import org.apache.camel.dsl.jbang.export.common.MavenResolver;
+import org.apache.camel.dsl.jbang.export.common.QuarkusPlatform;
 import org.apache.camel.dsl.jbang.export.common.RuntimeType;
 import org.apache.camel.dsl.jbang.export.common.StringPrinter;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
+import org.junitpioneer.jupiter.SetSystemProperty;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -39,11 +48,11 @@ class ProjectExporterTest {
     @TempDir
     Path dir;
 
-    private final List<ExportRequest> resolved = new ArrayList<>();
+    private final List<ExportRequest> resolved = 
Collections.synchronizedList(new ArrayList<>());
 
     private final DependencyResolver resolver = request -> {
         resolved.add(request);
-        Path settings = 
CamelHome.getWorkDir().resolve(ExportHelper.RUN_SETTINGS_FILE);
+        Path settings = 
request.workDir.resolve(ExportHelper.RUN_SETTINGS_FILE);
         Files.createDirectories(settings.getParent());
         Files.writeString(settings, """
                 camel.main.routesIncludePattern=file:%s
@@ -55,21 +64,9 @@ class ProjectExporterTest {
 
     @Test
     void exportsCamelMainProject() throws Exception {
-        Path route = dir.resolve("hello.camel.yaml");
-        Files.writeString(route, """
-                - from:
-                    uri: timer:hello
-                    steps:
-                      - to: log:hello
-                """);
+        Path route = writeRoute("hello");
         Path exportDir = dir.resolve("export");
-
-        ExportRequest request = new ExportRequest();
-        request.files.add(route.toString());
-        request.runtime = RuntimeType.main;
-        request.exportDir = exportDir.toString();
-        request.mavenWrapper = false;
-        request.quiet = true;
+        ExportRequest request = request(route, exportDir);
 
         StringPrinter printer = new StringPrinter();
         ExportContext context = new 
ExportContext().withPrinter(printer).withDependencyResolver(resolver);
@@ -85,6 +82,42 @@ class ProjectExporterTest {
         assertThat(pom).contains("<artifactId>hello</artifactId>", 
"<artifactId>camel-timer</artifactId>",
                 "<artifactId>camel-log</artifactId>");
         
assertThat(exportDir.resolve("src/main/resources/camel/hello.camel.yaml")).hasSameTextualContentAs(route);
+        // the run settings and the project are built in the work directory of 
the request
+        
assertThat(request.workDir.resolve(ExportHelper.RUN_SETTINGS_FILE)).exists();
+    }
+
+    @Test
+    void exportsInParallelWithWorkDirsOfTheirOwn() throws Exception {
+        List<String> before = listWorkDir();
+
+        int count = 4;
+        List<Callable<Integer>> tasks = new ArrayList<>();
+        for (int i = 0; i < count; i++) {
+            Path route = writeRoute("hello" + i);
+            ExportRequest request = request(route, dir.resolve("export" + i));
+            tasks.add(() -> new ProjectExporter(
+                    new ExportContext().withPrinter(new 
StringPrinter()).withDependencyResolver(resolver))
+                    .export(request));
+        }
+        ExecutorService executor = Executors.newFixedThreadPool(count);
+        try {
+            for (Future<Integer> exit : executor.invokeAll(tasks)) {
+                assertThat(exit.get()).isZero();
+            }
+        } finally {
+            executor.shutdownNow();
+        }
+
+        for (int i = 0; i < count; i++) {
+            Path exportDir = dir.resolve("export" + i);
+            String route = "hello" + i + ".camel.yaml";
+            
assertThat(Files.readString(exportDir.resolve("pom.xml"))).contains("<artifactId>hello"
 + i + "</artifactId>");
+            // each project has its own route only
+            
assertThat(exportDir.resolve("src/main/resources/camel")).isDirectoryContaining("glob:**/"
 + route)
+                    .isDirectoryNotContaining(p -> 
!p.getFileName().toString().equals(route));
+        }
+        // nothing is written to the .camel-jbang directory of the current 
directory
+        assertThat(listWorkDir()).isEqualTo(before);
     }
 
     @Test
@@ -116,6 +149,99 @@ class ProjectExporterTest {
         assertThat(resolved).isEmpty();
     }
 
+    private ExportContext exportContext() {
+        return new ExportContext().withPrinter(new 
StringPrinter()).withDependencyResolver(resolver);
+    }
+
+    private Path writeRoute(String name) throws Exception {
+        Path route = dir.resolve(name + ".camel.yaml");
+        Files.writeString(route, """
+                - from:
+                    uri: timer:%s
+                    steps:
+                      - to: log:%s
+                """.formatted(name, name));
+        return route;
+    }
+
+    private ExportRequest request(Path route, Path exportDir) {
+        ExportRequest request = new ExportRequest();
+        request.files.add(route.toString());
+        request.runtime = RuntimeType.main;
+        request.exportDir = exportDir.toString();
+        request.workDir = exportDir.resolveSibling(exportDir.getFileName() + 
"-work");
+        request.mavenWrapper = false;
+        request.quiet = true;
+        return request;
+    }
+
+    private static List<String> listWorkDir() throws Exception {
+        Path work = CamelHome.getWorkDir();
+        if (!Files.exists(work)) {
+            return List.of();
+        }
+        try (Stream<Path> files = Files.walk(work)) {
+            return files.map(p -> p + ":" + 
p.toFile().lastModified()).sorted().toList();
+        }
+    }
+
+    @Test
+    @SetSystemProperty(key = "camel.extra.repos", value = 
"sysprop=https://sysprop.example.com/maven2";)
+    void extraReposOfTheRequestInThePom() throws Exception {
+        Path exportDir = dir.resolve("export");
+        ExportRequest request = request(writeRoute("hello"), exportDir);
+        request.mavenResolver = new MavenResolver(true, null, false, 
"explicit=https://explicit.example.com/maven2";);
+
+        assertThat(new 
ProjectExporter(exportContext()).export(request)).isZero();
+
+        // the extra repositories of the request win over the system property
+        assertThat(Files.readString(exportDir.resolve("pom.xml")))
+                .contains("<url>https://explicit.example.com/maven2</url>")
+                .doesNotContain("sysprop.example.com");
+    }
+
+    @Test
+    @SetSystemProperty(key = "camel.extra.repos", value = 
"sysprop=https://sysprop.example.com/maven2";)
+    void noExtraRepos() throws Exception {
+        Path exportDir = dir.resolve("export");
+        ExportRequest request = request(writeRoute("hello"), exportDir);
+        request.mavenResolver = new MavenResolver(true, null, false, "");
+
+        assertThat(new 
ProjectExporter(exportContext()).export(request)).isZero();
+
+        
assertThat(Files.readString(exportDir.resolve("pom.xml"))).doesNotContain("sysprop.example.com");
+    }
+
+    @Test
+    @SetSystemProperty(key = "camel.extra.repos", value = 
"sysprop=https://sysprop.example.com/maven2";)
+    void extraReposOfTheSystemProperty() throws Exception {
+        Path exportDir = dir.resolve("export");
+        ExportRequest request = request(writeRoute("hello"), exportDir);
+
+        assertThat(new 
ProjectExporter(exportContext()).export(request)).isZero();
+
+        // as before the request had extra repositories
+        
assertThat(Files.readString(exportDir.resolve("pom.xml"))).contains("<url>https://sysprop.example.com/maven2</url>");
+    }
+
+    @Test
+    @SetSystemProperty(key = "camel.jbang.camelSpringBootVersion", value = 
"9.9.9")
+    @SetSystemProperty(key = "camel.jbang.quarkusVersion", value = "9.9.9")
+    @SetSystemProperty(key = "camel.jbang.quarkus.platform.url", value = 
"https://registry.example.com/client/platforms";)
+    void prepareReadsNoSystemProperties() throws Exception {
+        ExportRequest request = new ExportRequest();
+        request.exportBaseDir = dir;
+
+        new ProjectExporter(new ExportContext().withPrinter(new 
StringPrinter()).withDependencyResolver(resolver))
+                .prepare(request);
+
+        // the CLI sets what the system properties configure on the request
+        assertThat(request.camelSpringBootVersion).isNull();
+        assertThat(request.quarkusPlatform.quarkusVersion()).isNull();
+        assertThat(request.quarkusPlatform.quarkusExtensionRegistryBaseUri())
+                
.isEqualTo(QuarkusPlatform.extensionRegistryBaseUri(RuntimeType.QUARKUS_EXTENSION_REGISTRY_BASE_URL));
+    }
+
     @Test
     void projectNameAndVersion() {
         assertThat(ProjectExporter.projectName("foo", "com.acme:bar:2.0", 
List.of("baz.yaml"))).isEqualTo("foo");
diff --git 
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/KameletMain.java 
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/KameletMain.java
index 7e25887ac230..32df6dbcebca 100644
--- 
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/KameletMain.java
+++ 
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/KameletMain.java
@@ -134,6 +134,7 @@ public class KameletMain extends MainCommandLineSupport {
     private boolean download = true;
     private boolean packageScanJars;
     private String repositories;
+    private String extraRepositories;
     private boolean fresh;
     private boolean verbose;
     private String mavenSettings;
@@ -288,6 +289,19 @@ public class KameletMain extends MainCommandLineSupport {
         this.repositories = repositories;
     }
 
+    public String getExtraRepositories() {
+        return extraRepositories;
+    }
+
+    /**
+     * The extra default maven repositories for download on-demand 
(comma-separated URLs or id=url pairs, empty for
+     * none). When not set, they are the ones of the camel.extra.repos system 
property, else the
+     * camel.default.extra.repos.default.value system property.
+     */
+    public void setExtraRepositories(String extraRepositories) {
+        this.extraRepositories = extraRepositories;
+    }
+
     public boolean isFresh() {
         return fresh;
     }
@@ -512,7 +526,7 @@ public class KameletMain extends MainCommandLineSupport {
         CircuitBreakerDownloader.registerDownloadReifiers();
 
         // in case we use transacted
-        TransactedDownloader.registerDownloadReifiers(this);
+        TransactedDownloader.registerDownloadReifiers();
 
         // in case we use saga
         SagaDownloader.registerDownloadReifiers();
@@ -825,6 +839,7 @@ public class KameletMain extends MainCommandLineSupport {
         downloader.setCamelContext(answer);
         downloader.setVerbose(verbose);
         downloader.setRepositories(repositories);
+        downloader.setExtraRepositories(extraRepositories);
         downloader.setFresh(fresh);
         downloader.setMavenSettings(mavenSettings);
         downloader.setMavenSettingsSecurity(mavenSettingsSecurity);
diff --git 
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/MavenDependencyDownloader.java
 
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/MavenDependencyDownloader.java
index 5ec4b2b4b7a1..977f37127ed8 100644
--- 
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/MavenDependencyDownloader.java
+++ 
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/MavenDependencyDownloader.java
@@ -83,6 +83,8 @@ public class MavenDependencyDownloader extends ServiceSupport 
implements Depende
 
     // repository URLs set from "camel.jbang.repos" property or --repos option.
     private String repositories;
+    // the extra default repositories, or null for the ones of the 
camel.extra.repos system property
+    private String extraRepositories;
     private boolean fresh;
 
     // settings.xml and settings-security.xml locations to be passed to 
MavenDownloader from camel-tooling-maven
@@ -162,6 +164,19 @@ public class MavenDependencyDownloader extends 
ServiceSupport implements Depende
         return repositories;
     }
 
+    public String getExtraRepositories() {
+        return extraRepositories;
+    }
+
+    /**
+     * The extra default repositories (comma-separated URLs or id=url pairs, 
empty for none), used in addition to Maven
+     * Central, the Maven settings and the repositories. When not set, they 
are the ones of the camel.extra.repos system
+     * property, else the camel.default.extra.repos.default.value system 
property.
+     */
+    public void setExtraRepositories(String extraRepositories) {
+        this.extraRepositories = extraRepositories;
+    }
+
     @Override
     public void setRepositories(String repositories) {
         this.repositories = repositories;
@@ -593,6 +608,7 @@ public class MavenDependencyDownloader extends 
ServiceSupport implements Depende
         mavenDownloaderImpl.setMavenCentralEnabled(mavenCentralEnabled);
         
mavenDownloaderImpl.setMavenApacheSnapshotEnabled(mavenApacheSnapshotEnabled);
         mavenDownloaderImpl.setRepos(repositories);
+        mavenDownloaderImpl.setExtraRepos(extraRepositories);
         mavenDownloaderImpl.setFresh(fresh);
         mavenDownloaderImpl.setOffline(!download);
         mavenDownloaderImpl.setPreferLocal(preferLocal);
diff --git 
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/TransactedDownloader.java
 
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/TransactedDownloader.java
index 339abd473a61..9f8f14474dfc 100644
--- 
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/TransactedDownloader.java
+++ 
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/TransactedDownloader.java
@@ -21,10 +21,10 @@ import java.util.Arrays;
 import org.apache.camel.NamedNode;
 import org.apache.camel.Processor;
 import org.apache.camel.Route;
-import org.apache.camel.main.KameletMain;
 import org.apache.camel.model.TransactedDefinition;
 import org.apache.camel.reifier.ProcessReifier;
 import org.apache.camel.reifier.ProcessorReifier;
+import org.apache.camel.spi.Registry;
 import org.apache.camel.spi.TransactedPolicy;
 
 /**
@@ -45,7 +45,9 @@ public class TransactedDownloader {
     private TransactedDownloader() {
     }
 
-    public static void registerDownloadReifiers(KameletMain main) {
+    public static void registerDownloadReifiers() {
+        // the reifiers are static (for the JVM), so they refer to the 
CamelContext of the route only, not to the
+        // KameletMain that registers them, which would otherwise stay 
referenced when it is done
         ProcessorReifier.registerReifier(TransactedDefinition.class,
                 (route, processorDefinition) -> {
                     if (processorDefinition instanceof TransactedDefinition) {
@@ -54,7 +56,8 @@ public class TransactedDownloader {
                             downloader.downloadDependency("org.apache.camel", 
"camel-jta",
                                     route.getCamelContext().getVersion());
                             TransactedPolicy policy = new 
DummyTransactedPolicy();
-                            Arrays.stream(TRANSACTED_POLICIES).forEach(p -> 
main.bind(p, policy));
+                            Registry registry = 
route.getCamelContext().getRegistry();
+                            Arrays.stream(TRANSACTED_POLICIES).forEach(p -> 
registry.bind(p, policy));
                         }
                     }
                     return ProcessReifier.coreReifier(route, 
processorDefinition);
diff --git 
a/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloader.java
 
b/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloader.java
index eade50acf9a6..480e171621c9 100644
--- 
a/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloader.java
+++ 
b/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloader.java
@@ -155,6 +155,35 @@ public interface MavenDownloader extends Service {
      */
     void setRepos(String repos);
 
+    /**
+     * Configures the extra default repositories, which are used in addition 
to Maven Central, the repositories of the
+     * Maven settings and the configured repositories: a comma-separated list 
of URLs or {@code id=url} pairs. An empty
+     * value is no extra repositories, and null (the default) takes them from 
the {@code camel.extra.repos} system
+     * property, else the {@code camel.default.extra.repos.default.value} 
system property.
+     * <p>
+     * The default implementation throws, rather than ignoring the value: an 
implementation that does not support it
+     * would otherwise download from repositories the caller asked not to use 
(for example, none at all).
+     *
+     * @since 4.24
+     */
+    default void setExtraRepos(String extraRepos) {
+        throw new UnsupportedOperationException("setExtraRepos is not 
supported by " + getClass().getName());
+    }
+
+    /**
+     * The extra default repositories in use (see {@link 
#setExtraRepos(String)}): the configured ones, else the ones of
+     * the system properties.
+     * <p>
+     * The default implementation returns null (none), which is what an 
implementation that does not support extra
+     * repositories uses.
+     *
+     * @return the comma-separated repositories, or null or empty when there 
are none
+     * @since  4.24
+     */
+    default String getExtraRepos() {
+        return null;
+    }
+
     /**
      * Configure a location of {@code settings-security.xml} (when not set, 
defaults to
      * {@code ~/.m2/settings-security.xml} unless {@link 
#setMavenSettingsLocation(String)} is set explicitly set to
diff --git 
a/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloaderImpl.java
 
b/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloaderImpl.java
index bb5107c96455..65c7e3a87965 100644
--- 
a/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloaderImpl.java
+++ 
b/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloaderImpl.java
@@ -114,6 +114,7 @@ public class MavenDownloaderImpl extends ServiceSupport 
implements MavenDownload
     private String mavenSettings;
     private String mavenSettingsSecurity;
     private String repos;
+    private String extraRepos;
     private boolean fresh;
     private boolean offline;
     private boolean preferLocal = true;
@@ -516,8 +517,9 @@ public class MavenDownloaderImpl extends ServiceSupport 
implements MavenDownload
     }
 
     /**
-     * Loads extra default Maven repositories from a system property, so they 
are used in addition to Maven Central and
-     * the repositories from {@code settings.xml} without having to pass 
{@code --repos} on every command.
+     * Loads the extra default Maven repositories, so they are used in 
addition to Maven Central and the repositories
+     * from {@code settings.xml} without having to pass {@code --repos} on 
every command. They are the ones configured
+     * with {@link #setExtraRepos(String)}, else the ones of a system property.
      * <p>
      * The value is a comma-separated list of repositories, where each entry 
is either a plain URL or an {@code id=url}
      * pair, for example {@code 
repo1=https://repo1.example.com/maven2,repo2=https://repo2.example.com/releases}.
 See
@@ -533,16 +535,14 @@ public class MavenDownloaderImpl extends ServiceSupport 
implements MavenDownload
      * unless configured.
      */
     private void loadExtraDefaultRepositories(List<RemoteRepository> 
repositories) {
-        // user-provided value first, then any baseline a custom distribution 
provided
-        String sysProp
-                = System.getProperty(EXTRA_DEFAULT_REPOS_PROPERTY, 
System.getProperty(EXTRA_DEFAULT_REPOS_DEFAULT_VALUE));
-        if (sysProp != null && !sysProp.isBlank()) {
-            Set<String> repoSpecs = Arrays.stream(sysProp.split("\\s*,\\s*"))
+        String extra = getExtraRepos();
+        if (extra != null && !extra.isBlank()) {
+            Set<String> repoSpecs = Arrays.stream(extra.split("\\s*,\\s*"))
                     .filter(s -> !s.isBlank())
                     .collect(Collectors.toCollection(LinkedHashSet::new));
 
             if (!repoSpecs.isEmpty()) {
-                LOG.debug("Loaded extra default repositories from system 
property: {}", sysProp);
+                LOG.debug("Loaded extra default repositories: {}", extra);
                 configureRepositories(repositories, repoSpecs);
                 LOG.debug("Configured {} extra default Maven repositories", 
repoSpecs.size());
             }
@@ -927,6 +927,20 @@ public class MavenDownloaderImpl extends ServiceSupport 
implements MavenDownload
         this.repos = repos;
     }
 
+    @Override
+    public void setExtraRepos(String extraRepos) {
+        this.extraRepos = extraRepos;
+    }
+
+    @Override
+    public String getExtraRepos() {
+        if (extraRepos != null) {
+            return extraRepos;
+        }
+        // user-provided value first, then any baseline a custom distribution 
provided
+        return System.getProperty(EXTRA_DEFAULT_REPOS_PROPERTY, 
System.getProperty(EXTRA_DEFAULT_REPOS_DEFAULT_VALUE));
+    }
+
     public void setFresh(boolean fresh) {
         this.fresh = fresh;
     }
diff --git 
a/tooling/camel-tooling-maven/src/test/java/org/apache/camel/tooling/maven/MavenDownloaderImplTest.java
 
b/tooling/camel-tooling-maven/src/test/java/org/apache/camel/tooling/maven/MavenDownloaderImplTest.java
index 1cdbe1467de3..bca14460d7e1 100644
--- 
a/tooling/camel-tooling-maven/src/test/java/org/apache/camel/tooling/maven/MavenDownloaderImplTest.java
+++ 
b/tooling/camel-tooling-maven/src/test/java/org/apache/camel/tooling/maven/MavenDownloaderImplTest.java
@@ -43,6 +43,7 @@ import org.slf4j.LoggerFactory;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 
@@ -702,6 +703,40 @@ class MavenDownloaderImplTest {
         }
     }
 
+    @Test
+    void testExtraReposPrecedence() throws Exception {
+        String original = System.getProperty("camel.extra.repos");
+        String originalDefault = 
System.getProperty("camel.default.extra.repos.default.value");
+        try (MavenDownloaderImpl downloader = new MavenDownloaderImpl()) {
+            System.clearProperty("camel.extra.repos");
+            System.clearProperty("camel.default.extra.repos.default.value");
+            assertNull(downloader.getExtraRepos());
+
+            // the fallback for a custom distribution, then the user's system 
property: the first one set wins
+            System.setProperty("camel.default.extra.repos.default.value", 
"baseline=https://baseline.example.com/maven2";);
+            assertEquals("baseline=https://baseline.example.com/maven2";, 
downloader.getExtraRepos());
+            System.setProperty("camel.extra.repos", 
"user=https://user.example.com/maven2";);
+            assertEquals("user=https://user.example.com/maven2";, 
downloader.getExtraRepos());
+
+            // the configured value wins over the system properties, and empty 
is no extra repositories
+            
downloader.setExtraRepos("explicit=https://explicit.example.com/maven2";);
+            assertEquals("explicit=https://explicit.example.com/maven2";, 
downloader.getExtraRepos());
+            downloader.setExtraRepos("");
+            assertEquals("", downloader.getExtraRepos());
+        } finally {
+            restoreProperty("camel.extra.repos", original);
+            restoreProperty("camel.default.extra.repos.default.value", 
originalDefault);
+        }
+    }
+
+    private static void restoreProperty(String key, String value) {
+        if (value != null) {
+            System.setProperty(key, value);
+        } else {
+            System.clearProperty(key);
+        }
+    }
+
     @Test
     void testResolveArtifactDescriptor() throws Exception {
         File repo = createDescriptorFixtureRepository();

Reply via email to