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 ff4f64120cd7 CAMEL-25718: camel-jbang - Document the commands of camel 
test that come from Citrus (#28000)
ff4f64120cd7 is described below

commit ff4f64120cd7d34b2178c6a3a5268c60de22ebb5
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Oct 17 20:35:25 2026 +0200

    CAMEL-25718: camel-jbang - Document the commands of camel test that come 
from Citrus (#28000)
    
    The test plugin registers commands of Citrus as they are (inspect, run, ps, 
agent and its start, run and stop), and PrepareCamelJBangCommandsMojo only 
documented a command whose class has a source in the module, so 7 of the 9 
commands of camel test had no page. A command class that the source registering 
it imports by name is now read from the classpath of the project: its @Command 
(description, hidden) and its options, by reflection, as the options of a 
library class (CAMEL-25717). T [...]
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../jbang-commands/camel-jbang-test-agent-run.adoc |  43 +++++++
 .../camel-jbang-test-agent-start.adoc              |  44 +++++++
 ...-test.adoc => camel-jbang-test-agent-stop.adoc} |  15 +--
 ...jbang-test.adoc => camel-jbang-test-agent.adoc} |  10 +-
 ...ang-test.adoc => camel-jbang-test-inspect.adoc} |  15 +--
 ...el-jbang-test.adoc => camel-jbang-test-ps.adoc} |  17 +--
 .../pages/jbang-commands/camel-jbang-test-run.adoc |  41 +++++++
 .../pages/jbang-commands/camel-jbang-test.adoc     |   4 +
 .../camel-jbang-plugin-commands-metadata.json      |   2 +-
 .../packaging/PrepareCamelJBangCommandsMojo.java   |  89 ++++++++++++--
 .../PrepareCamelJBangCommandsMojoTest.java         | 130 ++++++++++++++++++---
 11 files changed, 346 insertions(+), 64 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-agent-run.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-agent-run.adoc
new file mode 100644
index 000000000000..ede01d282bf7
--- /dev/null
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-agent-run.adoc
@@ -0,0 +1,43 @@
+
+// AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE
+= camel test agent run
+
+Runs tests on the agent server
+
+NOTE: This command is provided by the `test` plugin. Install it with `camel 
plugin add test`.
+
+
+== Usage
+
+[source,bash]
+----
+camel test agent run [options]
+----
+
+
+
+== Options
+
+[cols="2,5,1,2",options="header"]
+|===
+| Option | Description | Default | Type
+| `--async` | Should the test engine print verbose test summary information. | 
 | boolean
