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 5cf8b1c632a3 CAMEL-22149: jbang-it, make tests Windows compatible 
(#21493)
5cf8b1c632a3 is described below

commit 5cf8b1c632a30683b502614c57cb79582e569e90
Author: Marco Carletti <[email protected]>
AuthorDate: Tue Feb 17 13:58:33 2026 +0100

    CAMEL-22149: jbang-it, make tests Windows compatible (#21493)
---
 dsl/camel-jbang/camel-jbang-it/README.adoc         |  11 +
 dsl/camel-jbang/camel-jbang-it/pom.xml             |  18 +-
 .../camel/dsl/jbang/it/CamelCatalogITCase.java     |   4 +
 .../camel/dsl/jbang/it/CamelDebugITCase.java       |   4 +
 .../camel/dsl/jbang/it/CmdStartStopITCase.java     |  14 +-
 .../camel/dsl/jbang/it/CustomJarsITCase.java       |   3 +-
 .../apache/camel/dsl/jbang/it/DevModeITCase.java   |   2 +
 .../camel/dsl/jbang/it/InfrastructureITCase.java   |   6 +
 .../apache/camel/dsl/jbang/it/JolokiaITCase.java   |   6 +-
 .../apache/camel/dsl/jbang/it/OpenApiITCase.java   |   2 +
 .../camel/dsl/jbang/it/RunCommandITCase.java       |  12 +-
 .../camel/dsl/jbang/it/RunCommandOnMqttITCase.java |   6 +-
 .../dsl/jbang/it/ScriptingWithPipesITCase.java     |   4 +
 .../dsl/jbang/it/support/JBangTestSupport.java     |  40 +-
 test-infra/camel-test-infra-cli/README.adoc        |  55 ++-
 test-infra/camel-test-infra-cli/pom.xml            |  10 +-
 .../camel/test/infra/cli/common/CliProperties.java |   2 +
 .../test/infra/cli/it/AbstractTestSupport.java     |   5 +
 .../camel/test/infra/cli/it/CliConfigITCase.java   |  11 +-
 .../apache/camel/test/infra/cli/it/RunITCase.java  |   3 +
 .../test/infra/cli/services/CliBuiltContainer.java |   2 +-
 .../cli/services/CliLocalContainerService.java     |   8 +-
 .../infra/cli/services/CliLocalProcessService.java | 480 +++++++++++++++++++++
 .../camel/test/infra/cli/services/CliService.java  |   4 +-
 .../test/infra/cli/services/CliServiceFactory.java |   1 +
 .../infra/common/services/TestServiceUtil.java     |  16 +-
 26 files changed, 659 insertions(+), 70 deletions(-)

diff --git a/dsl/camel-jbang/camel-jbang-it/README.adoc 
b/dsl/camel-jbang/camel-jbang-it/README.adoc
index 274c9d1bace3..be7963cbcb5f 100644
--- a/dsl/camel-jbang/camel-jbang-it/README.adoc
+++ b/dsl/camel-jbang/camel-jbang-it/README.adoc
@@ -13,6 +13,17 @@ To run tests use the dedicated Maven profile `jbang-it-test` 
activated by the sa
 mvn verify -Djbang-it-test
 ----
 
+== Run tests locally
+
+To run tests using a local Camel CLI process instead of a container, activate 
the `local-camel-cli-process` profile:
+
+[source,bash]
+----
+mvn verify -Djbang-it-test -Dcamel-cli.instance.type=local-camel-cli-process
+----
+
+Tests tagged with `container-only` will be automatically excluded when this 
profile is active.
+
 == Configuration
 
 Camel CLI infra configuration is defined in 
link:../../../test-infra/camel-test-infra-cli/README.adoc[README.adoc]
diff --git a/dsl/camel-jbang/camel-jbang-it/pom.xml 
b/dsl/camel-jbang/camel-jbang-it/pom.xml
index 9abfc35706ea..44122c9fbc6e 100644
--- a/dsl/camel-jbang/camel-jbang-it/pom.xml
+++ b/dsl/camel-jbang/camel-jbang-it/pom.xml
@@ -232,7 +232,9 @@
                 <maven.test.skip>false</maven.test.skip>
                 <shared.data.folder>target/data</shared.data.folder>
                 
<shared.maven.local.repo>${settings.localRepository}</shared.maven.local.repo>
+                <failsafe.forkCount>1C</failsafe.forkCount>
                 <x11.display>:0</x11.display>
+                <failsafe.excludedGroups/>
             </properties>
             <build>
                 <plugins>
@@ -267,8 +269,9 @@
                         </executions>
                         <configuration>
                             
<argLine>-DforkNumber=${surefire.forkNumber}</argLine>
-                            <forkCount>1C</forkCount>
+                            <forkCount>${failsafe.forkCount}</forkCount>
                             <groups>${export.runtime}</groups>
+                            
<excludedGroups>${failsafe.excludedGroups}</excludedGroups>
                             
<rerunFailingTestsCount>${surefire.rerunFailingTestsCount}</rerunFailingTestsCount>
                             <systemPropertyVariables>
                                 
<cli.service.data.folder>${shared.data.folder}</cli.service.data.folder>
@@ -287,6 +290,19 @@
                 </plugins>
             </build>
         </profile>
+        <profile>
+            <id>local-camel-cli-process</id>
+            <activation>
+                <property>
+                    <name>camel-cli.instance.type</name>
+                    <value>local-camel-cli-process</value>
+                </property>
+            </activation>
+            <properties>
+                <failsafe.forkCount>1</failsafe.forkCount>
+                
<failsafe.excludedGroups>container-only</failsafe.excludedGroups>
+            </properties>
+        </profile>
     </profiles>
 
 </project>
diff --git 
a/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/CamelCatalogITCase.java
 
b/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/CamelCatalogITCase.java
index 2c3723deee8f..daa14055150b 100644
--- 
a/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/CamelCatalogITCase.java
+++ 
b/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/CamelCatalogITCase.java
@@ -18,7 +18,11 @@ package org.apache.camel.dsl.jbang.it;
 
 import org.apache.camel.dsl.jbang.it.support.JBangTestSupport;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
 
+import static org.junit.jupiter.api.condition.OS.WINDOWS;
+
+@DisabledOnOs(WINDOWS)
 public class CamelCatalogITCase extends JBangTestSupport {
 
     private static final String COMPONENT_REGEX
diff --git 
a/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/CamelDebugITCase.java
 
b/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/CamelDebugITCase.java
index 885bc664f331..910eded0830a 100644
--- 
a/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/CamelDebugITCase.java
+++ 
b/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/CamelDebugITCase.java
@@ -20,7 +20,11 @@ import java.io.IOException;
 
 import org.apache.camel.dsl.jbang.it.support.JBangTestSupport;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
 
+import static org.junit.jupiter.api.condition.OS.WINDOWS;
+
+@DisabledOnOs(WINDOWS)
 public class CamelDebugITCase extends JBangTestSupport {
     @Test
     public void testDebug() throws IOException {
diff --git 
a/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/CmdStartStopITCase.java
 
b/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/CmdStartStopITCase.java
index f99a8c8da832..91fa4a74f64d 100644
--- 
a/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/CmdStartStopITCase.java
+++ 
b/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/CmdStartStopITCase.java
@@ -20,8 +20,13 @@ import java.io.IOException;
 
 import org.apache.camel.dsl.jbang.it.support.JBangTestSupport;
 import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
 
+import static org.junit.jupiter.api.condition.OS.WINDOWS;
+
+@DisabledOnOs(WINDOWS)
 public class CmdStartStopITCase extends JBangTestSupport {
 
     @Test
@@ -43,7 +48,7 @@ public class CmdStartStopITCase extends JBangTestSupport {
         String process = executeBackground(String.format("run 
%s/FromDirectoryRoute.java", mountPoint()));
         executeBackground(String.format("run %s/route2.yaml", mountPoint()));
         checkLogContains("Hello world!");
-        execute("cmd stop-route " + getPID(process));
+        execute("cmd stop-route " + process);
         checkCommandOutputsPattern("get route",
                 
"route1\\s+timer:\\/\\/(yaml|java)\\?period=1000\\s+Stopped.*\\n.*route2.*timer:\\/\\/(yaml|java)\\?period=1000\\s+Started",
                 ASSERTION_WAIT_SECONDS);
@@ -82,7 +87,7 @@ public class CmdStartStopITCase extends JBangTestSupport {
         executeBackground(String.format("run %s/route2.yaml", mountPoint()));
         checkLogContains("Hello world!");
         execute("cmd stop-route");
-        execute("cmd start-route " + getPID(process));
+        execute("cmd start-route " + process);
         checkCommandOutputsPattern("get route",
                 
"route1\\s+timer:\\/\\/(yaml|java)\\?period=1000\\s+Started.*\\n.*route2.*timer:\\/\\/(yaml|java)\\?period=1000\\s+Stopped",
                 ASSERTION_WAIT_SECONDS);
@@ -102,6 +107,7 @@ public class CmdStartStopITCase extends JBangTestSupport {
     }
 
     @Test
+    @Tag("container-only")
     public void testCamelWatch() throws IOException {
         copyResourceInDataFolder(TestResources.ROUTE2);
         String process = executeBackground(String.format("run %s/route2.yaml", 
mountPoint()));
@@ -111,7 +117,7 @@ public class CmdStartStopITCase extends JBangTestSupport {
         execInContainer(String.format("chmod +x %s/watch-sleep", 
mountPoint()));
         Assertions.assertThat(
                 execInContainer(String.format("%s/watch-sleep", mountPoint())))
-                .as("watch command should output PID" + getPID(process))
-                .contains(getPID(process));
+                .as("watch command should output PID" + process)
+                .contains(process);
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/CustomJarsITCase.java
 
b/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/CustomJarsITCase.java
index b78ccb0c4d47..b612b90b24d4 100644
--- 
a/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/CustomJarsITCase.java
+++ 
b/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/CustomJarsITCase.java
@@ -28,7 +28,8 @@ public class CustomJarsITCase extends JBangTestSupport {
     public void testCustomJars() throws IOException {
         copyResourceInDataFolder(TestResources.CIRCUIT_BREAKER);
         Assertions
-                .assertThatCode(() -> execute(String.format("run 
%s/CircuitBreakerRoute.java --dep=camel-timer", mountPoint())))
+                .assertThatCode(() -> execute(
+                        String.format("run %s/CircuitBreakerRoute.java 
--max-seconds=30", mountPoint())))
                 .as("the application without dependency will cause error")
                 .hasStackTraceContaining("Failed to create route: 
circuitBreaker")
                 .hasStackTraceContaining(
diff --git 
a/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/DevModeITCase.java
 
b/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/DevModeITCase.java
index c530e381ca2d..0d22f8220b03 100644
--- 
a/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/DevModeITCase.java
+++ 
b/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/DevModeITCase.java
@@ -31,6 +31,7 @@ import org.apache.camel.dsl.jbang.it.support.JBangTestSupport;
 import org.apache.camel.dsl.jbang.it.support.JiraIssue;
 import org.assertj.core.api.Assertions;
 import org.assertj.core.api.SoftAssertions;
+import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 
 public class DevModeITCase extends JBangTestSupport {
@@ -75,6 +76,7 @@ public class DevModeITCase extends JBangTestSupport {
 
     @Test
     @JiraIssue("CAMEL-20939")
+    @Tag("container-only")
     public void runUsingProfileTest() throws IOException {
         copyResourceInDataFolder(TestResources.HELLO_NAME);
         copyResourceInDataFolder(TestResources.TEST_PROFILE_PROP);
diff --git 
a/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/InfrastructureITCase.java
 
b/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/InfrastructureITCase.java
index ae27a71a6d2a..c5edb826f04d 100644
--- 
a/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/InfrastructureITCase.java
+++ 
b/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/InfrastructureITCase.java
@@ -21,9 +21,15 @@ import java.time.Duration;
 import org.apache.camel.dsl.jbang.it.support.JBangTestSupport;
 import org.assertj.core.api.Assertions;
 import org.awaitility.Awaitility;
+import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+import org.junit.jupiter.api.condition.DisabledOnOs;
 
+import static org.junit.jupiter.api.condition.OS.WINDOWS;
+
+@Tag("container-only")
+@DisabledOnOs(WINDOWS)
 public class InfrastructureITCase extends JBangTestSupport {
     private static final String SERVICE = "ftp";
     private static final String IMPL_SERVICE = "artemis";
diff --git 
a/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/JolokiaITCase.java
 
b/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/JolokiaITCase.java
index b6281a00f363..1b348b7566ed 100644
--- 
a/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/JolokiaITCase.java
+++ 
b/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/JolokiaITCase.java
@@ -23,7 +23,11 @@ import 
org.apache.camel.dsl.jbang.it.support.JBangTestSupport;
 import org.assertj.core.api.Assertions;
 import org.awaitility.Awaitility;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
 
+import static org.junit.jupiter.api.condition.OS.WINDOWS;
+
+@DisabledOnOs(WINDOWS)
 public class JolokiaITCase extends JBangTestSupport {
 
     @Test
@@ -37,7 +41,7 @@ public class JolokiaITCase extends JBangTestSupport {
                 .contains("\"agentContext\":\"/jolokia\"");
         Assertions.assertThat(execute("jolokia FromDirectoryRoute --stop"))
                 .as("Jolokia should stop")
-                .contains("Stopped Jolokia for PID " + getPID(process));
+                .contains("Stopped Jolokia for PID " + process);
     }
 
     @Test
diff --git 
a/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/OpenApiITCase.java
 
b/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/OpenApiITCase.java
index b9cb27dd6737..dd68bdd37511 100644
--- 
a/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/OpenApiITCase.java
+++ 
b/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/OpenApiITCase.java
@@ -30,9 +30,11 @@ import java.util.Random;
 import org.apache.camel.dsl.jbang.it.support.InVersion;
 import org.apache.camel.dsl.jbang.it.support.JBangTestSupport;
 import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper;
 
+@Tag("container-only")
 public class OpenApiITCase extends JBangTestSupport {
 
     final HttpClient httpClient = HttpClient.newHttpClient();
diff --git 
a/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/RunCommandITCase.java
 
b/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/RunCommandITCase.java
index 86bbe5ebef5d..c202246a8ebc 100644
--- 
a/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/RunCommandITCase.java
+++ 
b/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/RunCommandITCase.java
@@ -31,14 +31,15 @@ import org.apache.camel.test.infra.cli.common.CliProperties;
 import org.assertj.core.api.Assertions;
 import org.awaitility.Awaitility;
 import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.condition.DisabledIf;
 import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
-import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.DisabledOnOs;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
 
-import static org.junit.jupiter.api.condition.OS.LINUX;
+import static org.junit.jupiter.api.condition.OS.WINDOWS;
 
 public class RunCommandITCase extends JBangTestSupport {
 
@@ -80,6 +81,7 @@ public class RunCommandITCase extends JBangTestSupport {
     }
 
     @Test
+    @Tag("container-only")
     public void runRoutesFromMultipleFilesUsingWildcardTest() {
         execute("init one.yaml --directory=/tmp/one");
         execute("init two.xml --directory=/tmp/two");
@@ -126,6 +128,7 @@ public class RunCommandITCase extends JBangTestSupport {
     }
 
     @Test
+    @Tag("container-only")
     public void runDownloadedFromGithubTest() {
         execute("init 
https://github.com/apache/camel-kamelets-examples/tree/main/jbang/dependency-injection";);
         
Assertions.assertThat(containerService.listDirectory(DEFAULT_ROUTE_FOLDER))
@@ -156,7 +159,7 @@ public class RunCommandITCase extends JBangTestSupport {
         final String process = executeBackground(String.format("run 
%s/cheese.xml --camel-version=%s", mountPoint(), version));
         checkLogContainsPattern(String.format(" Apache Camel %s .* started", 
version));
         checkLogContains(DEFAULT_MSG);
-        execute("stop " + getPID(process));
+        execute("stop " + process);
     }
 
     @Test
@@ -168,7 +171,8 @@ public class RunCommandITCase extends JBangTestSupport {
     }
 
     @Test
-    @EnabledOnOs(LINUX)
+    @DisabledOnOs(WINDOWS)
+    @Tag("container-only")
     @DisabledIf(value = "java.awt.GraphicsEnvironment#isHeadless")
     public void runFromClipboardTest() throws IOException {
         Assumptions.assumeTrue(execInHost("command -v ssh").contains("ssh"));
diff --git 
a/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/RunCommandOnMqttITCase.java
 
b/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/RunCommandOnMqttITCase.java
index 7e3ac64f26bc..26d0107f8529 100644
--- 
a/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/RunCommandOnMqttITCase.java
+++ 
b/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/RunCommandOnMqttITCase.java
@@ -24,9 +24,11 @@ import org.apache.camel.test.AvailablePortFinder;
 import 
org.apache.camel.test.infra.mosquitto.services.MosquittoLocalContainerService;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.testcontainers.containers.GenericContainer;
 
+@Tag("container-only")
 public class RunCommandOnMqttITCase extends JBangTestSupport {
 
     private static int mqttPort = AvailablePortFinder.getNextAvailable();
@@ -54,7 +56,7 @@ public class RunCommandOnMqttITCase extends JBangTestSupport {
         checkLogContains("Started route1 (kamelet:mqtt5-source)");
         final String payloadFile = "payload.json";
         newFileInDataFolder(payloadFile, "{\"value\": 21}");
-        sendCmd(String.format("%s/%s", mountPoint(), payloadFile), 
getPID(process));
+        sendCmd(String.format("%s/%s", mountPoint(), payloadFile), process);
         checkLogContains("The temperature is 21");
     }
 
@@ -63,7 +65,7 @@ public class RunCommandOnMqttITCase extends JBangTestSupport {
         copyResourceInDataFolder(TestResources.STUB_ROUTE);
         final String process = executeBackground(String.format("run %s/%s 
--stub=jms",
                 mountPoint(), TestResources.STUB_ROUTE.getName()));
-        checkCommandOutputs("cmd send --body='Hello camel from stubbed jms' " 
+ getPID(process), "Sent (success)");
+        checkCommandOutputs("cmd send --body='Hello camel from stubbed jms' " 
+ process, "Sent (success)");
         checkCommandOutputs("cmd stub --browse", "Hello camel from stubbed 
jms", ASSERTION_WAIT_SECONDS);
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/ScriptingWithPipesITCase.java
 
b/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/ScriptingWithPipesITCase.java
index 6915b4c3c87a..2899bea8a5af 100644
--- 
a/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/ScriptingWithPipesITCase.java
+++ 
b/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/ScriptingWithPipesITCase.java
@@ -20,7 +20,11 @@ import java.io.IOException;
 
 import org.apache.camel.dsl.jbang.it.support.JBangTestSupport;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
 
+import static org.junit.jupiter.api.condition.OS.WINDOWS;
+
+@DisabledOnOs(WINDOWS)
 public class ScriptingWithPipesITCase extends JBangTestSupport {
     @Test
     public void testPipeScript() throws IOException {
diff --git 
a/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/support/JBangTestSupport.java
 
b/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/support/JBangTestSupport.java
index df1060a7562a..74d9e8da358f 100644
--- 
a/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/support/JBangTestSupport.java
+++ 
b/dsl/camel-jbang/camel-jbang-it/src/test/java/org/apache/camel/dsl/jbang/it/support/JBangTestSupport.java
@@ -29,6 +29,7 @@ import java.nio.channels.Channels;
 import java.nio.channels.ReadableByteChannel;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystems;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -66,6 +67,7 @@ public abstract class JBangTestSupport {
     protected static final int ASSERTION_WAIT_SECONDS
             = 
Integer.parseInt(System.getProperty("jbang.it.assert.wait.timeout", "60"));
 
+    //to use only in case of container execution, so mark with the tag 
@Tag("container-only") the test
     protected static final String DEFAULT_ROUTE_FOLDER = "/home/jbang";
 
     public static final String DEFAULT_MSG = "Hello Camel from";
@@ -76,7 +78,9 @@ public abstract class JBangTestSupport {
     protected void beforeEach(TestInfo testInfo) throws IOException {
         Assertions.assertThat(DATA_FOLDER).as("%s need to be set", 
CliProperties.DATA_FOLDER).isNotBlank();
         containerDataFolder = Files.createDirectory(Paths.get(DATA_FOLDER, 
containerService.id())).toAbsolutePath().toString();
-        Files.setPosixFilePermissions(Paths.get(containerDataFolder), 
EnumSet.allOf(PosixFilePermission.class));
+        if 
(FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) {
+            Files.setPosixFilePermissions(Paths.get(containerDataFolder), 
EnumSet.allOf(PosixFilePermission.class));
+        }
         logger.debug("running {}#{} using data folder {}", 
getClass().getName(), testInfo.getDisplayName(), getDataFolder());
     }
 
@@ -90,10 +94,6 @@ public abstract class JBangTestSupport {
         }
     }
 
-    protected void stopAllRoutes() {
-        execute("stop --all");
-    }
-
     protected enum TestResources {
         ROUTE2("route2.yaml", "/jbang/it/route2.yaml"),
         TEST_PROFILE_PROP("application-test.properties", 
"/jbang/it/application-test.properties"),
@@ -146,7 +146,7 @@ public abstract class JBangTestSupport {
     }
 
     protected String mountPoint() {
-        return String.format("%s/%s", containerService.getMountPoint(), 
containerService.id());
+        return Paths.get(containerService.getMountPoint(), 
containerService.id()).toString();
     }
 
     protected void initAndRunInBackground(String file) {
@@ -222,13 +222,6 @@ public abstract class JBangTestSupport {
                         .contains(contains)));
     }
 
-    protected void checkContainerLogContainsAllOf(int waitForSeconds, 
String... contains) {
-        Assertions.assertThatNoException().isThrownBy(() -> Awaitility.await()
-                .atMost(waitForSeconds, TimeUnit.SECONDS)
-                .untilAsserted(() -> Assertions.assertThat(getContainerLogs())
-                        .contains(contains)));
-    }
-
     protected void checkLogContains(String contains) {
         checkLogContains(contains, ASSERTION_WAIT_SECONDS);
     }
@@ -293,14 +286,6 @@ public abstract class JBangTestSupport {
         return getLogs(null);
     }
 
-    protected String getPID(String startupMessage) {
-        return startupMessage.split("PID:")[1].split(" 
")[1].replaceAll("[^0-9]", "");
-    }
-
-    protected String getContainerLogs() {
-        return containerService.getContainerLogs();
-    }
-
     protected String getLogs(String route) {
         return execute(Optional.ofNullable(route)
                 .map(r -> String.format("log %s --follow=false", r))
@@ -352,7 +337,10 @@ public abstract class JBangTestSupport {
 
     protected String execInHost(String command) {
         try {
-            ProcessBuilder builder = new ProcessBuilder("/bin/bash", "-c", 
command);
+            boolean isWindows = System.getProperty("os.name", 
"").toLowerCase().startsWith("win");
+            ProcessBuilder builder = isWindows
+                    ? new ProcessBuilder("cmd.exe", "/c", command)
+                    : new ProcessBuilder("/bin/bash", "-c", command);
             builder.redirectErrorStream(true);
             final Process process = builder.start();
             Awaitility.await("process is running")
@@ -419,9 +407,11 @@ public abstract class JBangTestSupport {
     }
 
     protected void assertFileInContainerExists(String fileAbsolutePath) {
-        String fileName = 
Path.of(fileAbsolutePath).getFileName().toFile().getName();
-        
Assertions.assertThat(containerService.listDirectory(Path.of(fileAbsolutePath).getParent().toAbsolutePath().toString())
-                .anyMatch(child -> fileName.equals(child)))
+        Path path = Path.of(fileAbsolutePath);
+        String fileName = path.getFileName().toString();
+        String parentDir = path.getParent().toString();
+        Assertions.assertThat(containerService.listDirectory(parentDir)
+                .anyMatch(fileName::equals))
                 .as("check if file " + fileAbsolutePath + " exists")
                 .isTrue();
     }
diff --git a/test-infra/camel-test-infra-cli/README.adoc 
b/test-infra/camel-test-infra-cli/README.adoc
index 30e2e8943131..326eae69f872 100644
--- a/test-infra/camel-test-infra-cli/README.adoc
+++ b/test-infra/camel-test-infra-cli/README.adoc
@@ -1,24 +1,65 @@
-:image-name: fedora
+:image-name: Fedora
 :config-class: 
src/test/java/org/apache/camel/test/infra/cli/common/CliProperties.java
 
-= Camel JBang CLI container
+= Camel JBang CLI test infrastructure
 
 == Overview
 
-This module is used to build a container based on {image-name} image and Camel 
JBang.
+This module provides test infrastructure for Camel CLI (Camel JBang) 
integration tests.
+Two service implementations are available:
+
+* *Container-based* (`CliLocalContainerService`) -- builds and runs a Docker 
container based on {image-name} image with Camel JBang. This is the default.
+* *Process-based* (`CliLocalProcessService`) -- runs JBang/Camel CLI directly 
on the host via `ProcessBuilder`, without Docker. Useful on environments where 
Docker is not available.
+
+== Service selection
+
+The service implementation is selected via the system property 
`camel-cli.instance.type`:
+
+* Default (or `local-camel-cli-container`): uses the container-based service
+* `local-camel-cli-process`: uses the process-based service (requires JBang 
pre-installed on the host)
+
+Example using the process-based service:
+
+[source,bash]
+----
+mvn verify -pl test-infra/camel-test-infra-cli -Pjbang-it-test 
-Dcamel-cli.instance.type=local-camel-cli-process
+----
 
 == Configuration
 
 System variables are defined in link:{config-class}[CliProperties]
 
+=== Common properties (both services)
+
  - `cli.service.repo` : the repo on github, default `apache/camel`
  - `cli.service.branch` : the branch of the repo on github, default `main`
  - `cli.service.version` : the version of the `camel.jbang.version` defined in 
the `CamelJBang.java` file, default value is `default`, it means it uses the 
default values in the file
- - `cli.service.data.folder` : mandatory - path to local folder to bind as 
volume in the container
- - `cli.service.ssh.password` : ssh password set to access to the container 
via ssh, default `jbang`
- - `cli.service.execute.version` : Camel version set just after container 
start, default is empty so the version is the one in the branch
+ - `cli.service.data.folder` : path to local folder used as working directory 
(container: bind as volume; process: working directory). For the process-based 
service, a temp directory is created if not set.
+ - `cli.service.execute.version` : Camel version set just after service start, 
default is empty so the version is the one in the branch
  - `cli.service.mvn.repos` : comma separated list of custom Maven 
repositories, default empty
- - `cli.service.mvn.local` : path to the host folder mounted as container 
local maven repository
+ - `cli.service.mvn.local` : path to the host folder used as local maven 
repository (container: mounted as bind volume; process: set via `JBANG_REPO` 
env var and a temporary `settings.xml` passed with `--maven-settings`)
+
+=== Process-only properties
+
+ - `cli.service.skip.install` : when set to `true`, skips the JBang 
trust/install/uninstall steps and uses the `camel` command already available on 
the host, default `false`
+
+=== Container-only properties
+
+ - `cli.service.ssh.password` : ssh password set to access to the container 
via ssh, default `jbang`
  - `cli.service.extra.hosts` : comma separated host=ip pairs to add in the 
hosts file
  - `cli.service.trusted.paths` : commas separated paths, relative to the host, 
of the files containing PEM trusted certificates
  - `cli.service.docker.file` : path to a custom Dockerfile, by default the one 
in the classpath at _org/apache/camel/test/infra/cli/services/Dockerfile_ will 
be used
+
+== Process-based service details
+
+The `CliLocalProcessService` replicates the container setup sequence using 
local JBang commands:
+
+1. `jbang trust add` for the configured repository
+2. `jbang app install` to install the Camel CLI app
+3. Optionally sets the Camel version and Maven repositories
+
+On shutdown, it runs `jbang app uninstall camel` and cleans up any 
auto-created temp directory.
+
+*Prerequisites:* JBang must be installed and available on the host. The 
`~/.jbang/bin` directory is automatically prepended to `PATH` for child 
processes.
+
+*Limitations:* `getSshPort()` and `getSshPassword()` throw 
`UnsupportedOperationException`. `getContainerLogs()` returns an empty string.
diff --git a/test-infra/camel-test-infra-cli/pom.xml 
b/test-infra/camel-test-infra-cli/pom.xml
index 6c6d26e15679..3bc9cfdce494 100644
--- a/test-infra/camel-test-infra-cli/pom.xml
+++ b/test-infra/camel-test-infra-cli/pom.xml
@@ -122,6 +122,7 @@
                                 <configuration>
                                     <target>
                                         <mkdir dir="target/tmp-repo"/>
+                                        <mkdir dir="target/data"/>
                                     </target>
                                 </configuration>
                                 <goals>
@@ -133,15 +134,6 @@
                     <plugin>
                         <groupId>org.apache.maven.plugins</groupId>
                         <artifactId>maven-failsafe-plugin</artifactId>
-                        <executions>
-                            <execution>
-                                <id>integration-test</id>
-                                <goals>
-                                    <goal>integration-test</goal>
-                                    <goal>verify</goal>
-                                </goals>
-                            </execution>
-                        </executions>
                         <configuration>
                             
<rerunFailingTestsCount>${surefire.rerunFailingTestsCount}</rerunFailingTestsCount>
                             <systemPropertyVariables>
diff --git 
a/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/common/CliProperties.java
 
b/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/common/CliProperties.java
index 7ad8e78064ab..914246208c5f 100644
--- 
a/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/common/CliProperties.java
+++ 
b/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/common/CliProperties.java
@@ -41,6 +41,8 @@ public final class CliProperties {
 
     public static final String TRUSTED_CERT_PATHS = 
"cli.service.trusted.paths";
 
+    public static final String SKIP_INSTALL = "cli.service.skip.install";
+
     private CliProperties() {
     }
 }
diff --git 
a/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/it/AbstractTestSupport.java
 
b/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/it/AbstractTestSupport.java
index 67b2d4d1fc82..a3b8e2aad798 100644
--- 
a/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/it/AbstractTestSupport.java
+++ 
b/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/it/AbstractTestSupport.java
@@ -23,6 +23,11 @@ import 
org.apache.camel.test.infra.cli.services.CliServiceFactory;
 
 public abstract class AbstractTestSupport {
 
+    static boolean isLocalProcessWithSkipInstall() {
+        return "true".equals(System.getProperty("cli.service.skip.install"))
+                && 
"local-camel-cli-process".equals(System.getProperty("camel-cli.instance.type"));
+    }
+
     protected void execute(Consumer<CliService> consumer) {
         try (CliService containerService = CliServiceFactory.createService()) {
             containerService.beforeAll(null);
diff --git 
a/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/it/CliConfigITCase.java
 
b/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/it/CliConfigITCase.java
index 01a727feca69..0823f0ab3719 100644
--- 
a/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/it/CliConfigITCase.java
+++ 
b/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/it/CliConfigITCase.java
@@ -22,6 +22,8 @@ import java.nio.file.Path;
 
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIf;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
 import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
 import org.junitpioneer.jupiter.ReadsSystemProperty;
 import org.junitpioneer.jupiter.RestoreSystemProperties;
@@ -32,6 +34,7 @@ public class CliConfigITCase extends AbstractTestSupport {
 
     @Test
     @SetSystemProperty(key = "cli.service.version", value = "4.16.0")
+    @DisabledIf("isLocalProcessWithSkipInstall")
     public void setJBangVersionTest() {
         execute(cliService -> {
             String version = cliService.version();
@@ -50,6 +53,7 @@ public class CliConfigITCase extends AbstractTestSupport {
 
     @Test
     @SetSystemProperty(key = "cli.service.branch", value = "camel-4.4.x")
+    @DisabledIf("isLocalProcessWithSkipInstall")
     public void setBranchTest() {
         execute(cliService -> {
             String version = cliService.version();
@@ -60,6 +64,7 @@ public class CliConfigITCase extends AbstractTestSupport {
     @Test
     @SetSystemProperty(key = "cli.service.repo", value = 
"mcarlett/apache-camel")
     @SetSystemProperty(key = "cli.service.branch", value = "camel-cli-test")
+    @DisabledIf("isLocalProcessWithSkipInstall")
     public void setRepoTest() {
         execute(cliService -> {
             String version = cliService.version();
@@ -71,6 +76,7 @@ public class CliConfigITCase extends AbstractTestSupport {
     @ReadsSystemProperty
     @EnabledIfSystemProperty(named = "currentProjectVersion", matches = 
"^(?!\\s*$).+",
                              disabledReason = "currentProjectVersion system 
property must be set")
+    @DisabledIf("isLocalProcessWithSkipInstall")
     public void setCurrentProjectVersionTest() {
         String currentCamelVersion = 
System.getProperty("currentProjectVersion");
         System.setProperty("cli.service.version", currentCamelVersion);
@@ -83,10 +89,13 @@ public class CliConfigITCase extends AbstractTestSupport {
 
     @Test
     @SetSystemProperty(key = "cli.service.mvn.local", value = 
"target/tmp-repo")
+    @DisabledIfSystemProperty(named = "camel-cli.instance.type", matches = 
"local-camel-cli-process",
+                              disabledReason = "Test not supported with local 
process service")
     public void setLocalMavenRepoTest() {
         final Path dir = Path.of("target/tmp-repo");
         execute(cliService -> {
-            cliService.version();
+            cliService.execute("init foo.yaml");
+            cliService.execute("run foo.yaml --max-seconds=5");
             Assertions.assertTrue(dir.toFile().exists(), "Check the local 
maven repository is created");
             try {
                 Assertions.assertTrue(Files.list(dir).findFirst().isPresent(),
diff --git 
a/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/it/RunITCase.java
 
b/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/it/RunITCase.java
index d64e43b57875..e58d2bc66ce9 100644
--- 
a/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/it/RunITCase.java
+++ 
b/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/it/RunITCase.java
@@ -20,6 +20,7 @@ import org.apache.camel.support.ObjectHelper;
 import org.apache.camel.test.infra.cli.services.CliService;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIf;
 import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
 import org.junitpioneer.jupiter.ReadsSystemProperty;
 import org.junitpioneer.jupiter.RestoreSystemProperties;
@@ -32,6 +33,7 @@ public class RunITCase extends AbstractTestSupport {
     @ReadsSystemProperty
     @EnabledIfSystemProperty(named = "currentProjectVersion", matches = 
"^(?!\\s*$).+",
                              disabledReason = "currentProjectVersion system 
property must be set")
+    @DisabledIf("isLocalProcessWithSkipInstall")
     public void readPidFromBackgroundExecutionInCurrentVersionTest() {
         String currentCamelVersion = 
System.getProperty("currentProjectVersion");
         System.setProperty("cli.service.version", currentCamelVersion);
@@ -41,6 +43,7 @@ public class RunITCase extends AbstractTestSupport {
 
     @Test
     @SetSystemProperty(key = "cli.service.version", value = "4.14.2")
+    @DisabledIf("isLocalProcessWithSkipInstall")
     public void readPidFromBackgroundExecutionInPreviousVersionTest() {
         execute(this::checkPidFromBackgroundExec);
     }
diff --git 
a/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/services/CliBuiltContainer.java
 
b/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/services/CliBuiltContainer.java
index 9d359629d537..2dee9d5b2b7b 100644
--- 
a/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/services/CliBuiltContainer.java
+++ 
b/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/services/CliBuiltContainer.java
@@ -66,7 +66,7 @@ public class CliBuiltContainer extends 
GenericContainer<CliBuiltContainer> {
             UID = (Integer) Files.getAttribute(target, "unix:uid");
             GID = (Integer) Files.getAttribute(target, "unix:gid");
             LOGGER.info("building container using user {} and group {}", UID, 
GID);
-        } catch (IOException e) {
+        } catch (IOException | UnsupportedOperationException e) {
             LOGGER.warn("unable to retrieve user id and group: {}", 
e.getMessage());
         }
     }
diff --git 
a/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/services/CliLocalContainerService.java
 
b/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/services/CliLocalContainerService.java
index 3f71c9958135..9f09e0208af4 100644
--- 
a/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/services/CliLocalContainerService.java
+++ 
b/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/services/CliLocalContainerService.java
@@ -35,7 +35,6 @@ import org.testcontainers.containers.Container;
 
 public class CliLocalContainerService implements CliService, 
ContainerService<CliBuiltContainer> {
     public static final String CONTAINER_NAME = "camel-cli";
-    public static final String MAIN_COMMAND = 
System.getProperty("cli.service.command", "camel");
     private static final Logger LOG = 
LoggerFactory.getLogger(CliLocalContainerService.class);
     private final CliBuiltContainer container;
     private String version;
@@ -114,7 +113,7 @@ public class CliLocalContainerService implements 
CliService, ContainerService<Cl
 
     @Override
     public String execute(String command) {
-        return executeGenericCommand(MAIN_COMMAND + " " + command);
+        return executeGenericCommand(getMainCommand() + " " + command);
     }
 
     @Override
@@ -216,11 +215,6 @@ public class CliLocalContainerService implements 
CliService, ContainerService<Cl
         return container.getSshPassword();
     }
 
-    @Override
-    public String getMainCommand() {
-        return MAIN_COMMAND;
-    }
-
     private static Map<String, String> getHostsMap() {
         return 
Optional.ofNullable(System.getProperty(CliProperties.EXTRA_HOSTS))
                 .map(p -> p.split(","))
diff --git 
a/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/services/CliLocalProcessService.java
 
b/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/services/CliLocalProcessService.java
new file mode 100644
index 000000000000..9a1c44ded42e
--- /dev/null
+++ 
b/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/services/CliLocalProcessService.java
@@ -0,0 +1,480 @@
+/*
+ * 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.test.infra.cli.services;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
+
+import org.apache.camel.test.infra.cli.common.CliProperties;
+import org.apache.camel.util.ObjectHelper;
+import org.apache.camel.util.StringHelper;
+import org.apache.commons.io.FileUtils;
+import org.junit.jupiter.api.Assertions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A {@link CliService} implementation that runs JBang/Camel CLI directly on 
the host via {@link ProcessBuilder},
+ * eliminating the Docker dependency. Requires JBang to be pre-installed on 
the host.
+ */
+public class CliLocalProcessService implements CliService {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(CliLocalProcessService.class);
+    private static final int COMMAND_TIMEOUT_SECONDS = 120;
+    private static final boolean IS_WINDOWS = System.getProperty("os.name", 
"").toLowerCase().startsWith("win");
+
+    private final String repo;
+    private final String branch;
+    private final String jbangVersion;
+    private final String forceToRunVersion;
+    private final String mavenRepos;
+    private final String localMavenRepo;
+    private final boolean skipInstall;
+    private final Path workDir;
+    private final boolean tempWorkDir;
+    private Path mavenSettingsFile;
+    private final List<String> backgroundPids = new ArrayList<>();
+    private String version;
+
+    public CliLocalProcessService() {
+        this.repo = System.getProperty(CliProperties.REPO, "apache/camel");
+        this.branch = System.getProperty(CliProperties.BRANCH, "main");
+        this.jbangVersion = System.getProperty(CliProperties.VERSION, 
"default");
+        this.forceToRunVersion = 
System.getProperty(CliProperties.FORCE_RUN_VERSION, "");
+        this.mavenRepos = System.getProperty(CliProperties.MVN_REPOS);
+        this.localMavenRepo = System.getProperty(CliProperties.MVN_LOCAL_REPO);
+        this.skipInstall = 
Boolean.parseBoolean(System.getProperty(CliProperties.SKIP_INSTALL, "false"));
+
+        String dataFolder = System.getProperty(CliProperties.DATA_FOLDER);
+        if (ObjectHelper.isNotEmpty(dataFolder)) {
+            this.workDir = Path.of(dataFolder);
+            this.tempWorkDir = false;
+        } else {
+            try {
+                this.workDir = Files.createTempDirectory("camel-cli-process-");
+            } catch (IOException e) {
+                throw new RuntimeException("Failed to create temp work 
directory", e);
+            }
+            this.tempWorkDir = true;
+        }
+    }
+
+    @Override
+    public void registerProperties() {
+        // no-op
+    }
+
+    @Override
+    public void initialize() {
+        LOG.info("Initializing local process CLI service");
+
+        if (ObjectHelper.isNotEmpty(localMavenRepo)) {
+            createMavenSettingsFile();
+        }
+
+        if (skipInstall) {
+            LOG.info("Skipping JBang installation, using existing camel 
command");
+        } else {
+            backupUserFiles();
+
+            if (!isJBangInstalled()) {
+                installJBang();
+            }
+
+            executeGenericCommand(String.format("jbang trust add 
https://github.com/%s/";, repo));
+
+            String installCmd;
+            if ("default".equals(jbangVersion)) {
+                installCmd = String.format("jbang app install --force --fresh 
camel@%s/%s", repo, branch);
+            } else {
+                installCmd = String.format(
+                        "jbang app install --force --fresh 
-Dcamel.jbang.version=%s camel@%s/%s", jbangVersion, repo, branch);
+            }
+            executeGenericCommand(installCmd);
+        }
+
+        if (ObjectHelper.isNotEmpty(forceToRunVersion)) {
+            LOG.info("force to use version {}", forceToRunVersion);
+            execute("version set " + forceToRunVersion);
+        }
+        if (ObjectHelper.isNotEmpty(mavenRepos)) {
+            LOG.info("set repositories {}", mavenRepos);
+            execute(String.format("config set repos=%s", mavenRepos));
+        }
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("Camel JBang version {}", version());
+        }
+    }
+
+    @Override
+    public void shutdown() {
+        LOG.info("Shutting down local process CLI service");
+        if (isCamelCommandAvailable()) {
+            stopBackgroundProcesses();
+            if (!skipInstall) {
+                try {
+                    executeGenericCommand("jbang app uninstall camel");
+                } catch (Exception e) {
+                    LOG.warn("Failed to uninstall camel jbang app: {}", 
e.getMessage());
+                }
+            }
+        }
+        if (mavenSettingsFile != null) {
+            FileUtils.deleteQuietly(mavenSettingsFile.toFile());
+        }
+        if (tempWorkDir) {
+            FileUtils.deleteQuietly(workDir.toFile());
+        } else {
+            
FileUtils.deleteQuietly(Path.of(workDir.toFile().getAbsolutePath(), 
".camel-jbang").toFile());
+        }
+        FileUtils.deleteQuietly(new File(".camel-jbang"));
+    }
+
+    @Override
+    public String execute(String command) {
+        String camelCommand = getMainCommand() + " " + command;
+        if (mavenSettingsFile != null && command.startsWith("run ")) {
+            camelCommand += " --maven-settings=" + 
mavenSettingsFile.toAbsolutePath();
+        }
+        return executeGenericCommand(camelCommand);
+    }
+
+    @Override
+    public String executeBackground(String command) {
+        final String pid = StringHelper.after(execute(command.concat(" 
--background")), "PID:").trim();
+        String cleanPid = org.apache.camel.support.ObjectHelper.isNumber(pid) 
? pid : StringHelper.before(pid, " ");
+        if (cleanPid != null) {
+            backgroundPids.add(cleanPid);
+        }
+        return cleanPid;
+    }
+
+    @Override
+    public String executeGenericCommand(String command) {
+        try {
+            LOG.debug("Executing command: {}", command);
+
+            ProcessBuilder pb;
+            if (IS_WINDOWS) {
+                pb = new ProcessBuilder("cmd", "/c", command);
+            } else {
+                pb = new ProcessBuilder("/bin/bash", "-c", command);
+            }
+
+            pb.directory(workDir.toFile());
+
+            // Prepend ~/.jbang/bin to PATH
+            String jbangBin = Path.of(System.getProperty("user.home"), 
".jbang", "bin").toString();
+            String currentPath = pb.environment().getOrDefault("PATH", "");
+            pb.environment().put("PATH", jbangBin + (IS_WINDOWS ? ";" : ":") + 
currentPath);
+
+            if (ObjectHelper.isNotEmpty(localMavenRepo)) {
+                pb.environment().put("JBANG_REPO", localMavenRepo);
+            }
+
+            Process process = pb.start();
+
+            // Read stderr concurrently to avoid deadlocks
+            CompletableFuture<String> stderrFuture = 
CompletableFuture.supplyAsync(() -> {
+                try (BufferedReader reader
+                        = new BufferedReader(new 
InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) {
+                    StringBuilder sb = new StringBuilder();
+                    String line;
+                    while ((line = reader.readLine()) != null) {
+                        sb.append(line).append(System.lineSeparator());
+                    }
+                    return sb.toString();
+                } catch (IOException e) {
+                    return e.getMessage();
+                }
+            });
+
+            String stdout;
+            try (BufferedReader reader
+                    = new BufferedReader(new 
InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
+                StringBuilder sb = new StringBuilder();
+                String line;
+                while ((line = reader.readLine()) != null) {
+                    sb.append(line).append(System.lineSeparator());
+                }
+                stdout = sb.toString();
+            }
+
+            boolean finished = process.waitFor(COMMAND_TIMEOUT_SECONDS, 
TimeUnit.SECONDS);
+            if (!finished) {
+                process.destroyForcibly();
+                Assertions.fail(String.format("command '%s' timed out after %d 
seconds", command, COMMAND_TIMEOUT_SECONDS));
+            }
+
+            String stderr = stderrFuture.join();
+
+            if (process.exitValue() != 0) {
+                Assertions.fail(String.format("command %s failed with output 
%s and error %s", command, stdout, stderr));
+            }
+
+            if (LOG.isDebugEnabled()) {
+                if (ObjectHelper.isNotEmpty(stdout)) {
+                    LOG.debug("result out {}", stdout);
+                }
+                if (ObjectHelper.isNotEmpty(stderr)) {
+                    LOG.debug("result error {}", stderr);
+                }
+            }
+
+            return stdout;
+        } catch (IOException | InterruptedException e) {
+            LOG.error("ERROR running command: {}", command, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public void copyFileInternally(String source, String destination) {
+        try {
+            Files.copy(Path.of(source), Path.of(destination), 
StandardCopyOption.REPLACE_EXISTING);
+        } catch (IOException e) {
+            Assertions.fail(String.format("unable to copy file %s to %s", 
source, destination), e);
+        }
+    }
+
+    @Override
+    public String getMountPoint() {
+        return workDir.toAbsolutePath().toString();
+    }
+
+    @Override
+    public String getContainerLogs() {
+        LOG.warn("getContainerLogs() is not supported for local process 
service");
+        return "";
+    }
+
+    @Override
+    public int getDevConsolePort() {
+        return 8080;
+    }
+
+    @Override
+    public Stream<String> listDirectory(String directoryPath) {
+        try {
+            return Files.list(Path.of(directoryPath))
+                    .map(p -> p.getFileName().toString());
+        } catch (IOException e) {
+            Assertions.fail("unable to list " + directoryPath, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public String id() {
+        return "local-" + ProcessHandle.current().pid();
+    }
+
+    @Override
+    public String version() {
+        return Optional.ofNullable(version)
+                .orElseGet(() -> {
+                    final String versionSummary = execute("version");
+                    if (versionSummary.contains("User configuration") && 
versionSummary.contains("camel-version = ")) {
+                        version = StringHelper.between(versionSummary, 
"camel-version = ", "\n").trim();
+                    }
+                    if (version == null) {
+                        version = StringHelper.between(versionSummary, "Camel 
JBang version:", "\n").trim();
+                    }
+                    return version;
+                });
+    }
+
+    private boolean isCamelCommandAvailable() {
+        try {
+            String checkCmd = IS_WINDOWS
+                    ? "where " + getMainCommand()
+                    : "which " + getMainCommand();
+            ProcessBuilder pb = IS_WINDOWS
+                    ? new ProcessBuilder("cmd", "/c", checkCmd)
+                    : new ProcessBuilder("/bin/bash", "-c", checkCmd);
+            String jbangBin = Path.of(System.getProperty("user.home"), 
".jbang", "bin").toString();
+            String currentPath = pb.environment().getOrDefault("PATH", "");
+            pb.environment().put("PATH", jbangBin + (IS_WINDOWS ? ";" : ":") + 
currentPath);
+            pb.redirectErrorStream(true);
+            Process process = pb.start();
+            boolean finished = process.waitFor(30, TimeUnit.SECONDS);
+            if (!finished) {
+                process.destroyForcibly();
+                return false;
+            }
+            boolean available = process.exitValue() == 0;
+            LOG.info("Camel command '{}' available: {}", getMainCommand(), 
available);
+            return available;
+        } catch (IOException | InterruptedException e) {
+            LOG.info("Camel command '{}' not found: {}", getMainCommand(), 
e.getMessage());
+            return false;
+        }
+    }
+
+    private boolean isJBangInstalled() {
+        try {
+            String jbangBin = Path.of(System.getProperty("user.home"), 
".jbang", "bin").toString();
+            String pathSeparator = IS_WINDOWS ? ";" : ":";
+            ProcessBuilder pb = IS_WINDOWS
+                    ? new ProcessBuilder("cmd", "/c", "jbang version")
+                    : new ProcessBuilder("/bin/bash", "-c", "jbang version");
+            String currentPath = pb.environment().getOrDefault("PATH", "");
+            pb.environment().put("PATH", jbangBin + pathSeparator + 
currentPath);
+            pb.redirectErrorStream(true);
+            Process process = pb.start();
+            boolean finished = process.waitFor(30, TimeUnit.SECONDS);
+            if (!finished) {
+                process.destroyForcibly();
+                return false;
+            }
+            boolean installed = process.exitValue() == 0;
+            LOG.info("JBang installed: {}", installed);
+            return installed;
+        } catch (IOException | InterruptedException e) {
+            LOG.info("JBang not found: {}", e.getMessage());
+            return false;
+        }
+    }
+
+    private void installJBang() {
+        LOG.info("Installing JBang");
+        try {
+            ProcessBuilder pb;
+            if (IS_WINDOWS) {
+                String script = "iex \"& { $(iwr -useb https://ps.jbang.dev) } 
app setup\"";
+                String encoded = java.util.Base64.getEncoder().encodeToString(
+                        script.getBytes(StandardCharsets.UTF_16LE));
+                pb = new ProcessBuilder("powershell", "-NoProfile", 
"-EncodedCommand", encoded);
+            } else {
+                pb = new ProcessBuilder(
+                        "/bin/bash", "-c",
+                        "curl -Ls https://sh.jbang.dev | bash -s - app setup");
+            }
+            pb.directory(workDir.toFile());
+            pb.redirectErrorStream(true);
+            Process process = pb.start();
+
+            String output;
+            try (BufferedReader reader
+                    = new BufferedReader(new 
InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
+                StringBuilder sb = new StringBuilder();
+                String line;
+                while ((line = reader.readLine()) != null) {
+                    sb.append(line).append(System.lineSeparator());
+                }
+                output = sb.toString();
+            }
+
+            boolean finished = process.waitFor(COMMAND_TIMEOUT_SECONDS, 
TimeUnit.SECONDS);
+            if (!finished) {
+                process.destroyForcibly();
+                Assertions.fail("JBang installation timed out");
+            }
+            if (process.exitValue() != 0) {
+                Assertions.fail("JBang installation failed: " + output);
+            }
+            LOG.info("JBang installed successfully");
+        } catch (IOException | InterruptedException e) {
+            throw new RuntimeException("Failed to install JBang", e);
+        }
+    }
+
+    private void stopBackgroundProcesses() {
+        // Try graceful stop first
+        try {
+            execute("stop");
+        } catch (Exception e) {
+            LOG.warn("Failed to stop camel instances gracefully: {}", 
e.getMessage());
+        }
+        // Force kill any remaining tracked PIDs
+        for (String pid : backgroundPids) {
+            try {
+                ProcessHandle.of(Long.parseLong(pid)).ifPresent(ph -> {
+                    LOG.info("Force killing background process {}", pid);
+                    ph.destroyForcibly();
+                });
+            } catch (NumberFormatException e) {
+                LOG.warn("Invalid PID: {}", pid);
+            }
+        }
+        backgroundPids.clear();
+    }
+
+    private Path getUserPropertiesFile() {
+        return Path.of(System.getProperty("user.home"), 
".camel-jbang-user.properties");
+    }
+
+    private void backupUserFiles() {
+        backupFile(getUserPropertiesFile());
+        backupFile(Path.of(System.getProperty("user.home"), 
".camel-jbang-plugins.json"));
+    }
+
+    private void backupFile(Path file) {
+        if (Files.exists(file)) {
+            try {
+                Path backup = Path.of(file + ".backup" + 
System.currentTimeMillis());
+                Files.move(file, backup, StandardCopyOption.REPLACE_EXISTING);
+                LOG.info("Backed up {} to {}", file, backup);
+            } catch (IOException e) {
+                LOG.warn("Failed to backup {}: {}", file, e.getMessage());
+            }
+        }
+    }
+
+    private void createMavenSettingsFile() {
+        try {
+            mavenSettingsFile = Files.createTempFile(workDir, 
"maven-settings", ".xml");
+            String settingsContent = String.format(
+                    "<?xml version=\"1.0\" encoding=\"UTF-8\"?>%n"
+                                                   + "<settings 
xmlns=\"http://maven.apache.org/SETTINGS/1.0.0\"%n";
+                                                   + "          
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"%n";
+                                                   + "          
xsi:schemaLocation=\"http://maven.apache.org/SETTINGS/1.0.0 "
+                                                   + 
"http://maven.apache.org/xsd/settings-1.0.0.xsd\";>%n"
+                                                   + "    
<localRepository>%s</localRepository>%n"
+                                                   + "</settings>%n",
+                    Path.of(localMavenRepo).toAbsolutePath());
+            Files.writeString(mavenSettingsFile, settingsContent, 
StandardCharsets.UTF_8);
+            LOG.info("Created temporary Maven settings file at {} with 
localRepository={}", mavenSettingsFile,
+                    localMavenRepo);
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to create temporary Maven 
settings file", e);
+        }
+    }
+
+    @Override
+    public int getSshPort() {
+        throw new UnsupportedOperationException("SSH is not supported for 
local process service");
+    }
+
+    @Override
+    public String getSshPassword() {
+        throw new UnsupportedOperationException("SSH is not supported for 
local process service");
+    }
+
+}
diff --git 
a/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/services/CliService.java
 
b/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/services/CliService.java
index 4b6e49ea2262..9c81eff5e55d 100644
--- 
a/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/services/CliService.java
+++ 
b/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/services/CliService.java
@@ -75,5 +75,7 @@ public interface CliService extends BeforeEachCallback, 
AfterEachCallback, TestS
 
     String getSshPassword();
 
-    String getMainCommand();
+    default String getMainCommand() {
+        return System.getProperty("cli.service.command", "camel");
+    }
 }
diff --git 
a/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/services/CliServiceFactory.java
 
b/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/services/CliServiceFactory.java
index 03f5eae91bcf..bad418ff5cad 100644
--- 
a/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/services/CliServiceFactory.java
+++ 
b/test-infra/camel-test-infra-cli/src/test/java/org/apache/camel/test/infra/cli/services/CliServiceFactory.java
@@ -30,6 +30,7 @@ public final class CliServiceFactory {
     public static CliService createService() {
         return builder()
                 .addLocalMapping(CliLocalContainerService::new)
+                .addMapping("local-camel-cli-process", 
CliLocalProcessService::new)
                 .build();
     }
 }
diff --git 
a/test-infra/camel-test-infra-common/src/test/java/org/apache/camel/test/infra/common/services/TestServiceUtil.java
 
b/test-infra/camel-test-infra-common/src/test/java/org/apache/camel/test/infra/common/services/TestServiceUtil.java
index de979691f9eb..ea40bb13e337 100644
--- 
a/test-infra/camel-test-infra-common/src/test/java/org/apache/camel/test/infra/common/services/TestServiceUtil.java
+++ 
b/test-infra/camel-test-infra-common/src/test/java/org/apache/camel/test/infra/common/services/TestServiceUtil.java
@@ -70,14 +70,18 @@ public final class TestServiceUtil {
      */
     public static void logAndRethrow(TestService service, ExtensionContext 
extensionContext, Exception exception)
             throws Exception {
-        final Object testInstance = 
extensionContext.getTestInstance().orElse(null);
+        if (extensionContext != null) {
+            final Object testInstance = 
extensionContext.getTestInstance().orElse(null);
 
-        if (testInstance != null) {
-            LOG.error("Failed to initialize service {} for test {} on ({})", 
service.getClass().getSimpleName(),
-                    extensionContext.getDisplayName(), 
testInstance.getClass().getName());
+            if (testInstance != null) {
+                LOG.error("Failed to initialize service {} for test {} on 
({})", service.getClass().getSimpleName(),
+                        extensionContext.getDisplayName(), 
testInstance.getClass().getName());
+            } else {
+                LOG.error("Failed to initialize service {} for test {}", 
service.getClass().getSimpleName(),
+                        extensionContext.getDisplayName());
+            }
         } else {
-            LOG.error("Failed to initialize service {} for test {}", 
service.getClass().getSimpleName(),
-                    extensionContext.getDisplayName());
+            LOG.error("Failed to initialize service {}", 
service.getClass().getSimpleName());
         }
 
         throw exception;

Reply via email to