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 5186b4b03f5b CAMEL-25391: camel-jbang - ActionClient, ProcessFinder 
and shared --timeout (#27914)
5186b4b03f5b is described below

commit 5186b4b03f5b7c8024d64f351b33ce41b9be7390
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 16 01:29:34 2026 +0200

    CAMEL-25391: camel-jbang - ActionClient, ProcessFinder and shared --timeout 
(#27914)
    
    One ProcessFinder finds the running integrations by name or pid (a prefix
    unless the name ends with !, wildcards or a regular expression), and
    requireOne/requireAny exit with 3 when nothing matches. A pid must be a
    running Camel integration, and an integration whose Camel has stopped is
    skipped by every command.
    
    One ActionClient talks to them: a file per request from Camel 4.21, the
    shared action file before, with the results OK, ERROR (and not-found),
    TIMEOUT and UNCONFIRMED as the exit codes 0, 1, 3 and 124. --timeout takes
    millis or a duration such as 30s.
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  11 +
 .../modules/ROOT/pages/camel-jbang-managing.adoc   |   4 +-
 .../jbang-commands/camel-jbang-cmd-receive.adoc    |   2 +-
 .../pages/jbang-commands/camel-jbang-cmd-send.adoc |   2 +-
 .../camel-jbang-eval-expression.adoc               |   2 +-
 .../jbang-commands/camel-jbang-semantic-audit.adoc |   2 +-
 .../jbang-commands/camel-jbang-semantic-eval.adoc  |   2 +-
 .../jbang-commands/camel-jbang-semantic-get.adoc   |   2 +-
 .../camel-jbang-semantic-monitoring.adoc           |   2 +-
 .../pages/jbang-commands/camel-jbang-semantic.adoc |   2 +-
 .../camel-jbang-transform-message.adoc             |   2 +-
 .../META-INF/camel-jbang-commands-metadata.json    |   8 +-
 .../camel/dsl/jbang/core/commands/Debug.java       |  52 +---
 .../core/commands/action/ActionBaseCommand.java    |  58 +---
 .../core/commands/action/CamelReceiveAction.java   |   5 +-
 .../core/commands/action/CamelSendAction.java      |   6 +-
 .../commands/action/EvalExpressionCommand.java     |   5 +-
 .../commands/action/SemanticActionCommand.java     |   5 +-
 .../commands/action/TransformMessageAction.java    |   5 +-
 .../core/commands/process/ProcessBaseCommand.java  |  52 +---
 .../camel/dsl/jbang/core/common/ActionClient.java  | 327 +++++++++++++++++++++
 .../camel/dsl/jbang/core/common/ProcessFinder.java | 226 ++++++++++++++
 .../camel/dsl/jbang/core/common/RuntimeHelper.java | 123 +-------
 .../dsl/jbang/core/common/TimeoutConverter.java    |  37 +++
 .../dsl/jbang/core/common/ActionClientTest.java    | 238 +++++++++++++++
 .../dsl/jbang/core/common/ProcessFinderTest.java   | 207 +++++++++++++
 .../jbang/core/common/TimeoutConverterTest.java    |  47 +++
 27 files changed, 1151 insertions(+), 283 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 7d4edff7dd70..f58c0870708a 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -196,6 +196,17 @@ model better at tool calling, such as `qwen3.6`, is 
installed.
 `bean`). Scripts that read the `REMOTE` column of the table must read `SCOPE` 
instead. The `--json` output keeps
 `remote` and adds `internal`.
 
+=== camel-jbang - finding integrations by name or PID
+
+All commands find running integrations the same way:
+
+* A PID given to a command must be a running Camel integration (a live process 
with a status file in `~/.camel`).
+  Before, any PID was used as is, so `camel stop 1234 --kill` killed whatever 
process had that PID.
+* The commands no longer find an integration whose Camel has stopped (phase 9 
or later), such as the launcher process
+  of an integration that runs in a child process. `camel ps` does not list it 
anymore.
+* The `--timeout` of `camel cmd send`, `camel cmd receive`, `camel eval 
expression`, `camel transform message` and the
+  `camel semantic` commands also takes a duration such as `30s` (a number is 
still millis).
+
 === camel-jbang - exit codes, and errors on stderr
 
 The Camel CLI has a documented set of exit codes (see 
xref:camel-jbang-exit-codes.adoc[Exit Codes and Output]):
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
index bd7ccaabf7a5..2f19964de767 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
@@ -31,7 +31,9 @@ Stopping running Camel integration (pid: 62506)
 ----
 
 NOTE: The name is matched as a prefix — `camel stop d` stops all integrations 
starting with _d_.
-To stop exactly one when names overlap (e.g., `dude` and `dude2`), use the PID 
or append `!`: `camel stop dude!`
+To stop exactly one when names overlap (e.g., `dude` and `dude2`), use the PID 
or append `!`: `camel stop dude!`.
+A PID must be a running Camel integration, and a name or PID that matches 
nothing exits with code `3`
+(see xref:camel-jbang-exit-codes.adoc[Exit Codes and Output]).
 
 Restart an integration (stop + re-launch):
 
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-receive.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-receive.adoc
index 10ead1002ddc..fff76b9c622f 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-receive.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-receive.adoc
@@ -44,7 +44,7 @@ camel cmd receive [options]
 | `--since` | Return messages newer than a relative duration like 5s, 2m, or 
1h. The value is in seconds if no unit specified. |  | String
 | `--sort` | Sort by pid, name or age for showing status of messages | pid | 
String
 | `--tail` | The number of messages from the end to show. Use -1 to read from 
the beginning. Use 0 to read only new lines. Defaults to showing all messages 
from beginning. | -1 | int
-| `--timeout` | Timeout in millis waiting for message to be received | 20000 | 
long
+| `--timeout` | Timeout waiting for message to be received, in millis or as a 
duration such as 30s | 20000 | long
 | `--wide-uri` | List endpoint URI in full details |  | boolean
 | `-h,--help` | Display the help and sub-commands |  | boolean
 |===
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-send.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-send.adoc
index b509f055f75a..517c948ef3fa 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-send.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-send.adoc
@@ -38,7 +38,7 @@ camel cmd send [options]
 | `--show-exchange-properties` | Show exchange properties from response 
message (InOut) | false | boolean
 | `--show-exchange-variables` | Show exchange variables from response message 
(InOut) | false | boolean
 | `--show-headers` | Show message headers from response message (InOut) | true 
| boolean
-| `--timeout` | Timeout in millis waiting for message to be sent (and reply 
message if InOut messaging) | 20000 | long
+| `--timeout` | Timeout waiting for message to be sent (and reply message if 
InOut messaging), in millis or as a duration such as 30s | 20000 | long
 | `-h,--help` | Display the help and sub-commands |  | boolean
 |===
 
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-eval-expression.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-eval-expression.adoc
index 258ebdad7dc5..a902876befe1 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-eval-expression.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-eval-expression.adoc
@@ -29,7 +29,7 @@ camel eval expression [options]
 | `--predicate` | Whether to force evaluating as predicate | false | boolean
 | `--repo,--repos` | Additional maven repositories for download on-demand (Use 
commas to separate multiple repositories) |  | String
 | `--template` | The template to use for evaluating (prefix with file: to 
refer to loading template from file) |  | String
-| `--timeout` | Timeout in millis waiting for evaluation to be done | 10000 | 
long
+| `--timeout` | Timeout waiting for evaluation to be done, in millis or as a 
duration such as 30s | 10000 | long
 | `--variable` | Exchange variable (key=value) |  | List
 | `--watch` | Execute periodically and showing output fullscreen |  | boolean
 | `-h,--help` | Display the help and sub-commands |  | boolean
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-audit.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-audit.adoc
index 58188160ae3c..0397c8f7062d 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-audit.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-audit.adoc
@@ -42,7 +42,7 @@ camel semantic audit [options]
 | `--score-max` | Inclusive numeric upper bound; requires --result-type=SCORE 
|  | String
 | `--score-min` | Inclusive numeric lower bound; requires --result-type=SCORE 
|  | String
 | `--since` | Earliest timestamp, inclusive, in ISO-8601 format with a UTC 
offset |  | String
-| `--timeout` | Timeout in milliseconds waiting for the integration | 60000 | 
long
+| `--timeout` | Timeout waiting for the integration, in millis or as a 
duration such as 30s | 60000 | long
 | `--until` | Latest timestamp, exclusive, in ISO-8601 format with a UTC 
offset |  | String
 | `-h,--help` | Display the help and sub-commands |  | boolean
 |===
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-eval.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-eval.adoc
index 4415aef5c4bf..9799ec5662d0 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-eval.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-eval.adoc
@@ -28,7 +28,7 @@ camel semantic eval [options]
 | `--json` | Output a single JSON document |  | boolean
 | `--operation` | Expert operation to call directly |  | String
 | `--parameter` | Expert parameter (key=value; use json: for a typed value) |  
| List
-| `--timeout` | Timeout in milliseconds waiting for the integration | 60000 | 
long
+| `--timeout` | Timeout waiting for the integration, in millis or as a 
duration such as 30s | 60000 | long
 | `--variable` | Sample exchange-local variable (key=value; use json: for a 
typed value) |  | List
 | `-h,--help` | Display the help and sub-commands |  | boolean
 |===
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-get.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-get.adoc
index 78378a9f5756..d7c647008f87 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-get.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-get.adoc
@@ -21,7 +21,7 @@ camel semantic get [options]
 | Option | Description | Default | Type
 | `--expert` | Show the operations and parameter contract of this expert |  | 
String
 | `--json` | Output a single JSON document |  | boolean
-| `--timeout` | Timeout in milliseconds waiting for the integration | 60000 | 
long
+| `--timeout` | Timeout waiting for the integration, in millis or as a 
duration such as 30s | 60000 | long
 | `-h,--help` | Display the help and sub-commands |  | boolean
 |===
 
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-monitoring.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-monitoring.adoc
index 7f54c47638f7..e0601985ef3f 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-monitoring.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-monitoring.adoc
@@ -21,7 +21,7 @@ camel semantic monitoring [options]
 | Option | Description | Default | Type
 | `--json` | Output a single JSON document |  | boolean
 | `--source` | Producer or configured metric source name |  | String
-| `--timeout` | Timeout in milliseconds waiting for the integration | 60000 | 
long
+| `--timeout` | Timeout waiting for the integration, in millis or as a 
duration such as 30s | 60000 | long
 | `-h,--help` | Display the help and sub-commands |  | boolean
 |===
 
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic.adoc
index 0ec56653637b..eadfc4c6c020 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic.adoc
@@ -33,7 +33,7 @@ camel semantic [options]
 | Option | Description | Default | Type
 | `--expert` | Show the operations and parameter contract of this expert |  | 
String
 | `--json` | Output a single JSON document |  | boolean
-| `--timeout` | Timeout in milliseconds waiting for the integration | 60000 | 
long
+| `--timeout` | Timeout waiting for the integration, in millis or as a 
duration such as 30s | 60000 | long
 | `-h,--help` | Display the help and sub-commands |  | boolean
 |===
 
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-transform-message.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-transform-message.adoc
index 634ac9b8a5a1..3c53c970aae7 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-transform-message.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-transform-message.adoc
@@ -38,7 +38,7 @@ camel transform message [options]
 | `--show-headers` | Show message headers from the output message | true | 
boolean
 | `--source` | Instead of using external template file then refer to an 
existing Camel route source with inlined Camel language expression in a route. 
(use :line-number or :id to refer to the exact location of the EIP to use) |  | 
String
 | `--template` | The template to use for message transformation (prefix with 
file: to refer to loading template from file) |  | String
-| `--timeout` | Timeout in millis waiting for message to be transformed | 
20000 | long
+| `--timeout` | Timeout waiting for message to be transformed, in millis or as 
a duration such as 30s | 20000 | long
 | `--watch` | Execute periodically and showing output fullscreen |  | boolean
 | `-h,--help` | Display the help and sub-commands |  | boolean
 |===
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index c9b2b774adfd..6363d0efb127 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -3,7 +3,7 @@
     { "name": "ask", "fullName": "ask", "description": "Ask a question about a 
running Camel application using AI", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Ask", "options": [ { "names": 
"--api-key", "description": "API key. Also reads ANTHROPIC_API_KEY, 
OPENAI_API_KEY, WATSONX_APIKEY, or LLM_API_KEY env vars", "javaType": 
"java.lang.String", "type": "string" }, { "names": "--api-type", "description": 
"API type: 'ollama', 'openai', 'anthropic', or 'watsonx'", "javaType":  [...]
     { "name": "bind", "fullName": "bind", "description": "DEPRECATED: Bind 
source and sink Kamelets as a new Camel integration", "deprecated": true, 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.bind.Bind", "options": 
[ { "names": "--error-handler", "description": "Add error handler 
(none|log|sink:<endpoint>). Sink endpoints are expected in the format 
[[apigroup\/]version:]kind:[namespace\/]name, plain Camel URIs or Kamelet 
name.", "javaType": "java.lang.String", "type": "stri [...]
     { "name": "catalog", "fullName": "catalog", "description": "List artifacts 
from Camel Catalog", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [ 
{ "names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"component", "fullName": "catalog component", "description": "List components 
from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...]
-    { "name": "cmd", "fullName": "cmd", "description": "Performs commands in 
the running Camel integrations, such as start\/stop route, or change logging 
levels.", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"browse", "fullName": "cmd browse", "description": "Browse pending messages on 
endpoints [...]
+    { "name": "cmd", "fullName": "cmd", "description": "Performs commands in 
the running Camel integrations, such as start\/stop route, or change logging 
levels.", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"browse", "fullName": "cmd browse", "description": "Browse pending messages on 
endpoints [...]
     { "name": "completion", "fullName": "completion", "description": "Generate 
completion script for bash\/zsh", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names": 
"-h,--help", "description": "Display the help and sub-commands", "javaType": 
"boolean", "type": "boolean" } ] },
     { "name": "config", "fullName": "config", "description": "Get and set user 
configuration values", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", 
"fullName": "config get", "description": "Display user configuration value", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...]
     { "name": "debug", "fullName": "debug", "description": "Debug local Camel 
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug", 
"options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd 
HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background", "description": "Run in the background", "defaultValue": 
"false", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background-wait", "description": "To  [...]
@@ -12,7 +12,7 @@
     { "name": "dirty", "fullName": "dirty", "description": "Check if there are 
dirty files from previous Camel runs that did not terminate gracefully", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.Dirty", 
"options": [ { "names": "--clean", "description": "Clean dirty files which are 
no longer in use", "defaultValue": "false", "javaType": "boolean", "type": 
"boolean" }, { "names": "-h,--help", "description": "Display the help and 
sub-commands", "javaType": "boolean", " [...]
     { "name": "doc", "fullName": "doc", "description": "Shows documentation 
for kamelet, component, and other Camel resources", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.catalog.CatalogDoc", "options": [ { 
"names": "--camel-version", "description": "To use a different Camel version 
than the default version", "javaType": "java.lang.String", "type": "string" }, 
{ "names": "--download", "description": "Whether to allow automatic downloading 
JAR dependencies (over the internet [...]
     { "name": "doctor", "fullName": "doctor", "description": "Checks the 
environment and reports potential issues", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Doctor", "options": [ { "names": 
"--json", "description": "Output in JSON Format: the status (OK, WARN or FAIL) 
of each check", "javaType": "boolean", "type": "boolean" }, { "names": 
"-h,--help", "description": "Display the help and sub-commands", "javaType": 
"boolean", "type": "boolean" } ] },
-    { "name": "eval", "fullName": "eval", "description": "Evaluate Camel 
expressions and scripts", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.EvalCommand", "options": [ { "names": 
"-h,--help", "description": "Display the help and sub-commands", "javaType": 
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "expression", 
"fullName": "eval expression", "description": "Evaluates Camel expression", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.EvalEx [...]
+    { "name": "eval", "fullName": "eval", "description": "Evaluate Camel 
expressions and scripts", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.EvalCommand", "options": [ { "names": 
"-h,--help", "description": "Display the help and sub-commands", "javaType": 
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "expression", 
"fullName": "eval expression", "description": "Evaluates Camel expression", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.EvalEx [...]
     { "name": "explain", "fullName": "explain", "description": "Explain what a 
Camel route does using AI\/LLM", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Explain", "options": [ { "names": 
"--api-key", "description": "API key for authentication. Also reads 
ANTHROPIC_API_KEY, OPENAI_API_KEY, WATSONX_APIKEY, or LLM_API_KEY env vars", 
"javaType": "java.lang.String", "type": "string" }, { "names": "--api-type", 
"description": "API type: 'ollama', 'openai' (OpenAI-compatible), ' [...]
     { "name": "export", "fullName": "export", "description": "Export to other 
runtimes (Camel Main, Spring Boot, or Quarkus)", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Export", "options": [ { "names": 
"--build-property", "description": "Maven build properties, ex. 
--build-property=prop1=foo", "javaType": "java.util.List", "type": "array" }, { 
"names": "--camel-spring-boot-version", "description": "Camel version to use 
with Spring Boot", "javaType": "java.lang.String", "ty [...]
     { "name": "get", "fullName": "get", "description": "Get status of Camel 
integrations", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.process.CamelStatus", "options": [ { 
"names": "--watch", "description": "Execute periodically and showing output 
fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": 
"-h,--help", "description": "Display the help and sub-commands", "javaType": 
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "activity", 
"fullName": " [...]
@@ -30,12 +30,12 @@
     { "name": "run", "fullName": "run", "description": "Run as local Camel 
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Run", 
"options": [ { "names": "--background", "description": "Run in the background", 
"defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background-wait", "description": "To wait for run in background to startup 
successfully, before returning", "defaultValue": "true", "javaType": "boolean", 
"type": "boolean" }, { [...]
     { "name": "sbom", "fullName": "sbom", "description": "Generate a CycloneDX 
or SPDX SBOM for a specific project", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.SBOMGenerator", "options": [ { 
"names": "--build-property", "description": "Maven build properties, ex. 
--build-property=prop1=foo", "javaType": "java.util.List", "type": "array" }, { 
"names": "--camel-spring-boot-version", "description": "Camel version to use 
with Spring Boot", "javaType": "java.lang.String", "type" [...]
     { "name": "script", "fullName": "script", "description": "Run Camel 
integration as shell script for terminal scripting", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Script", "options": [ { "names": 
"--logging", "description": "Can be used to turn on logging (logs to file in 
<user home>\/.camel directory)", "defaultValue": "false", "javaType": 
"boolean", "type": "boolean" }, { "names": "--logging-level", "description": 
"Logging level (ERROR, WARN, INFO, DEBUG, TRACE)", "d [...]
-    { "name": "semantic", "fullName": "semantic", "description": "List 
semantic definitions and expert contracts", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelSemantic", "options": [ { 
"names": "--expert", "description": "Show the operations and parameter contract 
of this expert", "javaType": "java.lang.String", "type": "string" }, { "names": 
"--json", "description": "Output a single JSON document", "javaType": 
"boolean", "type": "boolean" }, { "names": "--timeou [...]
+    { "name": "semantic", "fullName": "semantic", "description": "List 
semantic definitions and expert contracts", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelSemantic", "options": [ { 
"names": "--expert", "description": "Show the operations and parameter contract 
of this expert", "javaType": "java.lang.String", "type": "string" }, { "names": 
"--json", "description": "Output a single JSON document", "javaType": 
"boolean", "type": "boolean" }, { "names": "--timeou [...]
     { "name": "shell", "fullName": "shell", "description": "Interactive Camel 
CLI shell.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Shell", 
"options": [ { "names": "-h,--help", "description": "Display the help and 
sub-commands", "javaType": "boolean", "type": "boolean" } ] },
     { "name": "stop", "fullName": "stop", "description": "Shuts down running 
Camel integrations", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.process.StopProcess", "options": [ { 
"names": "--kill", "description": "To force killing the process (SIGKILL)", 
"javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", 
"description": "Display the help and sub-commands", "javaType": "boolean", 
"type": "boolean" } ] },
     { "name": "top", "fullName": "top", "description": "Top status of Camel 
integrations", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.process.CamelTop", "options": [ { 
"names": "--watch", "description": "Execute periodically and showing output 
fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": 
"-h,--help", "description": "Display the help and sub-commands", "javaType": 
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "context", 
"fullName": "top  [...]
     { "name": "trace", "fullName": "trace", "description": "Tail message 
traces from running Camel integrations", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelTraceAction", "options": 
[ { "names": "--action", "description": "Action to start, stop, clear, list 
status, or dump traces", "defaultValue": "status", "javaType": 
"java.lang.String", "type": "string" }, { "names": "--ago", "description": "Use 
ago instead of yyyy-MM-dd HH:mm:ss in timestamp.", "javaType": "b [...]
-    { "name": "transform", "fullName": "transform", "description": "Transform 
message or Camel routes", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.TransformCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"dataweave", "fullName": "transform dataweave", "description": "Convert 
DataWeave scripts to DataSonnet format", "sourceClass": "org.apache.camel.dsl. 
[...]
+    { "name": "transform", "fullName": "transform", "description": "Transform 
message or Camel routes", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.TransformCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"dataweave", "fullName": "transform dataweave", "description": "Convert 
DataWeave scripts to DataSonnet format", "sourceClass": "org.apache.camel.dsl. 
[...]
     { "name": "update", "fullName": "update", "description": "Update Camel 
project", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.update.UpdateCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"list", "fullName": "update list", "description": "List available update 
versions for Camel and its runtime variants", "sourceClass": 
"org.apache.camel.dsl.jbang.cor [...]
     { "name": "version", "fullName": "version", "description": "Manage Camel 
versions", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.version.VersionCommand", "options": [ 
{ "names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", 
"fullName": "version get", "description": "Displays current Camel version", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.version.VersionGet", 
[...]
     { "name": "wrapper", "fullName": "wrapper", "description": "Install Camel 
wrapper scripts for version pinning", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.WrapperCommand", "options": [ { 
"names": "--camel-version", "description": "Camel version to pin (defaults to 
current version)", "javaType": "java.lang.String", "type": "string" }, { 
"names": "--command-name", "description": "The name of the command to use 
(camel or camelw)", "defaultValue": "camel", "javaType": "java [...]
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java
index 6a19c526c15f..e24240454e8b 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java
@@ -36,6 +36,7 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
 
 import org.apache.camel.dsl.jbang.core.commands.action.MessageTableHelper;
 import org.apache.camel.dsl.jbang.core.common.CamelCommandHelper;
@@ -43,11 +44,10 @@ import org.apache.camel.dsl.jbang.core.common.CliExitCode;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper;
 import org.apache.camel.dsl.jbang.core.common.PathUtils;
-import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
+import org.apache.camel.dsl.jbang.core.common.ProcessFinder;
 import org.apache.camel.dsl.jbang.core.common.VersionHelper;
 import org.apache.camel.main.KameletMain;
 import org.apache.camel.support.LoggerHelper;
-import org.apache.camel.support.PatternHelper;
 import org.apache.camel.tooling.maven.MavenGav;
 import org.apache.camel.util.FileUtil;
 import org.apache.camel.util.IOHelper;
@@ -1301,51 +1301,9 @@ public class Debug extends Run {
     }
 
     List<Long> findPids(String name) {
-        List<Long> pids = new ArrayList<>();
-
-        // we need to know the pids of the running camel integrations
-        if (name.matches("\\d+")) {
-            return List.of(Long.parseLong(name));
-        } else {
-            if (name.endsWith("!")) {
-                // exclusive this name only
-                name = name.substring(0, name.length() - 1);
-            } else if (!name.endsWith("*")) {
-                // lets be open and match all that starts with this pattern
-                name = name + "*";
-            }
-        }
-
-        final long cur = ProcessHandle.current().pid();
-        final String pattern = name;
-        ProcessHandle.allProcesses()
-                .filter(ph -> ph.pid() != cur)
-                .forEach(ph -> {
-                    JsonObject root = loadStatus(ph.pid());
-                    // there must be a status file for the running Camel 
integration
-                    if (root != null) {
-                        String pName = ProcessHelper.extractName(root, ph);
-                        // ignore file extension, so it is easier to match by 
name
-                        pName = FileUtil.onlyName(pName);
-                        if (pName != null && !pName.isEmpty() && 
PatternHelper.matchPattern(pName, pattern)) {
-                            pids.add(ph.pid());
-                        } else {
-                            // try camel context name
-                            JsonObject context = (JsonObject) 
root.get("context");
-                            if (context != null) {
-                                pName = context.getString("name");
-                                if ("CamelJBang".equals(pName)) {
-                                    pName = null;
-                                }
-                                if (pName != null && !pName.isEmpty() && 
PatternHelper.matchPattern(pName, pattern)) {
-                                    pids.add(ph.pid());
-                                }
-                            }
-                        }
-                    }
-                });
-
-        return pids;
+        // the running integrations that match the name or pid (see 
ProcessFinder)
+        return 
ProcessFinder.find(name).stream().map(ProcessFinder.CamelProcess::pid)
+                .collect(Collectors.toCollection(ArrayList::new));
     }
 
     JsonObject loadStatus(long pid) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionBaseCommand.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionBaseCommand.java
index 8adf0526fccd..a6384a160d57 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionBaseCommand.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionBaseCommand.java
@@ -24,6 +24,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Consumer;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
@@ -33,10 +34,8 @@ import 
org.apache.camel.dsl.jbang.core.common.CamelJBangConstants;
 import org.apache.camel.dsl.jbang.core.common.CliException;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.dsl.jbang.core.common.PathUtils;
-import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
+import org.apache.camel.dsl.jbang.core.common.ProcessFinder;
 import org.apache.camel.main.KameletMain;
-import org.apache.camel.support.PatternHelper;
-import org.apache.camel.util.FileUtil;
 import org.apache.camel.util.StopWatch;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
@@ -78,56 +77,9 @@ abstract class ActionBaseCommand extends CamelCommand {
     }
 
     List<Long> findPids(String name) {
-        List<Long> pids = new ArrayList<>();
-
-        // we need to know the pids of the running camel integrations
-        if (name.matches("\\d+")) {
-            return List.of(Long.parseLong(name));
-        } else {
-            if (name.endsWith("!")) {
-                // exclusive this name only
-                name = name.substring(0, name.length() - 1);
-            } else if (!name.endsWith("*")) {
-                // lets be open and match all that starts with this pattern
-                name = name + "*";
-            }
-        }
-
-        final long cur = ProcessHandle.current().pid();
-        final String pattern = name;
-        ProcessHandle.allProcesses()
-                .filter(ph -> ph.pid() != cur)
-                .forEach(ph -> {
-                    JsonObject root = loadStatus(ph.pid());
-                    // there must be a status file for the running Camel 
integration
-                    if (root != null) {
-                        // skip terminated processes (e.g. launcher process 
for spring-boot/quarkus runtimes)
-                        JsonObject context = (JsonObject) root.get("context");
-                        if (context != null) {
-                            int phase = context.getIntegerOrDefault("phase", 
0);
-                            if (phase >= 9) {
-                                return;
-                            }
-                        }
-                        String pName = ProcessHelper.extractName(root, ph);
-                        // ignore file extension, so it is easier to match by 
name
-                        pName = FileUtil.onlyName(pName);
-                        if (pName != null && !pName.isEmpty() && 
PatternHelper.matchPattern(pName, pattern)) {
-                            pids.add(ph.pid());
-                        } else if (context != null) {
-                            // try camel context name
-                            pName = context.getString("name");
-                            if ("CamelJBang".equals(pName)) {
-                                pName = null;
-                            }
-                            if (pName != null && !pName.isEmpty() && 
PatternHelper.matchPattern(pName, pattern)) {
-                                pids.add(ph.pid());
-                            }
-                        }
-                    }
-                });
-
-        return pids;
+        // the running integrations that match the name or pid (see 
ProcessFinder)
+        return 
ProcessFinder.find(name).stream().map(ProcessFinder.CamelProcess::pid)
+                .collect(Collectors.toCollection(ArrayList::new));
     }
 
     static long extractSince(ProcessHandle ph) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelReceiveAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelReceiveAction.java
index 1207c5f8c422..8fbca6d57d2e 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelReceiveAction.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelReceiveAction.java
@@ -50,6 +50,7 @@ import org.apache.camel.dsl.jbang.core.common.PathUtils;
 import org.apache.camel.dsl.jbang.core.common.PidNameAgeCompletionCandidates;
 import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
 import org.apache.camel.dsl.jbang.core.common.TerminalWidthHelper;
+import org.apache.camel.dsl.jbang.core.common.TimeoutConverter;
 import org.apache.camel.main.KameletMain;
 import org.apache.camel.util.StopWatch;
 import org.apache.camel.util.StringHelper;
@@ -162,8 +163,8 @@ public class CamelReceiveAction extends ActionBaseCommand {
                         description = "Filter messages to only output matching 
text (ignore case).", arity = "0..*")
     String[] grep;
 
-    @CommandLine.Option(names = { "--timeout" }, defaultValue = "20000",
-                        description = "Timeout in millis waiting for message 
to be received")
+    @CommandLine.Option(names = { "--timeout" }, defaultValue = "20000", 
converter = TimeoutConverter.class,
+                        description = "Timeout waiting for message to be 
received, in millis or as a duration such as 30s")
     long timeout = 20000;
 
     @CommandLine.Option(names = { "--show-exchange-properties" }, defaultValue 
= "false",
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
index 304799dade23..7a6a29ab6d2e 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
@@ -40,6 +40,7 @@ import 
org.apache.camel.dsl.jbang.core.commands.MavenResolverMixin;
 import org.apache.camel.dsl.jbang.core.commands.Run;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.dsl.jbang.core.common.TimeoutConverter;
 import org.apache.camel.main.KameletMain;
 import org.apache.camel.util.StringHelper;
 import org.apache.camel.util.TimeUtils;
@@ -97,8 +98,9 @@ public class CamelSendAction extends ActionBaseCommand {
                         description = "Message header (key=value)")
     List<String> headers;
 
-    @CommandLine.Option(names = { "--timeout" }, defaultValue = "20000",
-                        description = "Timeout in millis waiting for message 
to be sent (and reply message if InOut messaging)")
+    @CommandLine.Option(names = { "--timeout" }, defaultValue = "20000", 
converter = TimeoutConverter.class,
+                        description = "Timeout waiting for message to be sent 
(and reply message if InOut messaging),"
+                                      + " in millis or as a duration such as 
30s")
     long timeout = 20000;
 
     @CommandLine.Option(names = { "--show-exchange-properties" }, defaultValue 
= "false",
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/EvalExpressionCommand.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/EvalExpressionCommand.java
index aef385b7b12e..725ac0835a25 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/EvalExpressionCommand.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/EvalExpressionCommand.java
@@ -27,6 +27,7 @@ import org.apache.camel.dsl.jbang.core.commands.Run;
 import org.apache.camel.dsl.jbang.core.common.CliExitCode;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.dsl.jbang.core.common.TimeoutConverter;
 import org.apache.camel.dsl.jbang.core.common.VersionHelper;
 import org.apache.camel.util.StopWatch;
 import org.apache.camel.util.StringHelper;
@@ -81,8 +82,8 @@ public class EvalExpressionCommand extends ActionWatchCommand 
{
                         description = "Exchange variable (key=value)")
     List<String> variables;
 
-    @CommandLine.Option(names = { "--timeout" }, defaultValue = "10000",
-                        description = "Timeout in millis waiting for 
evaluation to be done")
+    @CommandLine.Option(names = { "--timeout" }, defaultValue = "10000", 
converter = TimeoutConverter.class,
+                        description = "Timeout waiting for evaluation to be 
done, in millis or as a duration such as 30s")
     long timeout = 10000;
 
     @CommandLine.Mixin
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticActionCommand.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticActionCommand.java
index 3bf620773181..2226234abb56 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticActionCommand.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticActionCommand.java
@@ -26,6 +26,7 @@ import 
org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import 
org.apache.camel.dsl.jbang.core.commands.exceptionhandler.UsageErrorHandler;
 import org.apache.camel.dsl.jbang.core.common.CliExitCode;
 import org.apache.camel.dsl.jbang.core.common.RuntimeHelper;
+import org.apache.camel.dsl.jbang.core.common.TimeoutConverter;
 import org.apache.camel.dsl.jbang.core.common.VersionHelper;
 import org.apache.camel.util.json.JsonObject;
 import picocli.CommandLine;
@@ -38,8 +39,8 @@ public abstract class SemanticActionCommand extends 
ActionBaseCommand implements
     @CommandLine.Option(names = "--json", description = "Output a single JSON 
document")
     boolean json;
 
-    @CommandLine.Option(names = "--timeout", defaultValue = "60000",
-                        description = "Timeout in milliseconds waiting for the 
integration")
+    @CommandLine.Option(names = "--timeout", defaultValue = "60000", converter 
= TimeoutConverter.class,
+                        description = "Timeout waiting for the integration, in 
millis or as a duration such as 30s")
     long timeout = 60000;
 
     @CommandLine.Spec
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/TransformMessageAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/TransformMessageAction.java
index 8b03e1c1dd37..1c683a0a6628 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/TransformMessageAction.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/TransformMessageAction.java
@@ -28,6 +28,7 @@ import org.apache.camel.dsl.jbang.core.commands.Run;
 import org.apache.camel.dsl.jbang.core.common.CliExitCode;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.dsl.jbang.core.common.TimeoutConverter;
 import org.apache.camel.dsl.jbang.core.common.VersionHelper;
 import org.apache.camel.util.StopWatch;
 import org.apache.camel.util.StringHelper;
@@ -109,8 +110,8 @@ public class TransformMessageAction extends 
ActionWatchCommand {
                         description = "Show exception and stacktrace for 
failed transformation")
     boolean showException = true;
 
-    @CommandLine.Option(names = { "--timeout" }, defaultValue = "20000",
-                        description = "Timeout in millis waiting for message 
to be transformed")
+    @CommandLine.Option(names = { "--timeout" }, defaultValue = "20000", 
converter = TimeoutConverter.class,
+                        description = "Timeout waiting for message to be 
transformed, in millis or as a duration such as 30s")
     long timeout = 20000;
 
     @CommandLine.Option(names = { "--logging-color" }, defaultValue = "true", 
description = "Use colored logging")
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ProcessBaseCommand.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ProcessBaseCommand.java
index b2176c9bf49e..e40587e8cfaf 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ProcessBaseCommand.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ProcessBaseCommand.java
@@ -20,11 +20,11 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
-import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
-import org.apache.camel.support.PatternHelper;
+import org.apache.camel.dsl.jbang.core.common.ProcessFinder;
 import org.apache.camel.util.FileUtil;
 import org.apache.camel.util.StringHelper;
 import org.apache.camel.util.json.JsonObject;
@@ -37,51 +37,9 @@ abstract class ProcessBaseCommand extends CamelCommand {
     }
 
     List<Long> findPids(String name) {
-        List<Long> pids = new ArrayList<>();
-
-        // we need to know the pids of the running camel integrations
-        if (name.matches("\\d+")) {
-            return List.of(Long.parseLong(name));
-        } else {
-            if (name.endsWith("!")) {
-                // exclusive this name only
-                name = name.substring(0, name.length() - 1);
-            } else if (!name.endsWith("*")) {
-                // lets be open and match all that starts with this pattern
-                name = name + "*";
-            }
-        }
-
-        final long cur = ProcessHandle.current().pid();
-        final String pattern = name;
-        ProcessHandle.allProcesses()
-                .filter(ph -> ph.pid() != cur)
-                .forEach(ph -> {
-                    JsonObject root = loadStatus(ph.pid());
-                    // there must be a status file for the running Camel 
integration
-                    if (root != null) {
-                        String pName = ProcessHelper.extractName(root, ph);
-                        // ignore file extension, so it is easier to match by 
name
-                        pName = FileUtil.onlyName(pName);
-                        if (pName != null && !pName.isEmpty() && 
PatternHelper.matchPattern(pName, pattern)) {
-                            pids.add(ph.pid());
-                        } else {
-                            // try camel context name
-                            JsonObject context = (JsonObject) 
root.get("context");
-                            if (context != null) {
-                                pName = context.getString("name");
-                                if ("CamelJBang".equals(pName)) {
-                                    pName = null;
-                                }
-                                if (pName != null && !pName.isEmpty() && 
PatternHelper.matchPattern(pName, pattern)) {
-                                    pids.add(ph.pid());
-                                }
-                            }
-                        }
-                    }
-                });
-
-        return pids;
+        // the running integrations that match the name or pid (see 
ProcessFinder)
+        return 
ProcessFinder.find(name).stream().map(ProcessFinder.CamelProcess::pid)
+                .collect(Collectors.toCollection(ArrayList::new));
     }
 
     public static long extractSince(ProcessHandle ph) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ActionClient.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ActionClient.java
new file mode 100644
index 000000000000..d223a23e2485
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ActionClient.java
@@ -0,0 +1,327 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.common;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.UUID;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.camel.dsl.jbang.core.common.ProcessFinder.CamelProcess;
+import org.apache.camel.util.StopWatch;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+
+/**
+ * Sends actions to a running Camel integration and waits for its answer (see 
camel-cli-connector).
+ * <p/>
+ * An integration on Camel 4.21 or later takes a file per request 
(<tt>~/.camel/{pid}-action-{id}.json</tt>, answered in
+ * <tt>{pid}-output-{id}.json</tt>), so clients never take each other's 
answers; an older integration only reads the
+ * shared <tt>{pid}-action.json</tt>. An integration on Camel 4.24 or later 
answers every action, also one without a
+ * result, so no answer is a timeout; an older integration does not answer an 
action without a result, which is then
+ * sent but not confirmed.
+ */
+public final class ActionClient {
+
+    /**
+     * How long to wait for an answer by default, in millis.
+     */
+    public static final long DEFAULT_TIMEOUT = 10_000;
+
+    private static final long POLL_INTERVAL = 100;
+    private static final Pattern VERSION = Pattern.compile("^(\\d+)\\.(\\d+)");
+
+    /**
+     * How an action went.
+     */
+    public enum Status {
+        /**
+         * The integration answered with a result, or confirmed the action.
+         */
+        OK,
+        /**
+         * The integration answered that the action failed.
+         */
+        ERROR,
+        /**
+         * The integration did not answer in time.
+         */
+        TIMEOUT,
+        /**
+         * The integration took the action, but it is too old (before Camel 
4.24) to confirm an action without a result.
+         */
+        UNCONFIRMED
+    }
+
+    /**
+     * The answer of an integration to an action.
+     *
+     * @param status how the action went
+     * @param reply  the answer of the integration, or <tt>null</tt> when 
there is none
+     * @param error  why the action failed, or did not get an answer; 
<tt>null</tt> when it went well
+     * @param reason the kind of error, such as <tt>not-found</tt>, or 
<tt>null</tt>
+     */
+    public record ActionResult(Status status, JsonObject reply, String error, 
String reason) {
+
+        /**
+         * Whether the action went well (also when an older integration took 
it without confirming it).
+         */
+        public boolean isOk() {
+            return status == Status.OK || status == Status.UNCONFIRMED;
+        }
+
+        /**
+         * Whether the action failed because what it was asked to work on does 
not exist.
+         */
+        public boolean isNotFound() {
+            return status == Status.ERROR && "not-found".equals(reason);
+        }
+
+        /**
+         * The exit code of a command that ends with this result, see {@link 
CliExitCode}.
+         */
+        public int exitCode() {
+            return switch (status) {
+                case OK, UNCONFIRMED -> CliExitCode.OK;
+                case ERROR -> isNotFound() ? CliExitCode.NOT_FOUND : 
CliExitCode.FAILURE;
+                case TIMEOUT -> CliExitCode.TIMEOUT;
+            };
+        }
+
+        /**
+         * The reply when the action went well.
+         *
+         * @throws CliException with the exit code of the result when the 
action failed or timed out
+         */
+        public JsonObject orThrow() {
+            if (isOk()) {
+                return reply;
+            }
+            throw new CliException(exitCode(), error);
+        }
+    }
+
+    private ActionClient() {
+    }
+
+    /**
+     * Runs an action that has a result, such as a route dump, and waits for 
the result.
+     *
+     * @param process the integration
+     * @param action  the action name
+     * @param options sets the options of the action, or <tt>null</tt>
+     * @param timeout how long to wait for the result, in millis
+     */
+    public static ActionResult execute(CamelProcess process, String action, 
Consumer<JsonObject> options, long timeout) {
+        return execute(process.pid(), process.camelVersion(), request(action, 
options), timeout, true);
+    }
+
+    /**
+     * Sends an action without a result, such as a route stop, and waits until 
the integration confirms it.
+     *
+     * @param process the integration
+     * @param action  the action name
+     * @param options sets the options of the action, or <tt>null</tt>
+     * @param timeout how long to wait for the confirmation, in millis
+     */
+    public static ActionResult send(CamelProcess process, String action, 
Consumer<JsonObject> options, long timeout) {
+        return execute(process.pid(), process.camelVersion(), request(action, 
options), timeout, false);
+    }
+
+    /**
+     * Runs an action that has a result, for an integration known by its pid 
only.
+     *
+     * @throws IOException when the action cannot be written
+     */
+    public static ActionResult execute(long pid, JsonObject request, long 
timeout) throws IOException {
+        return run(pid, versionOf(pid), request, timeout, true);
+    }
+
+    private static ActionResult execute(
+            long pid, String camelVersion, JsonObject request, long timeout, 
boolean hasResult) {
+        try {
+            return run(pid, camelVersion, request, timeout, hasResult);
+        } catch (IOException e) {
+            return new ActionResult(
+                    Status.ERROR, null, "Cannot send the action to the 
integration with pid " + pid + ": " + e.getMessage(),
+                    null);
+        }
+    }
+
+    private static ActionResult run(long pid, String camelVersion, JsonObject 
request, long timeout, boolean hasResult)
+            throws IOException {
+        // an unknown version is a recent one (every connector writes its 
version in the status file)
+        boolean confirms = camelVersion == null || isAtLeast(camelVersion, 4, 
24);
+        Exchange<JsonObject> exchange = exchange(pid, camelVersion, request, 
timeout, hasResult || confirms,
+                text -> Jsoner.deserialize(text, (JsonObject) null));
+        if (exchange.reply() != null) {
+            return result(exchange.reply());
+        }
+        if (exchange.taken()) {
+            // the integration took the action (it deletes the file), but does 
not confirm it
+            return new ActionResult(Status.UNCONFIRMED, null, null, null);
+        }
+        String action = request.getString("action");
+        return new ActionResult(
+                Status.TIMEOUT, null, "No answer from the integration with pid 
" + pid + " to the action " + action
+                                      + " within " + timeout + " millis",
+                null);
+    }
+
+    /**
+     * Runs an action and returns the answer as the integration wrote it, for 
tools that pass it on as text.
+     *
+     * @return             the answer, or <tt>null</tt> when there is none in 
time
+     * @throws IOException when the action cannot be written
+     */
+    public static String executeText(long pid, JsonObject request, long 
timeout) throws IOException {
+        return exchange(pid, versionOf(pid), request, timeout, true, text -> 
text).reply();
+    }
+
+    // the answer to an action, as read, or null; and whether the integration 
took the action
+    private record Exchange<T>(T reply, boolean taken) {
+    }
+
+    private static <T> Exchange<T> exchange(
+            long pid, String camelVersion, JsonObject request, long timeout, 
boolean waitForAnswer,
+            Function<String, T> reader)
+            throws IOException {
+        boolean requestFiles = camelVersion == null || isAtLeast(camelVersion, 
4, 21);
+        Path dir = CommandLineHelper.getCamelDir();
+        Path actionFile;
+        Path outputFile;
+        if (requestFiles) {
+            String requestId = UUID.randomUUID().toString();
+            actionFile = dir.resolve(pid + "-action-" + requestId + ".json");
+            outputFile = dir.resolve(pid + "-output-" + requestId + ".json");
+        } else {
+            actionFile = dir.resolve(pid + "-action.json");
+            outputFile = dir.resolve(pid + "-output.json");
+            // an answer left behind by an earlier command
+            Files.deleteIfExists(outputFile);
+        }
+        Path temporary = dir.resolve(pid + "-" + UUID.randomUUID() + ".tmp");
+        try {
+            Files.writeString(temporary, request.toJson());
+            // the integration must never read half an action
+            Files.move(temporary, actionFile, StandardCopyOption.ATOMIC_MOVE, 
StandardCopyOption.REPLACE_EXISTING);
+
+            StopWatch watch = new StopWatch();
+            while (watch.taken() < timeout) {
+                try {
+                    Thread.sleep(Math.min(POLL_INTERVAL, Math.max(1, timeout - 
watch.taken())));
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                    break;
+                }
+                if (waitForAnswer) {
+                    T reply = readReply(outputFile, reader);
+                    if (reply != null) {
+                        return new Exchange<>(reply, true);
+                    }
+                } else if (!Files.exists(actionFile)) {
+                    return new Exchange<>(null, true);
+                }
+            }
+            return new Exchange<>(null, false);
+        } finally {
+            // removing an action the integration has not taken yet cancels it
+            Files.deleteIfExists(temporary);
+            Files.deleteIfExists(actionFile);
+            Files.deleteIfExists(outputFile);
+        }
+    }
+
+    private static <T> T readReply(Path outputFile, Function<String, T> 
reader) {
+        try {
+            if (Files.exists(outputFile)) {
+                String text = Files.readString(outputFile);
+                if (!text.isBlank()) {
+                    // null when the answer is still being written
+                    return reader.apply(text);
+                }
+            }
+        } catch (IOException e) {
+            // the integration writes the answer in place: try again
+        }
+        return null;
+    }
+
+    /**
+     * The result of an answer: an error when its status is <tt>failed</tt> or 
<tt>error</tt>, the same as the connector
+     * does for its WebSocket transport.
+     */
+    static ActionResult result(JsonObject reply) {
+        String status = reply.getString("status");
+        if ("failed".equals(status) || "error".equals(status)) {
+            return new ActionResult(Status.ERROR, reply, messageOf(reply), 
reply.getString("reason"));
+        }
+        return new ActionResult(Status.OK, reply, null, null);
+    }
+
+    private static String messageOf(JsonObject reply) {
+        if (reply.get("exception") instanceof JsonObject exception && 
exception.getString("message") != null) {
+            return exception.getString("message");
+        }
+        if (reply.get("error") instanceof String error && !error.isBlank()) {
+            return error;
+        }
+        String message = reply.getString("message");
+        return message != null && !message.isBlank() ? message : "Action 
failed";
+    }
+
+    private static JsonObject request(String action, Consumer<JsonObject> 
options) {
+        JsonObject request = new JsonObject();
+        request.put("action", action);
+        if (options != null) {
+            options.accept(request);
+        }
+        return request;
+    }
+
+    private static String versionOf(long pid) {
+        try {
+            Path f = CommandLineHelper.getCamelDir().resolve(pid + 
"-status.json");
+            JsonObject status = Files.exists(f) ? 
Jsoner.deserialize(Files.readString(f), (JsonObject) null) : null;
+            if (status != null && status.get("context") instanceof JsonObject 
context) {
+                return context.getString("version");
+            }
+        } catch (Exception e) {
+            // unknown
+        }
+        return null;
+    }
+
+    /**
+     * Whether a Camel version such as <tt>4.24.0-SNAPSHOT</tt> is the given 
major.minor or later.
+     */
+    static boolean isAtLeast(String version, int major, int minor) {
+        Matcher m = VERSION.matcher(version);
+        if (!m.find()) {
+            // not a version we know how to read: assume a recent one
+            return true;
+        }
+        int actualMajor = Integer.parseInt(m.group(1));
+        int actualMinor = Integer.parseInt(m.group(2));
+        return actualMajor > major || actualMajor == major && actualMinor >= 
minor;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ProcessFinder.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ProcessFinder.java
new file mode 100644
index 000000000000..37849967d869
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ProcessFinder.java
@@ -0,0 +1,226 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.common;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.StringJoiner;
+
+import org.apache.camel.support.PatternHelper;
+import org.apache.camel.util.FileUtil;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+
+/**
+ * Finds the running Camel integrations on this machine, by name or pid.
+ * <p/>
+ * An integration is running when its process is alive and has a status file 
in <tt>~/.camel</tt>, which the Camel CLI
+ * connector writes. An integration whose Camel has stopped (phase 9 or later, 
such as the launcher process of an
+ * integration that runs in a child process) is not running anymore.
+ * <p/>
+ * A number is a pid. A name matches the name of the integration (as <tt>camel 
ps</tt> shows it, without file extension)
+ * or its CamelContext name: it is a prefix (<tt>foo</tt> matches 
<tt>foo-bar</tt>) unless it ends with <tt>!</tt>
+ * (<tt>foo!</tt> matches only <tt>foo</tt>), and it may use wildcards and 
regular expressions.
+ */
+public final class ProcessFinder {
+
+    // the phase of a CamelContext that has stopped (see AbstractCamelContext)
+    private static final int STOPPED_PHASE = 9;
+
+    /**
+     * A running Camel integration, and the status it last wrote.
+     *
+     * @param pid    the pid
+     * @param name   the name of the integration: its main class or source file
+     * @param status the status file
+     */
+    public record CamelProcess(long pid, String name, JsonObject status) {
+
+        /**
+         * The name of the CamelContext, or <tt>null</tt>.
+         */
+        public String contextName() {
+            JsonObject context = context();
+            return context != null ? context.getString("name") : null;
+        }
+
+        /**
+         * The Camel version of the integration, or <tt>null</tt> when unknown.
+         */
+        public String camelVersion() {
+            JsonObject context = context();
+            return context != null ? context.getString("version") : null;
+        }
+
+        private JsonObject context() {
+            return status != null && status.get("context") instanceof 
JsonObject context ? context : null;
+        }
+    }
+
+    private ProcessFinder() {
+    }
+
+    /**
+     * Every running Camel integration.
+     */
+    public static List<CamelProcess> findAll() {
+        return scan(null, false);
+    }
+
+    /**
+     * The running Camel integrations that match the name or pid. A name never 
matches this process, as the Camel CLI
+     * can run Camel itself.
+     *
+     * @param nameOrPid a name, a pattern or a pid; <tt>null</tt> or 
<tt>*</tt> for every integration
+     */
+    public static List<CamelProcess> find(String nameOrPid) {
+        if (nameOrPid == null || nameOrPid.isBlank()) {
+            nameOrPid = "*";
+        }
+        return scan(nameOrPid, true);
+    }
+
+    /**
+     * The one running Camel integration that matches the name or pid.
+     *
+     * @throws CliException (exit code 3) when no integration matches, or more 
than one
+     */
+    public static CamelProcess requireOne(String nameOrPid) {
+        List<CamelProcess> found = find(nameOrPid);
+        if (found.isEmpty()) {
+            throw notFound(nameOrPid);
+        }
+        if (found.size() > 1) {
+            throw CliException.notFound("Name or pid " + nameOrPid + " matches 
" + found.size()
+                                        + " running Camel integrations: " + 
describe(found)
+                                        + ". Specify a name or pid that 
matches exactly one.");
+        }
+        return found.get(0);
+    }
+
+    /**
+     * The running Camel integrations that match the name or pid, at least one.
+     *
+     * @throws CliException (exit code 3) when no integration matches
+     */
+    public static List<CamelProcess> requireAny(String nameOrPid) {
+        List<CamelProcess> found = find(nameOrPid);
+        if (found.isEmpty()) {
+            throw notFound(nameOrPid);
+        }
+        return found;
+    }
+
+    private static CliException notFound(String nameOrPid) {
+        return CliException.notFound("No running Camel integration matches: " 
+ nameOrPid);
+    }
+
+    private static String describe(List<CamelProcess> processes) {
+        StringJoiner sj = new StringJoiner(", ");
+        for (CamelProcess p : processes) {
+            sj.add(p.name() != null && !p.name().isEmpty() ? p.pid() + " (" + 
p.name() + ")" : String.valueOf(p.pid()));
+        }
+        return sj.toString();
+    }
+
+    private static List<CamelProcess> scan(String nameOrPid, boolean 
skipThisProcess) {
+        long current = ProcessHandle.current().pid();
+        Map<Long, Path> statusFiles = statusFiles();
+        boolean byPid = nameOrPid != null && nameOrPid.matches("\\d+");
+        long pid = byPid ? Long.parseLong(nameOrPid) : -1;
+        String pattern = nameOrPid == null || byPid ? null : 
pattern(nameOrPid);
+
+        List<CamelProcess> answer = new ArrayList<>();
+        // the live processes among those with a status file
+        ProcessHandle.allProcesses()
+                .filter(ph -> statusFiles.containsKey(ph.pid()))
+                .forEach(ph -> {
+                    if (byPid ? ph.pid() != pid : skipThisProcess && ph.pid() 
== current) {
+                        return;
+                    }
+                    JsonObject status = readStatus(statusFiles.get(ph.pid()));
+                    if (status == null || stopped(status)) {
+                        return;
+                    }
+                    String name = ProcessHelper.extractName(status, ph);
+                    if (pattern == null || matches(name, status, pattern)) {
+                        answer.add(new CamelProcess(ph.pid(), name, status));
+                    }
+                });
+        answer.sort(Comparator.comparingLong(CamelProcess::pid));
+        return answer;
+    }
+
+    private static String pattern(String name) {
+        if (name.endsWith("!")) {
+            // exactly this name
+            return name.substring(0, name.length() - 1);
+        }
+        // a prefix
+        return name.endsWith("*") ? name : name + "*";
+    }
+
+    private static boolean matches(String name, JsonObject status, String 
pattern) {
+        // without file extension, so it is easier to match by name
+        String onlyName = FileUtil.onlyName(name);
+        if (onlyName != null && !onlyName.isEmpty() && 
PatternHelper.matchPattern(onlyName, pattern)) {
+            return true;
+        }
+        if (status.get("context") instanceof JsonObject context) {
+            String contextName = context.getString("name");
+            // the default name of the Camel CLI, which says nothing about the 
integration
+            if (contextName != null && !contextName.isEmpty() && 
!"CamelJBang".equals(contextName)) {
+                return PatternHelper.matchPattern(contextName, pattern);
+            }
+        }
+        return false;
+    }
+
+    private static boolean stopped(JsonObject status) {
+        return status.get("context") instanceof JsonObject context
+                && context.getIntegerOrDefault("phase", 0) >= STOPPED_PHASE;
+    }
+
+    // the status files in ~/.camel, by pid: listing them is much faster than 
looking at every process
+    private static Map<Long, Path> statusFiles() {
+        Map<Long, Path> answer = new HashMap<>();
+        File[] files = CommandLineHelper.getCamelDir().toFile().listFiles((d, 
n) -> n.matches("\\d+-status\\.json"));
+        if (files != null) {
+            for (File f : files) {
+                String n = f.getName();
+                answer.put(Long.parseLong(n.substring(0, n.indexOf('-'))), 
f.toPath());
+            }
+        }
+        return answer;
+    }
+
+    private static JsonObject readStatus(Path file) {
+        try {
+            String text = Files.readString(file);
+            return text.isBlank() ? null : Jsoner.deserialize(text, 
(JsonObject) null);
+        } catch (Exception e) {
+            // being written, or gone
+            return null;
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java
index 4ed9da5462d7..11c819f8ef67 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java
@@ -16,20 +16,13 @@
  */
 package org.apache.camel.dsl.jbang.core.common;
 
-import java.io.File;
 import java.io.IOException;
 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.UUID;
 import java.util.function.Consumer;
-import java.util.function.Function;
 
-import org.apache.camel.support.PatternHelper;
-import org.apache.camel.util.FileUtil;
-import org.apache.camel.util.StopWatch;
 import org.apache.camel.util.json.JsonObject;
 import org.apache.camel.util.json.Jsoner;
 
@@ -70,37 +63,8 @@ public final class RuntimeHelper {
      */
     public static List<ProcessInfo> discoverProcesses() {
         List<ProcessInfo> result = new ArrayList<>();
-        Path camelDir = CommandLineHelper.getCamelDir();
-        File dir = camelDir.toFile();
-        if (!dir.isDirectory()) {
-            return result;
-        }
-
-        File[] statusFiles = dir.listFiles((d, name) -> 
name.matches("\\d+-status\\.json"));
-        if (statusFiles == null) {
-            return result;
-        }
-
-        for (File sf : statusFiles) {
-            String fileName = sf.getName();
-            long pid = Long.parseLong(fileName.substring(0, 
fileName.indexOf('-')));
-            if 
(!ProcessHandle.of(pid).map(ProcessHandle::isAlive).orElse(false)) {
-                continue;
-            }
-            try {
-                JsonObject root = readStatusFromFile(sf.toPath());
-                if (root != null) {
-                    String name = ProcessHelper.extractName(root, 
ProcessHandle.of(pid).orElse(null));
-                    String contextName = null;
-                    JsonObject context = (JsonObject) root.get("context");
-                    if (context != null) {
-                        contextName = context.getString("name");
-                    }
-                    result.add(new ProcessInfo(pid, name, contextName));
-                }
-            } catch (Exception e) {
-                // skip
-            }
+        for (ProcessFinder.CamelProcess p : ProcessFinder.findAll()) {
+            result.add(new ProcessInfo(p.pid(), p.name(), p.contextName()));
         }
         return result;
     }
@@ -113,32 +77,11 @@ public final class RuntimeHelper {
      * @param nameOrPid a process name (with optional wildcard), a numeric PID 
string, or {@code null} for auto-detect
      */
     public static ProcessInfo findProcess(String nameOrPid) {
-        List<ProcessInfo> processes = discoverProcesses();
-        if (processes.isEmpty()) {
-            return null;
-        }
-
-        if (nameOrPid != null && !nameOrPid.isBlank()) {
-            if (nameOrPid.matches("\\d+")) {
-                long pid = Long.parseLong(nameOrPid);
-                return processes.stream()
-                        .filter(p -> p.pid == pid)
-                        .findFirst()
-                        .orElse(null);
-            }
-            String pattern = nameOrPid.endsWith("*") ? nameOrPid : nameOrPid + 
"*";
-            List<ProcessInfo> matched = processes.stream()
-                    .filter(p -> (p.name != null && 
PatternHelper.matchPattern(FileUtil.onlyName(p.name), pattern))
-                            || (p.contextName != null && 
PatternHelper.matchPattern(p.contextName, pattern)))
-                    .toList();
-            if (matched.size() == 1) {
-                return matched.get(0);
-            }
-            return null;
-        }
-
-        if (processes.size() == 1) {
-            return processes.get(0);
+        List<ProcessFinder.CamelProcess> found = nameOrPid != null && 
!nameOrPid.isBlank()
+                ? ProcessFinder.find(nameOrPid) : ProcessFinder.findAll();
+        if (found.size() == 1) {
+            ProcessFinder.CamelProcess p = found.get(0);
+            return new ProcessInfo(p.pid(), p.name(), p.contextName());
         }
         return null;
     }
@@ -196,7 +139,7 @@ public final class RuntimeHelper {
             configure.accept(request);
         }
         try {
-            String response = executeAction(pid, request, timeoutMs, 
Function.identity());
+            String response = ActionClient.executeText(pid, request, 
timeoutMs);
             if (response != null) {
                 return response;
             }
@@ -207,58 +150,14 @@ public final class RuntimeHelper {
     }
 
     /**
-     * Executes a request and waits for a complete JSON object. Each request 
uses independent files, published
-     * atomically, and removing an unfinished request asks the connector to 
cancel it.
+     * Executes a request and waits for a complete JSON object (see {@link 
ActionClient}). Each request uses independent
+     * files, published atomically, and removing an unfinished request asks 
the connector to cancel it.
      *
      * @return             the response, or {@code null} on timeout or 
interruption
      * @throws IOException if publishing or cleaning up the request fails
      */
     public static JsonObject executeAction(long pid, JsonObject request, long 
timeoutMs) throws IOException {
-        return executeAction(pid, request, timeoutMs, text -> 
Jsoner.deserialize(text, (JsonObject) null));
-    }
-
-    private static <T> T executeAction(long pid, JsonObject request, long 
timeoutMs, Function<String, T> reader)
-            throws IOException {
-        String requestId = UUID.randomUUID().toString();
-        Path directory = CommandLineHelper.getCamelDir();
-        Path actionFile = directory.resolve(pid + "-action-" + requestId + 
".json");
-        Path outputFile = directory.resolve(pid + "-output-" + requestId + 
".json");
-        Path temporary = directory.resolve(pid + "-" + requestId + ".tmp");
-        try {
-            Files.writeString(temporary, request.toJson());
-            Files.move(temporary, actionFile, StandardCopyOption.ATOMIC_MOVE);
-            StopWatch watch = new StopWatch();
-            while (watch.taken() < timeoutMs) {
-                try {
-                    Thread.sleep(Math.min(POLL_INTERVAL_MS, Math.max(1, 
timeoutMs - watch.taken())));
-                    if (Files.exists(outputFile)) {
-                        String text = Files.readString(outputFile);
-                        if (!text.isEmpty()) {
-                            T response = reader.apply(text);
-                            if (response != null) {
-                                return response;
-                            }
-                        }
-                    }
-                } catch (InterruptedException e) {
-                    Thread.currentThread().interrupt();
-                    break;
-                } catch (IOException e) {
-                    // The runtime writes output in place; retry until the 
response is complete.
-                }
-            }
-            return null;
-        } finally {
-            try {
-                Files.deleteIfExists(actionFile);
-            } finally {
-                try {
-                    Files.deleteIfExists(outputFile);
-                } finally {
-                    Files.deleteIfExists(temporary);
-                }
-            }
-        }
+        return ActionClient.execute(pid, request, timeoutMs).reply();
     }
 
     /**
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/TimeoutConverter.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/TimeoutConverter.java
new file mode 100644
index 000000000000..4598582e5567
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/TimeoutConverter.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.common;
+
+import org.apache.camel.util.TimeUtils;
+import picocli.CommandLine;
+
+/**
+ * Converts the <tt>--timeout</tt> option of the commands that talk to a 
running integration, to millis: a number is
+ * millis (<tt>20000</tt>), and a duration has units (<tt>30s</tt>, 
<tt>1m30s</tt>).
+ */
+public class TimeoutConverter implements CommandLine.ITypeConverter<Long> {
+
+    @Override
+    public Long convert(String value) {
+        try {
+            return TimeUtils.toMilliSeconds(value.trim());
+        } catch (RuntimeException e) {
+            throw new CommandLine.TypeConversionException(
+                    "'" + value + "' is not a timeout: use millis, or a 
duration such as 30s");
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ActionClientTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ActionClientTest.java
new file mode 100644
index 000000000000..dec47a966cb2
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ActionClientTest.java
@@ -0,0 +1,238 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.common;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+
+import org.apache.camel.dsl.jbang.core.common.ActionClient.ActionResult;
+import org.apache.camel.dsl.jbang.core.common.ActionClient.Status;
+import org.apache.camel.dsl.jbang.core.common.ProcessFinder.CamelProcess;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.AfterEach;
+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.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.awaitility.Awaitility.await;
+
+/**
+ * Talks to a fake integration that answers like the camel-cli-connector of a 
given Camel version.
+ */
+class ActionClientTest {
+
+    private static final long PID = 123;
+
+    @TempDir
+    Path home;
+
+    private Path previousHome;
+    private ExecutorService executor;
+
+    @BeforeEach
+    void setup() throws Exception {
+        previousHome = CommandLineHelper.getHomeDir();
+        CommandLineHelper.useHomeDir(home.toString());
+        Files.createDirectories(CommandLineHelper.getCamelDir());
+        executor = Executors.newSingleThreadExecutor();
+    }
+
+    @AfterEach
+    void restore() {
+        executor.shutdownNow();
+        CommandLineHelper.useHomeDir(previousHome.toString());
+    }
+
+    @Test
+    void resultOfAnAction() throws Exception {
+        Future<JsonObject> request = answerRequest(r -> 
json("{\"status\":\"success\",\"routes\":[]}"));
+
+        ActionResult result = ActionClient.execute(integration("4.24.0"), 
"route-dump", o -> o.put("filter", "*"), 5000);
+
+        assertThat(result.status()).isEqualTo(Status.OK);
+        assertThat(result.reply()).containsKey("routes");
+        assertThat(result.exitCode()).isEqualTo(CliExitCode.OK);
+        assertThat(request.get(5, TimeUnit.SECONDS)).containsEntry("action", 
"route-dump").containsEntry("filter", "*");
+        assertNoFilesLeft();
+    }
+
+    @Test
+    void failedActionIsAnError() throws Exception {
+        answerRequest(r -> json("{\"status\":\"failed\",\"error\":\"Boom\"}"));
+
+        ActionResult result = ActionClient.execute(integration("4.24.0"), 
"eval", null, 5000);
+
+        assertThat(result.status()).isEqualTo(Status.ERROR);
+        assertThat(result.error()).isEqualTo("Boom");
+        assertThat(result.exitCode()).isEqualTo(CliExitCode.FAILURE);
+        
assertThatThrownBy(result::orThrow).isInstanceOf(CliException.class).hasMessage("Boom");
+    }
+
+    @Test
+    void notFoundIsExitCodeThree() throws Exception {
+        answerRequest(r -> json("{\"status\":\"failed\",\"error\":\"No route 
matching: x\",\"reason\":\"not-found\"}"));
+
+        ActionResult result = ActionClient.send(integration("4.24.0"), 
"route", o -> o.put("id", "x"), 5000);
+
+        assertThat(result.isNotFound()).isTrue();
+        assertThat(result.exitCode()).isEqualTo(CliExitCode.NOT_FOUND);
+        assertThatThrownBy(result::orThrow).isInstanceOf(CliException.class)
+                .extracting(e -> ((CliException) 
e).getExitCode()).isEqualTo(CliExitCode.NOT_FOUND);
+    }
+
+    @Test
+    void actionWithoutResultIsConfirmed() throws Exception {
+        answerRequest(r -> json("{\"status\":\"success\"}"));
+
+        ActionResult result = ActionClient.send(integration("4.24.0"), "gc", 
null, 5000);
+
+        assertThat(result.status()).isEqualTo(Status.OK);
+        assertNoFilesLeft();
+    }
+
+    @Test
+    void noAnswerIsATimeout() throws Exception {
+        ActionResult result = ActionClient.send(integration("4.24.0"), "gc", 
null, 300);
+
+        assertThat(result.status()).isEqualTo(Status.TIMEOUT);
+        assertThat(result.exitCode()).isEqualTo(CliExitCode.TIMEOUT);
+        assertThat(result.error()).contains("No answer from the integration 
with pid 123 to the action gc");
+        // the action is withdrawn, so the integration does not run it later
+        assertNoFilesLeft();
+    }
+
+    @Test
+    void olderIntegrationTakesAnActionWithoutConfirmingIt() throws Exception {
+        // before 4.24 the connector deletes the request without answering an 
action without a result
+        answerRequest(r -> null);
+
+        ActionResult result = ActionClient.send(integration("4.22.1"), "gc", 
null, 5000);
+
+        assertThat(result.status()).isEqualTo(Status.UNCONFIRMED);
+        assertThat(result.isOk()).isTrue();
+        assertThat(result.exitCode()).isEqualTo(CliExitCode.OK);
+    }
+
+    @Test
+    void integrationBefore421UsesTheSharedActionFile() throws Exception {
+        Path output = CommandLineHelper.getCamelDir().resolve(PID + 
"-output.json");
+        // a stale answer left by an earlier command must not be taken as the 
answer
+        Files.writeString(output, "{\"status\":\"success\",\"stale\":true}");
+        Path action = CommandLineHelper.getCamelDir().resolve(PID + 
"-action.json");
+        executor.submit(() -> {
+            await().atMost(5, TimeUnit.SECONDS).until(() -> 
Files.exists(action));
+            Files.writeString(output, 
"{\"status\":\"success\",\"fresh\":true}");
+            Files.delete(action);
+            return null;
+        });
+
+        ActionResult result = ActionClient.execute(integration("4.18.3"), 
"route-dump", null, 5000);
+
+        assertThat(result.reply()).containsEntry("fresh", 
true).doesNotContainKey("stale");
+        assertThat(output).doesNotExist();
+    }
+
+    @Test
+    void executeByPidReadsTheVersionFromTheStatusFile() throws Exception {
+        Files.writeString(CommandLineHelper.getCamelDir().resolve(PID + 
"-status.json"),
+                "{\"context\":{\"version\":\"4.24.0\"}}");
+        answerRequest(r -> json("{\"status\":\"success\",\"value\":42}"));
+
+        JsonObject request = new JsonObject();
+        request.put("action", "jvm");
+        ActionResult result = ActionClient.execute(PID, request, 5000);
+
+        assertThat(result.status()).isEqualTo(Status.OK);
+        assertThat(result.reply().getInteger("value")).isEqualTo(42);
+    }
+
+    @Test
+    void errorMessageComesFromTheAnswer() {
+        
assertThat(ActionClient.result(json("{\"status\":\"error\",\"exception\":{\"message\":\"Forced\"}}")).error())
+                .isEqualTo("Forced");
+        
assertThat(ActionClient.result(json("{\"status\":\"error\",\"message\":\"No 
such datasource\"}")).error())
+                .isEqualTo("No such datasource");
+        
assertThat(ActionClient.result(json("{\"status\":\"failed\"}")).error()).isEqualTo("Action
 failed");
+        
assertThat(ActionClient.result(json("{\"value\":1}")).status()).isEqualTo(Status.OK);
+    }
+
+    @Test
+    void versions() {
+        assertThat(ActionClient.isAtLeast("4.24.0-SNAPSHOT", 4, 24)).isTrue();
+        assertThat(ActionClient.isAtLeast("4.24.1", 4, 21)).isTrue();
+        assertThat(ActionClient.isAtLeast("4.22.0", 4, 24)).isFalse();
+        assertThat(ActionClient.isAtLeast("3.22.0", 4, 21)).isFalse();
+        assertThat(ActionClient.isAtLeast("5.0.0", 4, 24)).isTrue();
+    }
+
+    private static CamelProcess integration(String version) {
+        JsonObject context = new JsonObject();
+        context.put("name", "app");
+        context.put("version", version);
+        JsonObject status = new JsonObject();
+        status.put("context", context);
+        return new CamelProcess(PID, "app", status);
+    }
+
+    /**
+     * Answers the next request with a file of its own like the connector 
does: writes the answer (unless null), then
+     * deletes the request.
+     */
+    private Future<JsonObject> answerRequest(Function<JsonObject, JsonObject> 
answer) {
+        return executor.submit(() -> {
+            Path dir = CommandLineHelper.getCamelDir();
+            Path request = await().atMost(5, TimeUnit.SECONDS).until(() -> 
requestFiles(dir), files -> !files.isEmpty())
+                    .get(0);
+            JsonObject action = json(Files.readString(request));
+            JsonObject reply = answer.apply(action);
+            String id = request.getFileName().toString().substring((PID + 
"-action-").length()).replace(".json", "");
+            if (reply != null) {
+                Files.writeString(dir.resolve(PID + "-output-" + id + 
".json"), reply.toJson());
+            }
+            Files.delete(request);
+            return action;
+        });
+    }
+
+    private static List<Path> requestFiles(Path dir) throws Exception {
+        try (var files = Files.list(dir)) {
+            return files.filter(f -> f.getFileName().toString().startsWith(PID 
+ "-action-")).toList();
+        }
+    }
+
+    private static void assertNoFilesLeft() throws Exception {
+        await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
+            try (var files = Files.list(CommandLineHelper.getCamelDir())) {
+                assertThat(files.map(f -> f.getFileName().toString()).filter(n 
-> !n.endsWith("-status.json")))
+                        .isEmpty();
+            }
+        });
+    }
+
+    private static JsonObject json(String text) {
+        return (JsonObject) Jsoner.deserialize(text, (JsonObject) null);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ProcessFinderTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ProcessFinderTest.java
new file mode 100644
index 000000000000..6aed63614207
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ProcessFinderTest.java
@@ -0,0 +1,207 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.common;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.camel.dsl.jbang.core.common.ProcessFinder.CamelProcess;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.MockedStatic;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+/**
+ * Which integrations a name or pid finds: a running integration has a live 
process and a status file, and its Camel has
+ * not stopped.
+ */
+class ProcessFinderTest {
+
+    private static final long CURRENT = 99999L;
+
+    @TempDir
+    Path home;
+
+    private Path previousHome;
+    private final List<ProcessHandle> processes = new ArrayList<>();
+    private MockedStatic<ProcessHandle> mocked;
+
+    @BeforeEach
+    void setup() throws Exception {
+        previousHome = CommandLineHelper.getHomeDir();
+        CommandLineHelper.useHomeDir(home.toString());
+        Files.createDirectories(CommandLineHelper.getCamelDir());
+
+        mocked = mockStatic(ProcessHandle.class);
+        ProcessHandle current = handle(CURRENT);
+        mocked.when(ProcessHandle::current).thenReturn(current);
+        mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
processes.stream());
+    }
+
+    @AfterEach
+    void restore() {
+        mocked.close();
+        CommandLineHelper.useHomeDir(previousHome.toString());
+    }
+
+    @Test
+    void findsRunningIntegrations() throws Exception {
+        running(100, "orders", 5);
+        running(200, "billing", 5);
+        // Camel has stopped: the launcher of an integration that runs in a 
child process
+        running(300, "launcher", 9);
+        // a status file left behind by a process that is gone
+        status(400, "gone", 5);
+        // a process without status file is no Camel integration
+        processes.add(handle(500));
+
+        
assertThat(ProcessFinder.findAll()).extracting(CamelProcess::pid).containsExactly(100L,
 200L);
+        
assertThat(ProcessFinder.find("*")).extracting(CamelProcess::pid).containsExactly(100L,
 200L);
+    }
+
+    @Test
+    void nameIsAPrefixUnlessItEndsWithExclamationMark() throws Exception {
+        running(100, "orders", 5);
+        running(200, "orders-eu", 5);
+
+        
assertThat(ProcessFinder.find("ord")).extracting(CamelProcess::pid).containsExactly(100L,
 200L);
+        
assertThat(ProcessFinder.find("orders!")).extracting(CamelProcess::pid).containsExactly(100L);
+        assertThat(ProcessFinder.find("ord!")).isEmpty();
+        // a regular expression
+        
assertThat(ProcessFinder.find("orders-(eu|us)")).extracting(CamelProcess::pid).containsExactly(200L);
+    }
+
+    @Test
+    void theDefaultContextNameOfTheCliMatchesNothing() throws Exception {
+        // the name comes from the main class, and the context has the default 
name of the Camel CLI
+        JsonObject root = statusOf("CamelJBang", 5);
+        JsonObject runtime = new JsonObject();
+        runtime.put("mainClass", "Orders");
+        root.put("runtime", runtime);
+        
Files.writeString(CommandLineHelper.getCamelDir().resolve("100-status.json"), 
root.toJson());
+        processes.add(handle(100));
+
+        assertThat(ProcessFinder.find("Camel")).isEmpty();
+        
assertThat(ProcessFinder.find("Ord")).extracting(CamelProcess::name).containsExactly("Orders");
+    }
+
+    @Test
+    void pidMustBeARunningIntegration() throws Exception {
+        running(100, "orders", 5);
+        processes.add(handle(500));
+
+        
assertThat(ProcessFinder.find("100")).extracting(CamelProcess::name).containsExactly("orders");
+        // a process that is no Camel integration, and a pid that does not 
exist
+        assertThat(ProcessFinder.find("500")).isEmpty();
+        assertThat(ProcessFinder.find("600")).isEmpty();
+    }
+
+    @Test
+    void aNameNeverMatchesThisProcessButItsPidDoes() throws Exception {
+        status(CURRENT, "cli", 5);
+        processes.add(handle(CURRENT));
+
+        assertThat(ProcessFinder.find("cli")).isEmpty();
+        assertThat(ProcessFinder.find(String.valueOf(CURRENT))).hasSize(1);
+    }
+
+    @Test
+    void processKnowsItsVersionAndContextName() throws Exception {
+        JsonObject root = statusOf("orders", 5);
+        ((JsonObject) root.get("context")).put("version", "4.24.0-SNAPSHOT");
+        
Files.writeString(CommandLineHelper.getCamelDir().resolve("100-status.json"), 
root.toJson());
+        processes.add(handle(100));
+
+        CamelProcess p = ProcessFinder.requireOne("orders");
+        assertThat(p.camelVersion()).isEqualTo("4.24.0-SNAPSHOT");
+        assertThat(p.contextName()).isEqualTo("orders");
+    }
+
+    @Test
+    void requireOneNeedsExactlyOneMatch() throws Exception {
+        running(100, "orders", 5);
+        running(200, "orders-eu", 5);
+
+        assertThat(ProcessFinder.requireOne("orders!").pid()).isEqualTo(100L);
+        assertThatThrownBy(() -> ProcessFinder.requireOne("billing"))
+                .isInstanceOf(CliException.class)
+                .hasMessage("No running Camel integration matches: billing")
+                .extracting(e -> ((CliException) 
e).getExitCode()).isEqualTo(CliExitCode.NOT_FOUND);
+        assertThatThrownBy(() -> ProcessFinder.requireOne("orders"))
+                .isInstanceOf(CliException.class)
+                .hasMessage("Name or pid orders matches 2 running Camel 
integrations: 100 (orders), 200 (orders-eu)."
+                            + " Specify a name or pid that matches exactly 
one.")
+                .extracting(e -> ((CliException) 
e).getExitCode()).isEqualTo(CliExitCode.NOT_FOUND);
+    }
+
+    @Test
+    void requireAnyNeedsAMatch() throws Exception {
+        running(100, "orders", 5);
+
+        assertThat(ProcessFinder.requireAny("ord")).hasSize(1);
+        assertThatThrownBy(() -> ProcessFinder.requireAny("billing"))
+                .isInstanceOf(CliException.class)
+                .extracting(e -> ((CliException) 
e).getExitCode()).isEqualTo(CliExitCode.NOT_FOUND);
+    }
+
+    @Test
+    void noCamelDirectoryFindsNothing() {
+        CommandLineHelper.useHomeDir(home.resolve("missing").toString());
+
+        assertThat(ProcessFinder.findAll()).isEmpty();
+        assertThat(ProcessFinder.find("orders")).isEmpty();
+    }
+
+    private void running(long pid, String contextName, int phase) throws 
Exception {
+        status(pid, contextName, phase);
+        processes.add(handle(pid));
+    }
+
+    private static void status(long pid, String contextName, int phase) throws 
Exception {
+        Files.writeString(CommandLineHelper.getCamelDir().resolve(pid + 
"-status.json"),
+                statusOf(contextName, phase).toJson());
+    }
+
+    private static JsonObject statusOf(String contextName, int phase) {
+        JsonObject context = new JsonObject();
+        context.put("name", contextName);
+        context.put("phase", phase);
+        JsonObject root = new JsonObject();
+        root.put("context", context);
+        return root;
+    }
+
+    private static ProcessHandle handle(long pid) {
+        ProcessHandle ph = mock(ProcessHandle.class);
+        ProcessHandle.Info info = mock(ProcessHandle.Info.class);
+        when(ph.pid()).thenReturn(pid);
+        when(ph.info()).thenReturn(info);
+        when(info.commandLine()).thenReturn(Optional.empty());
+        return ph;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/TimeoutConverterTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/TimeoutConverterTest.java
new file mode 100644
index 000000000000..393393c722d3
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/TimeoutConverterTest.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.common;
+
+import org.junit.jupiter.api.Test;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class TimeoutConverterTest {
+
+    private final TimeoutConverter converter = new TimeoutConverter();
+
+    @Test
+    void numberIsMillis() {
+        assertThat(converter.convert("20000")).isEqualTo(20000L);
+    }
+
+    @Test
+    void durationHasUnits() {
+        assertThat(converter.convert("30s")).isEqualTo(30_000L);
+        assertThat(converter.convert("1m30s")).isEqualTo(90_000L);
+        assertThat(converter.convert(" 2m ")).isEqualTo(120_000L);
+    }
+
+    @Test
+    void anythingElseIsAnInvalidOption() {
+        assertThatThrownBy(() -> converter.convert("soon"))
+                .isInstanceOf(CommandLine.TypeConversionException.class)
+                .hasMessage("'soon' is not a timeout: use millis, or a 
duration such as 30s");
+    }
+}

Reply via email to