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 55ed20f14e90 CAMEL-25717: camel-jbang - The docs of camel test init 
list its own options, not those of camel init (#27997)
55ed20f14e90 is described below

commit 55ed20f14e902a400dc5048691f4f592481ffbbf
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Oct 17 10:08:11 2026 +0200

    CAMEL-25717: camel-jbang - The docs of camel test init list its own 
options, not those of camel init (#27997)
    
    PrepareCamelJBangCommandsMojo found the source of a parent class (and of a 
mixin type) by its simple name, so camel test init, whose parent is the Init of 
Citrus, was documented with the options of camel init. A type that the class 
imports by name is now only taken from a source of that type, or else (also 
when that source cannot be parsed) its options are read from the class on the 
classpath of the project, by reflection. A type that is not imported by name is 
looked up as before, as [...]
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../jbang-commands/camel-jbang-test-init.adoc      |   9 +-
 .../camel-jbang-plugin-commands-metadata.json      |   2 +-
 .../packaging/PrepareCamelJBangCommandsMojo.java   | 137 ++++++++---
 .../PrepareCamelJBangCommandsMojoTest.java         | 270 +++++++++++++++++++++
 4 files changed, 377 insertions(+), 41 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-init.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-init.adoc
index b8f803053c14..197f4499bcf8 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-init.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-init.adoc
@@ -21,14 +21,7 @@ camel test init [options]
 [cols="2,5,1,2",options="header"]
 |===
 | Option | Description | Default | Type
-| `--clean-dir,--clean-directory` | Whether to clean directory first (deletes 
all files in directory) |  | boolean
-| `--dir,--directory` | Directory relative path where the new Camel 
integration will be saved | . | String
-| `--force` | Overwrite a file that exists already (by default the command 
fails instead) |  | boolean
-| `--from-kamelet` | To be used when extending an existing Kamelet |  | String
-| `--kamelets-version` | Apache Camel Kamelets version (auto-detected from 
classpath if not set) |  | String
-| `--list` | List available templates |  | boolean
-| `--pipe` | When creating a yaml file should it be created as a Pipe CR |  | 
boolean
-| `--repo,--repos` | Additional maven repositories (Use commas to separate 
multiple repositories) |  | String
+| `--directory` | Directory where the files will be created | . | String
 | `-h,--help` | Display the help and sub-commands |  | boolean
 |===
 
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 36d3c46b9e14..173749a94911 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": "init", "fullName": "test init", "description": "Creates a new Citrus 
test", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.test.T [...]
   ]
 }
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 6cd3b9952277..9e46cd905846 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
@@ -18,6 +18,8 @@ package org.apache.camel.maven.packaging;
 
 import java.io.File;
 import java.io.IOException;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
@@ -48,6 +50,7 @@ import org.jboss.forge.roaster.Roaster;
 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.JavaClassSource;
 import org.jboss.forge.roaster.model.source.JavaSource;
 