+| `--background` | When enabled the command is not blocking for the test 
result response. |  | boolean
+| `--dep` | Set of additional Maven dependencies that should be loaded with 
the agent. |  | String[]
+| `--engine` | Name of the test engine that is used ti run tests. One of 
junit, junit-jupiter, junit4, testng, cucumber |  | String
+| `--includes` | Includes test name pattern. |  | String[]
+| `--modules` | Comma delimited list of additional Citrus modules that should 
be loaded with the agent. |  | String
+| `--packages` | Test package name to include in the test run. |  | String[]
+| `--polling-interval` | Interval used to poll for test results. Only used in 
asynchronous test execution mode. | 2000 | String
+| `--port` | Server port to connect to. |  | String
+| `--property` | Default System property to set before the test run. |  | 
String[]
+| `--reset` | Should the test engine reset the suite state for each run. | 
true | String
+| `--test-jar` | Path to a Java archive that holds tests to run. |  | String
+| `--timeout` | Http request timeout. | 60000 | String
+| `--url` | Server endpoint URL to connect to. |  | String
+| `--verbose` | Should the test engine print verbose test summary information. 
| true | String
+| `--work-directory` | The working directory used by the file based test 
engines to load file resources from. |  | String
+| `-h,--help` | Display the help and sub-commands |  | boolean
+|===
+
+
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-agent-start.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-agent-start.adoc
new file mode 100644
index 000000000000..6e3a9a3bb86a
--- /dev/null
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-agent-start.adoc
@@ -0,0 +1,44 @@
+
+// AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE
+= camel test agent start
+
+Starts the Citrus agent as a server
+
+NOTE: This command is provided by the `test` plugin. Install it with `camel 
plugin add test`.
+
+
+== Usage
+
+[source,bash]
+----
+camel test agent start [options]
+----
+
+
+
+== Options
+
+[cols="2,5,1,2",options="header"]
+|===
+| Option | Description | Default | Type
+| `--config-class` | Configuration class name. |  | String
+| `--dep` | Set of additional Maven dependencies that should be loaded with 
the agent. |  | String[]
+| `--engine` | Name of the test engine that is used ti run tests. One of 
junit, junit-jupiter, junit4, testng, cucumber |  | String
+| `--includes` | Includes test name pattern. |  | String[]
+| `--inspect-code` | When enabled the source code gets analyzed for required 
modules and dependencies that are added to the classpath. | true | String
+| `--modules` | Comma delimited list of additional Citrus modules that should 
be loaded with the agent. |  | String
+| `--offline` | When enabled there will be no attempts to resolve Maven 
artifacts via internet connection. |  | String
+| `--packages` | Test package name to include in the test run. |  | String[]
+| `--port` | Server port. |  | String
+| `--property` | Default System property to set before the test run. |  | 
String[]
+| `--reset` | Should the test engine reset the suite state for each run. | 
true | String
+| `--skip-tests` | Should the server skip the test run at startup. |  | String
+| `--system-exit` | Should the server exit based on success or failure of the 
test run. |  | String
+| `--test-jar` | Path to a Java archive that holds tests to run. |  | String
+| `--time-to-live` | If this time is set the server automatically terminates 
after the given time. |  | String
+| `--verbose` | Should the test engine print verbose test summary information. 
| true | String
+| `--work-directory` | The working directory used by the file based test 
engines to load file resources from. |  | String
+| `-h,--help` | Display the help and sub-commands |  | boolean
+|===
+
+
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-agent-stop.adoc
similarity index 60%
copy from 
docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test.adoc
copy to 
docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-agent-stop.adoc
index 13a7c0844597..b0db61a3798b 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-agent-stop.adoc
@@ -1,8 +1,8 @@
 
 // AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE
-= camel test
+= camel test agent stop
 
-Manage tests for Camel applications
+Stop the Citrus agent service
 
 NOTE: This command is provided by the `test` plugin. Install it with `camel 
plugin add test`.
 
@@ -11,19 +11,10 @@ NOTE: This command is provided by the `test` plugin. 
Install it with `camel plug
 
 [source,bash]
 ----
-camel test [options]
+camel test agent stop [options]
 ----
 
 
-== Subcommands
-
-[cols="2,5",options="header"]
-|===
-| Subcommand | Description
-| xref:jbang-commands/camel-jbang-test-init.adoc[init] | Creates a new Citrus 
test
-|===
-
-
 
 == Options
 
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-agent.adoc
similarity index 56%
copy from 
docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test.adoc
copy to 
docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-agent.adoc
index 13a7c0844597..ffc5c5a0874e 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-agent.adoc
@@ -1,8 +1,8 @@
 
 // AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE
-= camel test
+= camel test agent
 
-Manage tests for Camel applications
+Manage Citrus agents
 
 NOTE: This command is provided by the `test` plugin. Install it with `camel 
plugin add test`.
 
@@ -11,7 +11,7 @@ NOTE: This command is provided by the `test` plugin. Install 
it with `camel plug
 
 [source,bash]
 ----
-camel test [options]
+camel test agent [options]
 ----
 
 
@@ -20,7 +20,9 @@ camel test [options]
 [cols="2,5",options="header"]
 |===
 | Subcommand | Description
