This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch fix/CAMEL-25718
in repository https://gitbox.apache.org/repos/asf/camel.git

commit b4736de5b5594afb82cf5a04e1ff178d65c66ce8
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Oct 17 10:49:20 2026 +0200

    CAMEL-25718: camel-jbang - Document the commands of camel test that come 
from Citrus
    
    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]>
    Signed-off-by: Claus Ibsen <[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   |  85 ++++++++++++--
 .../PrepareCamelJBangCommandsMojoTest.java         | 130 ++++++++++++++++++---
 11 files changed, 343 insertions(+), 63 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..27b6cc42ea3c 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,70 @@ 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) {
+            getLog().debug("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 +626,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();
             }
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