@@ -485,26 +488,7 @@ public class PrepareCamelJBangCommandsMojo extends 
AbstractGeneratorMojo {
         // First, parse options from parent class (if any)
         String superType = clazz.getSuperType();
         if (superType != null && !superType.equals("java.lang.Object") && 
!superType.equals("Object")) {
-            // Try to find and parse the parent class
-            String parentClassName = superType;
-            if (parentClassName.contains(".")) {
-                parentClassName = 
parentClassName.substring(parentClassName.lastIndexOf('.') + 1);
-            }
-
-            // Search for parent class file
-            File parentFile = findClassFile(baseDir, parentClassName);
-            if (parentFile != null && parentFile.exists()) {
-                try {
-                    JavaType<?> parsedParent = Roaster.parse(parentFile);
-                    if (parsedParent instanceof JavaClassSource parentClazz) {
-                        parseOptionsFromClassHierarchy(parentClazz, options, 
parentFile.getParentFile());
-                    } else {
-                        getLog().debug("Skipping options from non-class parent 
type: " + parentClassName);
-                    }
-                } catch (Exception e) {
-                    getLog().debug("Could not parse parent class: " + 
parentClassName + " - " + e.getMessage());
-                }
-            }
+            parseOptionsFromType(clazz, superType, options, baseDir, "parent");
         }
 
         // Then parse options from this class (will override parent options 
with same name)
@@ -534,24 +518,113 @@ public class PrepareCamelJBangCommandsMojo extends 
AbstractGeneratorMojo {
         // Also parse options from @CommandLine.Mixin fields
         for (FieldSource<JavaClassSource> field : clazz.getFields()) {
             if (hasMixinAnnotation(field)) {
-                String mixinTypeName = field.getType().getName();
-                File mixinFile = findClassFile(baseDir, mixinTypeName);
-                if (mixinFile != null && mixinFile.exists()) {
-                    try {
-                        JavaType<?> parsedMixin = Roaster.parse(mixinFile);
-                        if (parsedMixin instanceof JavaClassSource mixinClazz) 
{
-                            parseOptionsFromClassHierarchy(mixinClazz, 
options, mixinFile.getParentFile());
-                        } else {
-                            getLog().debug("Skipping options from non-class 
mixin type: " + mixinTypeName);
-                        }
-                    } catch (Exception e) {
-                        getLog().debug("Could not parse mixin class: " + 
mixinTypeName + " - " + e.getMessage());
+                parseOptionsFromType(clazz, field.getType().getName(), 
options, baseDir, "mixin");
+            }
+        }
+    }
+
+    /**
+     * Parses the options of a parent class or a mixin type 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, such 
as the class of a library that a plugin
+     * command extends. The source is found by the simple name of the type, so 
for a type that the class imports by name
+     * it is only taken when it is of that type, as a class of a library may 
have the simple name of a command of
+     * camel-jbang-core. (The package that Roaster resolves for a type that is 
not imported by name is not certain: with
+     * an import on demand of a class, it takes a class of the same package 
for a nested class of the imported one.)
+     */
+    private void parseOptionsFromType(JavaClassSource clazz, String type, 
List<OptionInfo> options, File baseDir, String kind) {
+        String simpleName = type.substring(type.lastIndexOf('.') + 1);
+        String imported = importedByName(clazz, simpleName);
+        File file = findClassFile(baseDir, simpleName);
+        if (file != null && file.exists()) {
+            try {
+                JavaType<?> parsed = Roaster.parse(file);
+                if (imported == null || 
imported.equals(parsed.getQualifiedName())) {
+                    if (parsed instanceof JavaClassSource source) {
+                        parseOptionsFromClassHierarchy(source, options, 
file.getParentFile());
+                    } else {
+                        getLog().debug("Skipping options from non-class " + 
kind + " type: " + simpleName);
                     }
+                    return;
+                }
+            } catch (Exception e) {
+                getLog().debug("Could not parse " + kind + " class: " + 
simpleName + " - " + e.getMessage());
+                if (imported == null) {
+                    return;
+                }
+            }
+        }
+        if (imported != null) {
+            parseOptionsFromClass(imported, options);
+        }
+    }
+
+    /**
+     * The qualified name of the type with the simple name that the class 
imports by name, or null when it does not.
+     */
+    private static String importedByName(JavaClassSource clazz, String 
simpleName) {
+        for (Import imported : clazz.getImports()) {
+            if (!imported.isWildcard() && !imported.isStatic() && 
imported.getSimpleName().equals(simpleName)) {
+                return imported.getQualifiedName();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Parses the options of a class that is not one of the command classes 
from its picocli annotations (and those of
+     * its parent classes and mixins), with the classpath of the project. 
picocli is not a dependency of this plugin, so
+     * the annotations are read by reflection.
+     */
+    @SuppressWarnings("unchecked")
+    private void parseOptionsFromClass(String className, List<OptionInfo> 
options) {
+        try {
+            ClassLoader loader = getProjectClassLoader();
+            Class<? extends Annotation> optionType
+                    = (Class<? extends Annotation>) 
loader.loadClass("picocli.CommandLine$Option");
+            Class<? extends Annotation> mixinType
+                    = (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());
+        }
+    }
+
+    private void parseOptionsFromClass(
+            Class<?> clazz, Class<? extends Annotation> optionType, Class<? 
extends Annotation> mixinType,
+            List<OptionInfo> options)
+            throws ReflectiveOperationException {
+        if (clazz.getSuperclass() != null && clazz.getSuperclass() != 
Object.class) {
+            parseOptionsFromClass(clazz.getSuperclass(), optionType, 
mixinType, options);
+        }
+        for (Field field : clazz.getDeclaredFields()) {
+            Annotation optionAnn = field.getAnnotation(optionType);
+            if (optionAnn != null) {
+                OptionInfo option = new OptionInfo();
+                option.names = String.join(",", (String[]) 
attribute(optionAnn, "names"));
+                option.description = sanitizeDescription(String.join(" ", 
(String[]) attribute(optionAnn, "description")));
+                String defaultValue = (String) attribute(optionAnn, 
"defaultValue");
+                // how picocli marks an option without a default value
+                if (!"__no_default_value__".equals(defaultValue)) {
+                    option.defaultValue = sanitizeDescription(defaultValue);
                 }
+                option.required = (Boolean) attribute(optionAnn, "required");
+                option.hidden = (Boolean) attribute(optionAnn, "hidden");
+                String paramLabel = (String) attribute(optionAnn, 
"paramLabel");
+                option.paramLabel = paramLabel.isEmpty() ? null : paramLabel;
+                option.javaType = field.getType().getTypeName();
+                option.deprecated = 
field.isAnnotationPresent(Deprecated.class);
+                options.removeIf(o -> o.names.equals(option.names));
+                options.add(option);
+            } else if (field.isAnnotationPresent(mixinType)) {
+                parseOptionsFromClass(field.getType(), optionType, mixinType, 
options);
             }
         }
     }
 
+    private static Object attribute(Annotation annotation, String name) throws 
ReflectiveOperationException {
+        return annotation.annotationType().getMethod(name).invoke(annotation);
+    }
+
     private File findClassFile(File dir, String className) {
         // Search in current directory
         File file = new File(dir, className + ".java");
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 582e47054fa1..553683627b87 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
@@ -17,11 +17,20 @@
 package org.apache.camel.maven.packaging;
 
 import java.io.File;
+import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.stream.Stream;
 
+import javax.tools.JavaCompiler;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.ToolProvider;
+
+import org.apache.camel.tooling.model.JBangCommandModel.JBangCommand;
+import org.apache.camel.tooling.model.JBangCommandModel.JBangCommandOption;
+import org.apache.camel.tooling.model.JsonMapper;
 import org.apache.maven.model.Build;
 import org.apache.maven.plugin.logging.Log;
 import org.apache.maven.plugin.logging.SystemStreamLog;
@@ -37,6 +46,21 @@ import static org.assertj.core.api.Assertions.assertThat;
  */
 class PrepareCamelJBangCommandsMojoTest {
 
+    // the command Init of camel-jbang-core
+    private static final String CORE_INIT = """
+            package org.apache.camel.dsl.jbang.core.commands;
+
+            import picocli.CommandLine.Command;
+            import picocli.CommandLine.Option;
+
+            @Command(name = "init", description = "Creates a new Camel 
integration")
+            public class Init {
+
+                @Option(names = "--from-kamelet", description = "To be used 
when extending an existing Kamelet")
+                String fromKamelet;
+            }
+            """;
+
     @TempDir
     Path tempDir;
 
@@ -204,6 +228,252 @@ class PrepareCamelJBangCommandsMojoTest {
                 .isEmpty();
     }
 
+    /**
+     * CAMEL-25717: a plugin command whose parent class is of a library does 
not get the options of the command of
+     * camel-jbang-core with the simple name of that class, as camel test init 
(whose parent is the Init of Citrus) got
+     * the options of camel init.
+     */
+    @Test
+    void 
testPluginCommandDoesNotGetTheOptionsOfTheCoreCommandWithTheNameOfItsParent() 
throws Exception {
+        JBangCommand init = generatePluginWithLibraryParent(CORE_INIT);
+
+        
assertThat(init.getOptions()).extracting(JBangCommandOption::getNames).doesNotContain("--from-kamelet");
+    }
+
+    /**
+     * CAMEL-25717: a plugin command whose parent class is of a library gets 
the options of that class, and of its
+     * parent classes, from the classpath of the project.
+     */
+    @Test
+    void testPluginCommandGetsTheOptionsOfTheLibraryClassItExtends() throws 
Exception {
+        compileLibrary();
+
+        JBangCommand init = generatePluginWithLibraryParent(CORE_INIT);
+
+        
assertThat(init.getOptions()).extracting(JBangCommandOption::getNames).containsExactly("--directory",
 "-h,--help");
+        JBangCommandOption directory = init.getOptions().get(0);
+        assertThat(directory.getDescription()).isEqualTo("Directory where the 
files will be created");
+        assertThat(directory.getDefaultValue()).isEqualTo(".");
+        assertThat(directory.getJavaType()).isEqualTo("java.lang.String");
+    }
+
+    /**
+     * A source with the simple name of the library class that cannot be 
parsed does not keep the options of the library
+     * class from the classpath of the project.
+     */
+    @Test
+    void 
testPluginCommandGetsTheOptionsOfTheLibraryClassWhenASourceWithItsNameCannotBeParsed()
 throws Exception {
+        compileLibrary();
+
+        JBangCommand init = generatePluginWithLibraryParent("this is not 
java");
+
+        
assertThat(init.getOptions()).extracting(JBangCommandOption::getNames).containsExactly("--directory",
 "-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
+     * {@code import static ...CamelJBangConstants.*}).
+     */
+    @Test
+    void testParentOfTheSamePackageWithAStaticImportOnDemand() throws 
Exception {
+        Path commands = tempDir.resolve("commands");
+        write(commands.resolve("BaseCommand.java"), """
+                package org.apache.camel.dsl.jbang.core.commands;
+
+                import picocli.CommandLine.Option;
+
+                public abstract class BaseCommand {
+
+                    @Option(names = "--verbose", description = "Verbose 
output")
+                    boolean verbose;
+                }
+                """);
+        write(commands.resolve("Export.java"), """
+                package org.apache.camel.dsl.jbang.core.commands;
+
+                import picocli.CommandLine.Command;
+                import picocli.CommandLine.Option;
+
+                import static 
org.apache.camel.dsl.jbang.core.common.CamelJBangConstants.*;
+
+                @Command(name = "export", description = "Export to other 
runtimes")
+                public class Export extends BaseCommand {
+
+                    @Option(names = "--dir", description = "Directory where 
the project will be exported")
+                    String dir;
+                }
+                """);
+        write(commands.resolve("CamelJBangMain.java"), """
+                package org.apache.camel.dsl.jbang.core.commands;
+
+                import picocli.CommandLine;
+                import picocli.CommandLine.Command;
+
+                @Command(name = "camel")
+                public class CamelJBangMain {
+                    public static void main(String[] args) {
+                        CommandLine commandLine;
+                        commandLine = new CommandLine(new CamelJBangMain())
+                                .addSubcommand("export", new CommandLine(new 
Export()));
+                    }
+                }
+                """);
+
+        PrepareCamelJBangCommandsMojo mojo = new 
PrepareCamelJBangCommandsMojo(null, null);
+        mojo.commandsDir = commands.toFile();
+        mojo.outFolder = tempDir.resolve("generated-resources").toFile();
+        mojo.project = 
projectWithOutputDir(tempDir.resolve("classes").toFile());
+
+        mojo.execute();
+
+        String json = Files.readString(
+                
tempDir.resolve("generated-resources").resolve(PrepareCamelJBangCommandsMojo.CORE_METADATA_FILE));
+        JBangCommand export = 
JsonMapper.generateJBangCommandModel(json).getCommands().get(0);
+        
assertThat(export.getOptions()).extracting(JBangCommandOption::getNames).containsExactly("--dir",
 "--verbose");
+    }
+
+    /**
+     * A plugin as camel-jbang-plugin-test: camel test init, whose parent 
class Init is of a library, while
+     * camel-jbang-core has a command class Init too (with the given source). 
Returns the generated test init command.
+     */
+    private JBangCommand generatePluginWithLibraryParent(String coreInit) 
throws Exception {
+        Path core = tempDir.resolve("core/commands");
+        write(core.resolve("Init.java"), coreInit);
+        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 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("init", new CommandLine(new 
TestInit(main)));
+
+                        commandLine.addSubcommand("test", cmd);
+                    }
+                }
+                """);
+        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/TestInit.java"), """
+                package org.apache.camel.dsl.jbang.core.commands.test;
+
+                import org.example.cli.Init;
+                import picocli.CommandLine.Command;
+
+                @Command(name = "init", description = "Creates a new test")
+                public class TestInit extends Init {
+                }
+                """);
+
+        PrepareCamelJBangCommandsMojo mojo = new 
PrepareCamelJBangCommandsMojo(null, null);
+        mojo.commandsDir = plugin.toFile();
+        mojo.coreCommandsDir = core.toFile();
+        mojo.outFolder = tempDir.resolve("generated-resources").toFile();
+        mojo.project = 
projectWithOutputDir(tempDir.resolve("classes").toFile());
+
+        mojo.execute();
+
+        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);
+    }
+
+    /**
+     * Compiles the library of the command classes into the classes of the 
project. picocli is not a dependency of this
+     * plugin, so the library has the annotations of picocli that the plugin 
reads.
+     */
+    private void compileLibrary() throws IOException {
+        Path sources = tempDir.resolve("library");
+        write(sources.resolve("picocli/CommandLine.java"), """
+                package picocli;
+
+                import java.lang.annotation.ElementType;
+                import java.lang.annotation.Retention;
+                import java.lang.annotation.RetentionPolicy;
+                import java.lang.annotation.Target;
+
+                public class CommandLine {
+
+                    @Retention(RetentionPolicy.RUNTIME)
+                    @Target(ElementType.FIELD)
+                    public @interface Option {
+                        String[] names();
+
+                        String[] description() default {};
+
+                        String defaultValue() default "__no_default_value__";
+
+                        boolean required() default false;
+
+                        boolean hidden() default false;
+
+                        String paramLabel() default "";
+                    }
+
+                    @Retention(RetentionPolicy.RUNTIME)
+                    @Target(ElementType.FIELD)
+                    public @interface Mixin {
+                    }
+                }
+                """);
+        write(sources.resolve("org/example/cli/LibraryCommand.java"), """
+                package org.example.cli;
+
+                import picocli.CommandLine;
+
+                public abstract class LibraryCommand {
+
+                    @CommandLine.Option(names = { "-h", "--help" }, 
description = "Display the help and sub-commands")
+                    boolean help;
+                }
+                """);
+        write(sources.resolve("org/example/cli/Init.java"), """
+                package org.example.cli;
+
+                import picocli.CommandLine;
+
+                public class Init extends LibraryCommand {
+
+                    @CommandLine.Option(names = "--directory", description = 
"Directory where the files will be created",
+                                        defaultValue = ".")
+                    String directory;
+                }
+                """);
+        Path classes = tempDir.resolve("classes");
+        Files.createDirectories(classes);
+        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+        try (StandardJavaFileManager fileManager = 
compiler.getStandardFileManager(null, null, null);
+             Stream<Path> files = Files.walk(sources)) {
+            List<File> javaFiles = files.filter(f -> 
f.toString().endsWith(".java")).map(Path::toFile).toList();
+            Boolean compiled = compiler.getTask(null, fileManager, null, 
List.of("-d", classes.toString()), null,
+                    fileManager.getJavaFileObjectsFromFiles(javaFiles)).call();
+            assertThat(compiled).isTrue();
+        }
+    }
+
+    private static void write(Path file, String content) throws IOException {
+        Files.createDirectories(file.getParent());
+        Files.writeString(file, content);
+    }
+
     private static MavenProject projectWithOutputDir(File outputDir) {
         outputDir.mkdirs();
         Build build = new Build();

Reply via email to