-| xref:jbang-commands/camel-jbang-test-init.adoc[init] | Creates a new Citrus 
test
+| xref:jbang-commands/camel-jbang-test-agent-run.adoc[run] | Runs tests on the 
agent server
+| xref:jbang-commands/camel-jbang-test-agent-start.adoc[start] | Starts the 
Citrus agent as a server
+| xref:jbang-commands/camel-jbang-test-agent-stop.adoc[stop] | Stop the Citrus 
agent service
 |===
 
 
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-inspect.adoc
similarity index 60%
copy from 
docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test.adoc
copy to 
docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-inspect.adoc
index 13a7c0844597..68529de396be 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-inspect.adoc
@@ -1,8 +1,8 @@
 
 // AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE
-= camel test
+= camel test inspect
 
-Manage tests for Camel applications
+Inspect a Citrus test and its source code in order to provide detailed 
information such as used endpoints as well as required modules and dependencies.
 
 NOTE: This command is provided by the `test` plugin. Install it with `camel 
plugin add test`.
 
@@ -11,19 +11,10 @@ NOTE: This command is provided by the `test` plugin. 
Install it with `camel plug
 
 [source,bash]
 ----
-camel test [options]
+camel test inspect [options]
 ----
 
 
-== Subcommands
-
-[cols="2,5",options="header"]
-|===
-| Subcommand | Description
-| xref:jbang-commands/camel-jbang-test-init.adoc[init] | Creates a new Citrus 
test
-|===
-
-
 
 == Options
 
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-ps.adoc
similarity index 60%
copy from 
docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test.adoc
copy to 
docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-ps.adoc
index 13a7c0844597..3f62893c0cd3 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-ps.adoc
@@ -1,8 +1,8 @@
 
 // AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE
-= camel test
+= camel test ps
 
-Manage tests for Camel applications
+List running Citrus tests
 
 NOTE: This command is provided by the `test` plugin. Install it with `camel 
plugin add test`.
 
@@ -11,25 +11,18 @@ NOTE: This command is provided by the `test` plugin. 
Install it with `camel plug
 
 [source,bash]
 ----
-camel test [options]
+camel test ps [options]
 ----
 
 
-== Subcommands
-
-[cols="2,5",options="header"]
-|===
-| Subcommand | Description
-| xref:jbang-commands/camel-jbang-test-init.adoc[init] | Creates a new Citrus 
test
-|===
-
-
 
 == Options
 
 [cols="2,5,1,2",options="header"]
 |===
 | Option | Description | Default | Type
+| `--pid` | List only pid in the output |  | boolean
+| `--sort` | Sort by pid, name or age | pid | String
 | `-h,--help` | Display the help and sub-commands |  | boolean
 |===
 
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-run.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-run.adoc
new file mode 100644
index 000000000000..9151e00295a3
--- /dev/null
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-run.adoc
@@ -0,0 +1,41 @@
+
+// AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE
+= camel test run
+
+Run as local Citrus test
+
+NOTE: This command is provided by the `test` plugin. Install it with `camel 
plugin add test`.
+
+
+== Usage
+
+[source,bash]
+----
+camel test run [options]
+----
+
+
+
+== Options
+
+[cols="2,5,1,2",options="header"]
+|===
+| Option | Description | Default | Type
+| `--dep,--dependency` | Comma delimited list of additional Maven GAV 
dependencies that must be loaded to run the test. |  | List
+| `--engine` | Name of the test engine that is used to run tests. One of 
junit, junit-jupiter, junit4, testng, cucumber |  | String
+| `--includes` | Includes test name pattern. |  | List
+| `--inspect-code` | When enabled the source code gets analyzed for required 
modules and dependencies that are added to the classpath. | true | boolean
+| `--logging` | Can be used to turn off logging | true | boolean
+| `--logging-color` | Use colored logging | true | boolean
+| `--logging-level` | Logging level | info | String
+| `--modules` | Comma delimited list of additional Citrus modules that must be 
loaded to run the test. |  | String
+| `--offline` | When enabled there will be no attempts to resolve Maven 
artifacts via internet connection. | false | boolean
+| `--property,--properties` | Default System property to set before the test 
run. |  | List
+| `--repository,--repositories` | Set of Maven repositories that should be 
used to resolve dependencies. |  | List
+| `--reset` | Should the test engine reset the suite state for this run. | 
true | String
+| `--verbose` | Should the test engine print verbose test summary information. 
| true | String
+| `--work-directory` | The working directory used by the file based test 
engines to load file resources from. |  | String
+| `-h,--help` | Display the help and sub-commands |  | boolean
+|===
+
+
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test.adoc
index 13a7c0844597..e7afbee9facd 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test.adoc
@@ -20,7 +20,11 @@ camel test [options]
 [cols="2,5",options="header"]
 |===
 | Subcommand | Description
+| xref:jbang-commands/camel-jbang-test-agent.adoc[agent] | Manage Citrus agents
 | xref:jbang-commands/camel-jbang-test-init.adoc[init] | Creates a new Citrus 
test
+| xref:jbang-commands/camel-jbang-test-inspect.adoc[inspect] | Inspect a 
Citrus test and its source code in order to provide detailed information such 
as used endpoints as well as required modules and dependencies.
+| xref:jbang-commands/camel-jbang-test-ps.adoc[ps] | List running Citrus tests
+| xref:jbang-commands/camel-jbang-test-run.adoc[run] | Run as local Citrus test
 |===
 
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-test/src/generated/resources/META-INF/camel-jbang-plugin-commands-metadata.json
 
b/dsl/camel-jbang/camel-jbang-plugin-test/src/generated/resources/META-INF/camel-jbang-plugin-commands-metadata.json
index 173749a94911..c48595f91fc2 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-test/src/generated/resources/META-INF/camel-jbang-plugin-commands-metadata.json
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-test/src/generated/resources/META-INF/camel-jbang-plugin-commands-metadata.json
@@ -1,5 +1,5 @@
 {
   "commands": [
-    { "name": "test", "fullName": "test", "description": "Manage tests for 
Camel applications", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.test.TestCommand", "plugin": "test", 
"options": [ { "names": "-h,--help", "description": "Display the help and 
sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { 
"name": "init", "fullName": "test init", "description": "Creates a new Citrus 
test", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.test.T [...]
+    { "name": "test", "fullName": "test", "description": "Manage tests for 
Camel applications", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.test.TestCommand", "plugin": "test", 
"options": [ { "names": "-h,--help", "description": "Display the help and 
sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { 
"name": "agent", "fullName": "test agent", "description": "Manage Citrus 
agents", "sourceClass": "org.citrusframework.jbang.cli.commands.Agent", "pl 
[...]
   ]
 }
diff --git 
a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCamelJBangCommandsMojo.java
 
b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCamelJBangCommandsMojo.java
index 9e46cd905846..5d9759622d82 100644
--- 
a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCamelJBangCommandsMojo.java
+++ 
b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCamelJBangCommandsMojo.java
@@ -51,6 +51,7 @@ import org.jboss.forge.roaster.model.JavaType;
 import org.jboss.forge.roaster.model.source.AnnotationSource;
 import org.jboss.forge.roaster.model.source.FieldSource;
 import org.jboss.forge.roaster.model.source.Import;
+import org.jboss.forge.roaster.model.source.Importer;
 import org.jboss.forge.roaster.model.source.JavaClassSource;
 import org.jboss.forge.roaster.model.source.JavaSource;
 
@@ -200,6 +201,7 @@ public class PrepareCamelJBangCommandsMojo extends 
AbstractGeneratorMojo {
             throws IOException {
 
         List<String> lines = Files.readAllLines(mainFile.toPath());
+        JavaSource<?> registering = parseSource(mainFile);
 
         // Find the start of the commandLine builder chain
         int startLine = -1;
@@ -226,7 +228,7 @@ public class PrepareCamelJBangCommandsMojo extends 
AbstractGeneratorMojo {
                 break;
             }
 
-            processSubcommandLine(lines.get(i), parentStack, 
commandsByClassName, model, null);
+            processSubcommandLine(lines.get(i), parentStack, 
commandsByClassName, model, null, registering);
         }
     }
 
@@ -244,6 +246,7 @@ public class PrepareCamelJBangCommandsMojo extends 
AbstractGeneratorMojo {
         String plugin = nameMatcher.group(1);
 
         List<String> lines = Files.readAllLines(pluginFile.toPath());
+        JavaSource<?> registering = parseSource(pluginFile);
         for (String line : lines) {
             Matcher matcher = PLUGIN_ROOT_PATTERN.matcher(line.trim());
             if (!matcher.find()) {
@@ -255,7 +258,7 @@ public class PrepareCamelJBangCommandsMojo extends 
AbstractGeneratorMojo {
             Matcher newMatcher = NEW_COMMAND_PATTERN.matcher(target);
             if (newMatcher.find()) {
                 // a command without subcommands: 
commandLine.addSubcommand("name", new Command(main))
-                CommandInfo info = 
commandsByClassName.get(newMatcher.group(1));
+                CommandInfo info = commandInfo(newMatcher.group(1), 
commandsByClassName, registering);
                 if (info != null && !info.hidden) {
                     JBangCommand cmd = createCommand(info, cmdName, "");
                     cmd.setPlugin(plugin);
@@ -273,7 +276,7 @@ public class PrepareCamelJBangCommandsMojo extends 
AbstractGeneratorMojo {
                 if (!declMatcher.find()) {
                     continue;
                 }
-                CommandInfo info = 
commandsByClassName.get(declMatcher.group(1));
+                CommandInfo info = commandInfo(declMatcher.group(1), 
commandsByClassName, registering);
                 if (info == null || info.hidden) {
                     break;
                 }
@@ -285,7 +288,7 @@ public class PrepareCamelJBangCommandsMojo extends 
AbstractGeneratorMojo {
                 Stack<ParentInfo> parentStack = new Stack<>();
                 parentStack.push(new ParentInfo(root, -1));
                 for (int j = i + 1; j < lines.size(); j++) {
-                    processSubcommandLine(lines.get(j), parentStack, 
commandsByClassName, model, plugin);
+                    processSubcommandLine(lines.get(j), parentStack, 
commandsByClassName, model, plugin, registering);
                     if (lines.get(j).trim().endsWith(";")) {
                         break;
                     }
@@ -297,7 +300,7 @@ public class PrepareCamelJBangCommandsMojo extends 
AbstractGeneratorMojo {
 
     private void processSubcommandLine(
             String line, Stack<ParentInfo> parentStack, Map<String, 
CommandInfo> commandsByClassName,
-            JBangCommandModel model, String plugin) {
+            JBangCommandModel model, String plugin, JavaSource<?> registering) 
{
 
         String trimmedLine = line.trim();
         Matcher matcher = SUBCOMMAND_PATTERN.matcher(trimmedLine);
@@ -340,7 +343,7 @@ public class PrepareCamelJBangCommandsMojo extends 
AbstractGeneratorMojo {
         // - Ends with 3 parens ())) = leaf command (closes CommandLine + 
addSubcommand)
         boolean hasSubcommands = closingParens == 2;
 
-        CommandInfo info = commandsByClassName.get(className);
+        CommandInfo info = commandInfo(className, commandsByClassName, 
registering);
         boolean parentSkipped = !parentStack.isEmpty() && 
parentStack.peek().command == null;
         if (info == null || info.hidden || parentSkipped) {
             if (info == null) {
@@ -375,6 +378,71 @@ public class PrepareCamelJBangCommandsMojo extends 
AbstractGeneratorMojo {
         }
     }
 
+    /**
+     * The source that registers the commands, for the imports of their 
classes, or null when it cannot be parsed.
+     */
+    private JavaSource<?> parseSource(File file) {
+        try {
+            return Roaster.parse(file) instanceof JavaSource<?> source ? 
source : null;
+        } catch (Exception e) {
+            getLog().debug("Could not parse: " + file + " - " + 
e.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * The command of a class: from its source when it is one of the command 
classes, or else from its class on the
+     * classpath of the project when the source that registers it imports it 
by name, such as a command of a library
+     * that a plugin adds as it is (camel test run is the Run of Citrus).
+     */
+    private CommandInfo commandInfo(
+            String className, Map<String, CommandInfo> commandsByClassName, 
JavaSource<?> registering) {
+        CommandInfo info = commandsByClassName.get(className);
+        if (info == null && registering != null) {
+            String imported = importedByName(registering, className);
+            if (imported != null) {
+                info = parseCommandFromClass(imported);
+            }
+        }
+        return info;
+    }
+
+    /**
+     * Parses a command from the picocli annotations of its class on the 
classpath of the project, read by reflection,
+     * as picocli is not a dependency of this plugin.
+     */
+    @SuppressWarnings("unchecked")
+    private CommandInfo parseCommandFromClass(String className) {
+        try {
+            ClassLoader loader = getProjectClassLoader();
+            Class<?> clazz = loader.loadClass(className);
+            Annotation command
+                    = clazz.getAnnotation((Class<? extends Annotation>) 
loader.loadClass("picocli.CommandLine$Command"));
+            if (command == null) {
+                return null;
+            }
+            CommandInfo info = new CommandInfo();
+            info.className = clazz.getSimpleName();
+            info.qualifiedName = clazz.getName();
+            info.name = (String) attribute(command, "name");
+            String description = String.join(" ", (String[]) 
attribute(command, "description"));
+            // without the hint to the help, as for a command of the sources
+            if (description.contains("(use")) {
+                description = description.substring(0, 
description.indexOf("(use")).trim();
+            }
+            info.description = description.isEmpty() ? null : description;
+            info.hidden = (Boolean) attribute(command, "hidden");
+            info.deprecated = clazz.isAnnotationPresent(Deprecated.class);
+            info.options = new ArrayList<>();
+            parseOptionsFromClass(className, info.options);
+            return info;
+        } catch (Exception | LinkageError e) {
+            // the command is then missing from the documentation
+            getLog().warn("Could not read the command of class: " + className 
+ " - " + e.getMessage());
+            return null;
+        }
+    }
+
     private File findPluginFile(File dir) throws IOException {
         try (Stream<Path> paths = Files.walk(dir.toPath())) {
             return paths.filter(p -> p.toString().endsWith(".java"))
@@ -559,10 +627,10 @@ public class PrepareCamelJBangCommandsMojo extends 
AbstractGeneratorMojo {
     }
 
     /**
-     * The qualified name of the type with the simple name that the class 
imports by name, or null when it does not.
+     * The qualified name of the type with the simple name that the source 
imports by name, or null when it does not.
      */
-    private static String importedByName(JavaClassSource clazz, String 
simpleName) {
-        for (Import imported : clazz.getImports()) {
+    private static String importedByName(Importer<?> source, String 
simpleName) {
+        for (Import imported : source.getImports()) {
             if (!imported.isWildcard() && !imported.isStatic() && 
imported.getSimpleName().equals(simpleName)) {
                 return imported.getQualifiedName();
             }
@@ -585,7 +653,8 @@ public class PrepareCamelJBangCommandsMojo extends 
AbstractGeneratorMojo {
                     = (Class<? extends Annotation>) 
loader.loadClass("picocli.CommandLine$Mixin");
             parseOptionsFromClass(loader.loadClass(className), optionType, 
mixinType, options);
         } catch (Exception | LinkageError e) {
-            getLog().debug("Could not read the options of class: " + className 
+ " - " + e.getMessage());
+            // the options are then missing from the documentation
+            getLog().warn("Could not read the options of class: " + className 
+ " - " + e.getMessage());
         }
     }
 
diff --git 
a/tooling/maven/camel-package-maven-plugin/src/test/java/org/apache/camel/maven/packaging/PrepareCamelJBangCommandsMojoTest.java
 
b/tooling/maven/camel-package-maven-plugin/src/test/java/org/apache/camel/maven/packaging/PrepareCamelJBangCommandsMojoTest.java
index 553683627b87..e6a31707b6c5 100644
--- 
a/tooling/maven/camel-package-maven-plugin/src/test/java/org/apache/camel/maven/packaging/PrepareCamelJBangCommandsMojoTest.java
+++ 
b/tooling/maven/camel-package-maven-plugin/src/test/java/org/apache/camel/maven/packaging/PrepareCamelJBangCommandsMojoTest.java
@@ -61,6 +61,17 @@ class PrepareCamelJBangCommandsMojoTest {
             }
             """;
 
+    // the command test of the plugin
+    private static final String TEST_COMMAND = """
+            package org.apache.camel.dsl.jbang.core.commands.test;
+
+            import picocli.CommandLine.Command;
+
+            @Command(name = "test", description = "Manage tests")
+            public class TestCommand {
+            }
+            """;
+
     @TempDir
     Path tempDir;
 
@@ -270,6 +281,56 @@ class PrepareCamelJBangCommandsMojoTest {
         
assertThat(init.getOptions()).extracting(JBangCommandOption::getNames).containsExactly("--directory",
 "-h,--help");
     }
 
+    /**
+     * CAMEL-25718: the commands of a library that a plugin adds as they are 
(as camel test run is the Run of Citrus)
+     * are documented from their classes on the classpath of the project, with 
their subcommands.
+     */
+    @Test
+    void testPluginCommandsOfALibraryAreDocumented() throws Exception {
+        compileLibrary();
+        Path plugin = tempDir.resolve("plugin/commands");
+        write(plugin.resolve("test/TestPlugin.java"), """
+                package org.apache.camel.dsl.jbang.core.commands.test;
+
+                import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+                import org.apache.camel.dsl.jbang.core.common.CamelJBangPlugin;
+                import org.apache.camel.dsl.jbang.core.common.Plugin;
+                import org.example.cli.Agent;
+                import org.example.cli.AgentStart;
+                import org.example.cli.Run;
+                import picocli.CommandLine;
+
+                @CamelJBangPlugin(name = "camel-jbang-plugin-test", 
firstVersion = "4.14.0")
+                public class TestPlugin implements Plugin {
+
+                    @Override
+                    public void customize(CommandLine commandLine, 
CamelJBangMain main) {
+                        var cmd = new CommandLine(new TestCommand(main))
+                                .addSubcommand("run", new CommandLine(new 
Run(main)))
+                                .addSubcommand("agent", new CommandLine(new 
Agent(main))
+                                        .addSubcommand("start", new 
CommandLine(new AgentStart(main))));
+
+                        commandLine.addSubcommand("test", cmd);
+                    }
+                }
+                """);
+        write(plugin.resolve("test/TestCommand.java"), TEST_COMMAND);
+
+        JBangCommand test = generatePlugin(plugin, null);
+
+        
assertThat(test.getSubcommands()).extracting(JBangCommand::getFullName).containsExactly("test
 agent", "test run");
+        JBangCommand agent = test.getSubcommands().get(0);
+        // without the hint to the help of the subcommands
+        assertThat(agent.getDescription()).isEqualTo("Manage agents");
+        
assertThat(agent.getSubcommands()).extracting(JBangCommand::getFullName).containsExactly("test
 agent start");
+        
assertThat(agent.getSubcommands().get(0).getOptions()).extracting(JBangCommandOption::getNames)
+                .containsExactly("--port", "-h,--help");
+        JBangCommand run = test.getSubcommands().get(1);
+        assertThat(run.getDescription()).isEqualTo("Run as local test");
+        assertThat(run.getSourceClass()).isEqualTo("org.example.cli.Run");
+        
assertThat(run.getOptions()).extracting(JBangCommandOption::getNames).containsExactly("--engine",
 "-h,--help");
+    }
+
     /**
      * A parent class of the same package is found when the class has a static 
import on demand, for which Roaster
      * resolves the parent class as a nested class of the imported one (as 
camel export, whose ExportBaseCommand has
@@ -361,15 +422,7 @@ class PrepareCamelJBangCommandsMojoTest {
                     }
                 }
                 """);
-        write(plugin.resolve("test/TestCommand.java"), """
-                package org.apache.camel.dsl.jbang.core.commands.test;
-
-                import picocli.CommandLine.Command;
-
-                @Command(name = "test", description = "Manage tests")
-                public class TestCommand {
-                }
-                """);
+        write(plugin.resolve("test/TestCommand.java"), TEST_COMMAND);
         write(plugin.resolve("test/TestInit.java"), """
                 package org.apache.camel.dsl.jbang.core.commands.test;
 
@@ -381,9 +434,19 @@ class PrepareCamelJBangCommandsMojoTest {
                 }
                 """);
 
+        JBangCommand test = generatePlugin(plugin, core);
+        
assertThat(test.getSubcommands()).extracting(JBangCommand::getFullName).containsExactly("test
 init");
+        return test.getSubcommands().get(0);
+    }
+
+    /**
+     * Generates the metadata of the plugin with the commands in the given 
directory (and the commands of
+     * camel-jbang-core in the other one, when given). Returns the root 
command of the plugin.
+     */
+    private JBangCommand generatePlugin(Path plugin, Path core) throws 
Exception {
         PrepareCamelJBangCommandsMojo mojo = new 
PrepareCamelJBangCommandsMojo(null, null);
         mojo.commandsDir = plugin.toFile();
-        mojo.coreCommandsDir = core.toFile();
+        mojo.coreCommandsDir = core != null ? core.toFile() : null;
         mojo.outFolder = tempDir.resolve("generated-resources").toFile();
         mojo.project = 
projectWithOutputDir(tempDir.resolve("classes").toFile());
 
@@ -391,9 +454,7 @@ class PrepareCamelJBangCommandsMojoTest {
 
         String json = Files.readString(
                 
tempDir.resolve("generated-resources").resolve(PrepareCamelJBangCommandsMojo.PLUGIN_METADATA_FILE));
-        JBangCommand test = 
JsonMapper.generateJBangCommandModel(json).getCommands().get(0);
-        
assertThat(test.getSubcommands()).extracting(JBangCommand::getFullName).containsExactly("test
 init");
-        return test.getSubcommands().get(0);
+        return JsonMapper.generateJBangCommandModel(json).getCommands().get(0);
     }
 
     /**
@@ -432,6 +493,16 @@ class PrepareCamelJBangCommandsMojoTest {
                     @Target(ElementType.FIELD)
                     public @interface Mixin {
                     }
+
+                    @Retention(RetentionPolicy.RUNTIME)
+                    @Target(ElementType.TYPE)
+                    public @interface Command {
+                        String name() default "<main class>";
+
+                        String[] description() default {};
+
+                        boolean hidden() default false;
+                    }
                 }
                 """);
         write(sources.resolve("org/example/cli/LibraryCommand.java"), """
@@ -457,6 +528,39 @@ class PrepareCamelJBangCommandsMojoTest {
                     String directory;
                 }
                 """);
+        write(sources.resolve("org/example/cli/Run.java"), """
+                package org.example.cli;
+
+                import picocli.CommandLine;
+
+                @CommandLine.Command(name = "run", description = "Run as local 
test")
+                public class Run extends LibraryCommand {
+
+                    @CommandLine.Option(names = "--engine", description = "The 
test engine")
+                    String engine;
+                }
+                """);
+        write(sources.resolve("org/example/cli/Agent.java"), """
+                package org.example.cli;
+
+                import picocli.CommandLine;
+
+                @CommandLine.Command(name = "agent", description = "Manage 
agents (use agent --help to see sub commands)")
+                public class Agent extends LibraryCommand {
+                }
+                """);
+        write(sources.resolve("org/example/cli/AgentStart.java"), """
+                package org.example.cli;
+
+                import picocli.CommandLine;
+
+                @CommandLine.Command(name = "start", description = "Starts the 
agent as a server")
+                public class AgentStart extends LibraryCommand {
+
+                    @CommandLine.Option(names = "--port", description = "The 
port of the agent", defaultValue = "4567")
+                    int port;
+                }
+                """);
         Path classes = tempDir.resolve("classes");
         Files.createDirectories(classes);
         JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

Reply via email to