src/main/java/com.github.forax.pro.api/com/github/forax/pro/api/Config.java
public interface Config {
  /**
   * Returns the value associated with the key or {@code Optional#empty()} if there is no key registered. 
   * 
   * @param <T> type of the value.
   * @param key a qualified name 
   * @param type the expected type of the value associated with the key.
   * @return either the value corresponding to the key or {@code Optional#empty()} otherwise.
   * 
   * @throws ClassCastException if the value is not a subclass of the given type.
   * @throws IllegalStateException if one of the component of the key is not a tree node.
   */
  public <T> Optional<T> get(String key, Class<T> type);
  
  /**
   * Calls the consumer with each pair key/value contained in a node and its sub nodes.
   * @param key a qualified name corresponding to a node
   * @param consumer the consumer called for each pair key/value
   */
  //public void forEach(String key, BiConsumer<? super String, Object> consumer);
  
  /**
   * Returns the value associated with a key or throw a {@link NoSuchElementException} if the value do not exist.
   *
   * @param <T> type of the value.
   * @param key a qualified name.
   * @param type the expected type of the value corresponding to the key
   * @return  the value associated with the key.
   * 
   * @throws NoSuchElementException if the key has no corresponding value
   * @throws ClassCastException if the value is not a subclass of the given type.  
   * @throws IllegalStateException if one of the component of the key is not a tree node.
   */
  public default <T> T getOrThrow(String key, Class<T> type) {
    return get(key, type).orElseThrow(() -> new NoSuchElementException("no key " + key + " defined"));  
  }
}
src/main/java/com.github.forax.pro.api/com/github/forax/pro/api/helper/ProConf.java
@TypeCheckedConfig
public interface ProConf {
  Path currentDir();
  void currentDir(Path path);
  
  Path pluginDir();
  void pluginDir(Path path);
  
  String loglevel();
  void loglevel(String loglevel);
  
  boolean exitOnError();
  void exitOnError(boolean exitOnError);
  
  int errorCode();
  void errorCode(int errorCode);
  
  List<String> arguments();
  void arguments(List<String> arguments);
  
  Optional<List<String>> commands();
  void commands(List<String> commands);
}
src/main/java/com.github.forax.pro.helper/com/github/forax/pro/helper/FileHelper.java
public class FileHelper {
  private FileHelper() {
    throw new AssertionError();
  }

  public static List<Path> pathFromFilesThatExist(List<Path> paths) {
    return List.of(paths.stream().filter(Files::exists).toArray(Path[]::new));
  }

  /**
   * Expand a path containing some * into a list of the corresponding path.
   * @param path a path to expand
   * @return a list of the expanded path
   */
  public static List<Path> expand(Path path) {
    // IOExceptions are suppressed
    if (!path.toString().contains("*")) {
      return List.of(path);
    }
    var components = new ArrayList<Path>();
    path.iterator().forEachRemaining(components::add);
    if (components.size() == 1) {
      throw new IllegalArgumentException("a path can not start with a *");
    }
    var result = new ArrayList<Path>();
    try {
      expand(components.get(0), components, 1, result);
    } catch (IOException e) {
      throw new UncheckedIOException(e);
    }
    return List.copyOf(result);
  }

  private static void expand(Path path, ArrayList<Path> components, int index, ArrayList<Path> result) throws IOException {
    if (index == components.size()) {
      result.add(path);
      return;
    }
    var component = components.get(index);
    if (component.getFileName().toString().equals("*")) {
      for(var child: (Iterable<Path>)Files.list(path)::iterator) {
        expand(child, components, index + 1, result);
      }
    } else {
      expand(path.resolve(components.get(index)), components, index + 1, result);
    }
  }

  /**
   * Delete recursively all files inside a directory.
   * @param directory a directory
   * @param removeDirectory also delete the top level directory.
   */
  public static void deleteAllFiles(Path directory, boolean removeDirectory) {
    // IOExceptions are suppressed
    runUnchecked(() -> walkFileTree(directory, new FileVisitor<>() {
      @Override
      public FileVisitResult postVisitDirectory(Path path, IOException e) throws IOException {
        if (removeDirectory || !path.equals(directory)) {
          Files.delete(path);  
        }
        return CONTINUE;
      }

      @Override
      public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes attrs) throws IOException {
        return CONTINUE;
      }

      @Override
      public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException {
        Files.delete(path);
        return CONTINUE;
      }

      @Override
      public FileVisitResult visitFileFailed(Path path, IOException e) throws IOException {
        return CONTINUE;
      }
    }));
  }
  
  public static Predicate<Path> pathFilenameEndsWith(String text) {
    return path -> path.getFileName().toString().endsWith(text);
  }
  
  public static Predicate<Path> pathFilenameEquals(String text) {
    return path -> path.getFileName().toString().equals(text);
  }

  public static Stream<Path> list(Path directory) {
    // IOExceptions are suppressed
    try {
      return Files.list(directory);
    } catch (IOException e) {
      throw new UncheckedIOException(e);
    }
  }

  public static List<Path> walkIfNecessary(List<Path> list, Predicate<? super Path> filter) {
    // IOExceptions are suppressed
    return list.stream()
               .flatMap(suppress(path -> {
                   if (!isDirectory(path)) {
                     return Stream.of(path);
                   }
                   return walk(path).filter(filter);
               }))
               .collect(toUnmodifiableList());
  }
  
  public static void walkAndFindCounterpart(Path srcPath, Path dstPath, Function<Stream<Path>, Stream<Path>> configure, Unchecked.IOBiConsumer<Path, Path> consumer) {
    // IOExceptions are suppressed
    try(var stream = getUnchecked(() -> Files.walk(srcPath))) {
      configure.apply(stream)
        .forEach(suppress(path -> {
          var targetPath = dstPath.resolve(srcPath.relativize(path));
          consumer.accept(path, targetPath);
      }));
    }
  }
  
  public static void download(URI uri, Path targetDirectory) {
    // IOExceptions are suppressed
    var fileName = Path.of(uri.getPath()).getFileName();
    var targetFile = targetDirectory.resolve(fileName);
    if (exists(targetFile)) {
      return;
    }
    runUnchecked(() -> {
      try { 
        createDirectories(targetDirectory);
        try(var input = uri.toURL().openStream();
            var output = newOutputStream(targetFile, CREATE_NEW, WRITE)) {
          input.transferTo(output);
        }
      } catch (IOException e) {
        throw new IOException("download failed: url=" + uri + " targetDirectory=" + targetDirectory, e);
      }
    });
  }
}
src/main/java/com.github.forax.pro.api/com/github/forax/pro/api/helper/OptionAction.java
public interface OptionAction<C> {
  public Optional<UnaryOperator<CmdLine>> apply(C config);
  
  public static <C> OptionAction<C> rawValues(Function<? super C, ? extends Optional<? extends Collection<?>>> mapper) {
    return config -> mapper.apply(config).map(collection -> line -> { collection.forEach(value -> line.add(value)); return line; });
  }
  public static <C> OptionAction<C> exists(String optionName, Predicate<? super C> predicate) {
    return config -> Optional.of(config).filter(predicate).map(__ -> line -> line.add(optionName));
  }
  public static <C> OptionAction<C> action(String optionName, Function<? super C, ?> mapper) {
    return actionMaybe(optionName, mapper.andThen(Optional::of));
  }
  public static <C> OptionAction<C> action(String optionName, Function<? super C, ? extends Collection<?>> mapper, String separator) {
    return actionMaybe(optionName, mapper.andThen(Optional::of), separator);
  }
  public static <C> OptionAction<C> actionLoop(String optionName, Function<? super C, ? extends Collection<?>> mapper) {
    return config -> mapper.apply(config).stream().<UnaryOperator<CmdLine>>map(value -> line -> line.add(optionName).add(value)).reduce((op1, op2) -> line -> op2.apply(op1.apply(line)));
  }
  public static <C> OptionAction<C> actionMaybe(String optionName, Function<? super C, ? extends Optional<?>> mapper) {
    return config -> mapper.apply(config).map(value -> line -> line.add(optionName).add(value));
  }
  public static <C> OptionAction<C> actionMaybe(String optionName, Function<? super C, ? extends Optional<? extends Collection<?>>> mapper, String separator) {
    return config -> mapper.apply(config).map(value -> line -> line.add(optionName).add(value, separator));
  }
  
  public static <C, O> BiFunction<C, CmdLine, CmdLine> gatherAll(Class<O> optionType, Function<? super O, ? extends OptionAction<C>> mapper) {
    return gatherAll(Arrays.stream(optionType.getEnumConstants()).map(mapper));
  }
  private static <C> BiFunction<C, CmdLine, CmdLine> gatherAll(Stream<OptionAction<? super C>> actions) {
    return (config, cmdLine) -> actions.flatMap(action -> action.apply(config).stream())
                                       .reduce(line -> line, (op1, op2) -> line -> op2.apply(op1.apply(line)))
                                       .apply(cmdLine);
  }
  
  public static <C, O> BiFunction<C, String, String> toPrettyString(Class<O> optionType, Function<? super O, ? extends OptionAction<C>> mapper) {
    return toPrettyString(Arrays.stream(optionType.getEnumConstants()).map(mapper));
  }
  private static <C> BiFunction<C, String, String> toPrettyString(Stream<OptionAction<? super C>> actions) {
    return (config, command) -> {
      var spaces = IntStream.range(0, 1 + command.length()).mapToObj(__ -> " ").collect(joining());
      return actions.flatMap(action -> action.apply(config).stream()).map(op -> op.apply(new CmdLine()).toString()).collect(joining('\n' + spaces, command + ' ', ""));
    };
  }
}
src/main/java/com.github.forax.pro.api/com/github/forax/pro/api/Plugin.java
public interface Plugin extends Command {
  /**
   * Called to register the plugin properties with their default values
   * (with {@link MutableConfig#set(String, Object)}).
   * 
   * This method can be called several times with different configurations.
   * The configuration of other plugins may not available when this method
   * is called. If you want to access to the configuration of other plugins,
   * see {@link #configure(MutableConfig)}.
   * 
   * This method can only set values for properties that starts with the
   * {@link #name()} of the plugin.
   * 
   * The implementation of this method should *never* try to store the
   * {@code config} object send as parameter.
   * 
   * @param config a mutable configuration
   */
  public void init(MutableConfig config);
  
  /**
   * Called to register plugin properties that depends on values of other plugins.
   * 
   * This method can be called several times with different configurations.
   * 
   * The implementation of this method should use 
   * {@link MutableConfig#derive(Object, java.util.function.BiConsumer, Object, java.util.function.Function)}
   * to explain how a value is computed from the values of other properties.
   * 
   * The implementation of this method should *never* try to store the
   * {@code config} object send as parameter.
   * 
   * @param config a mutable configuration
   */
  public void configure(MutableConfig config);
  
  /**
   * Register the paths that should be watched when pro is started in daemon mode
   * 
   * THIS METHOD IS EXPERIMENTAL AND MAY CHANGE IN THE FUTURE
   * 
   * @param config    a read only configuration
   * @param registry  the registry of path that need to be watched 
   * 
   */
  public void watch(Config config, WatcherRegistry registry);
}
src/main/java/com.github.forax.pro.helper/com/github/forax/pro/helper/util/Unchecked.java
public static <V> V getUnchecked(IOSupplier<? extends V> supplier) {
  try {
    return supplier.get(); 
   } catch(Error | RuntimeException e) {
     throw e;
   } catch(Throwable throwable) {
     throw Hack.rethrow(throwable);
   }
}
src/main/java/com.github.forax.pro.api/com/github/forax/pro/api/MutableConfig.java
public interface MutableConfig extends Config {
  /**
   * Returns the value associated with the key or use vivification
   * to dynamically create an object implementing the interface {@code type}.
   * 
   * This method can only be used if the tree is in read/write mode.
   * @see #get(String, Class) for a read-only access.
   * 
   * @param <T> type of the value
   * @param key a qualified name
   * @param type the type of the value, it must be an interface tagged
   *        with the annotation {@code TypeCheckedConfig} to allow vivification.
   * @return the value associated with the key       
   *        
   * @throws NoSuchElementException if there is no value associated with the key.
   * @throws ClassCastException if the value is not a subclass of the given type.  
   * @throws IllegalStateException if one of the component of the key is not a tree node.   
   */
  public <T> T getOrUpdate(String key, Class<T> type);
  
  /**
   * Change the value associated with the key.
   * 
   * @param key a qualified name
   * @param value a value
   * 
   * @throws ClassCastException if the component prefix is a vivified object and
   *         the value is not a subclass of the type of the getter method of the
   *         interface used to create the vivified object. 
   * @throws IllegalStateException if one of the component of the key is not a tree node.
   */
  public void set(String key, Object value);
  
  /**
   * Allow to create a computed value that depends on another value from the same tree.
   * Each time the value will be asked, the function {@code eval} will be evaluated.
   * 
   * @param <T> type the vivified object {@code to}.
   * @param <U> type the vivified object {@code from}.
   * @param <V> type of the values.
   * @param to a vivified object that was obtained from the tree
   * @param setter a lambda that will set a key of the vivified object, it must be a method of
   *        the interface of the vivified object.
   * @param from a vivified object containing the value from which the current value will be derived
   * @param eval a function that calculate the value from the from object.
   */
  public static <T, U, V> void derive(T to, BiConsumer<? super T, ? super V> setter, U from, Function<? super U, ? extends V> eval) {
    Configs.derive(to, setter, from, eval);
  }
}
src/main/java/com.github.forax.pro.plugin.packager/com/github/forax/pro/plugin/packager/MetadataParser.java
interface Metadata {
  String moduleName();
  Optional<String> version();
  Optional<String> mainClass();
    
  static Metadata parse(String data) {
    String module, mainClass;
    var slashIndex = data.lastIndexOf('/');
    if (slashIndex != -1) {
      mainClass = data.substring(slashIndex + 1);
      module = data.substring(0, slashIndex);
    } else {
      module = data;
      mainClass = null;
    }
    String moduleName, version;
    var dashIndex = module.lastIndexOf('@');
    if (dashIndex != -1) {
      version = module.substring(dashIndex + 1);
      moduleName = module.substring(0, dashIndex);
    } else {
      version = null;
      moduleName = module;
    }
    return new Metadata() {
      @Override
      public String moduleName() {
        return moduleName;
      }
      @Override
      public Optional<String> mainClass() {
        return Optional.ofNullable(mainClass);
      }
      @Override
      public Optional<String> version() {
        return Optional.ofNullable(version);
      }
    };
  }
}
src/main/java/com.github.forax.pro.api/com/github/forax/pro/api/helper/OptionAction.java
public static <C> OptionAction<C> rawValues(Function<? super C, ? extends Optional<? extends Collection<?>>> mapper) {
  return config -> mapper.apply(config).map(collection -> line -> { collection.forEach(value -> line.add(value)); return line; });
}
src/main/java/com.github.forax.pro.helper/com/github/forax/pro/helper/Log.java
public class Log {
  /**
   * Log levels.
   */
  public enum Level {
    DEBUG, VERBOSE, INFO, QUIET
    ;
    final int level;

    private Level() {
      this.level = 1 + ordinal();
    }
    
    /** Get level from a name (not case sensitive)
     * @param name name of the levels.
     * @return the corresponding level
     */
    public static Level of(String name) {
      return Level.valueOf(name.toUpperCase());
    }
  }
  
  private final String name;
  private final int level;

  private Log(String name, int level) {
    this.name = Objects.requireNonNull(name);
    this.level = level;
  }
  
  private static final int DEBUG_LEVEL = Level.DEBUG.level;
  private static final int VERBOSE_LEVEL = Level.VERBOSE.level;
  private static final int INFO_LEVEL = Level.INFO.level;
  
  /**
   * Creates a logger with a name and a level.
   * @param name name of the logger.
   * @param level level of the logger.
   * @return a new logger
   */
  public static Log create(String name, String level)  {
    return create(name, Level.of(level));
  }
  
  /**
   * Creates a logger with a name and a level.
   * @param name name of the logger.
   * @param level level of the logger.
   * @return a new logger
   */
  public static Log create(String name, Level level)  {
    return new Log("[" + name + "] ", level.level);
  }
  
  /**
   * log an error message.
   * @param <T> type of the value.
   * @param value value that will be sent to the message factory.
   * @param messageFactory function that will creates the error message. 
   */
  public <T> void error(T value, Function<? super T, String> messageFactory) {
    System.err.println(name + messageFactory.apply(value));
  }
  
  /**
   * log an exception.
   * @param exception the exception to log. 
   */
  public void error(Throwable exception) {
    if (level == DEBUG_LEVEL) {
      exception.printStackTrace();
    } else {
      System.err.println(name + exception.getMessage());
    }
  }
  
  /**
   * log a debug message.
   * @param <T> type of the value.
   * @param value value that will be sent to the message factory.
   * @param messageFactory function that will creates the error message. 
   */
  public <T> void debug(T value, Function<? super T, String> messageFactory) {
    if (level == DEBUG_LEVEL) {
      System.out.println(name + messageFactory.apply(value));
    }
  }
  
  /**
   * log a verbose message.
   * @param <T> type of the value.
   * @param value value that will be sent to the message factory.
   * @param messageFactory function that will creates the error message. 
   */
  public <T> void verbose(T value, Function<? super T, String> messageFactory) {
    if (level <= VERBOSE_LEVEL) {
      System.out.println(name + messageFactory.apply(value));
    }
  }
  
  /**
   * log an info message.
   * @param <T> type of the value.
   * @param value value that will be sent to the message factory.
   * @param messageFactory function that will creates the error message. 
   */
  public <T> void info(T value, Function<? super T, String> messageFactory) {
    if (level <= INFO_LEVEL) {
      System.out.println(name + messageFactory.apply(value));
    }
  }
}
src/main/java/com.github.forax.pro.api/com/github/forax/pro/api/helper/OptionAction.java
public static <C> OptionAction<C> action(String optionName, Function<? super C, ?> mapper) {
  return actionMaybe(optionName, mapper.andThen(Optional::of));
}
src/main/java/com.github.forax.pro.api/com/github/forax/pro/api/helper/OptionAction.java
public static <C> OptionAction<C> actionMaybe(String optionName, Function<? super C, ? extends Optional<?>> mapper) {
  return config -> mapper.apply(config).map(value -> line -> line.add(optionName).add(value));
}
src/main/java/com.github.forax.pro.api/com/github/forax/pro/api/helper/CmdLine.java
public final class CmdLine {
  private final ArrayList<String> arguments = new ArrayList<>();
  
  public CmdLine add(Object value) {
    arguments.add(value.toString());  // implicit nullcheck
    return this;
  }
  
  public CmdLine addAll(Object... values) {
    for(var value: values) {
      add(value);
    }
    return this;
  }
  
  public CmdLine add(Collection<?> collection, String separator) {
    return add(collection.stream().map(Object::toString).collect(joining(separator)));
  }
  
  public String[] toArguments() {
    return arguments.toArray(String[]::new);
  }
  
  @Override
  public String toString() {
    return arguments.stream().collect(joining(" "));
  }
}


Question: package com.github.forax.pro.plugin.packager;
import static com.github.forax.pro.api.helper.OptionAction.action;
import static com.github.forax.pro.api.helper.OptionAction.actionMaybe;
import static com.github.forax.pro.api.helper.OptionAction.rawValues;
import static com.github.forax.pro.helper.util.Unchecked.getUnchecked;
import static java.nio.file.Files.list;
import java.io.IOException;
import java.lang.module.ModuleFinder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.spi.ToolProvider;
import com.github.forax.pro.api.Config;
import com.github.forax.pro.api.MutableConfig;
import com.github.forax.pro.api.Plugin;
import com.github.forax.pro.api.WatcherRegistry;
import com.github.forax.pro.api.helper.CmdLine;
import com.github.forax.pro.api.helper.OptionAction;
import com.github.forax.pro.api.helper.ProConf;
import com.github.forax.pro.helper.FileHelper;
import com.github.forax.pro.helper.Log;
import com.github.forax.pro.plugin.packager.MetadataParser.Metadata;




public class PackagerPlugin implements Plugin {
  @Override
  public String name() {
    return "packager";
  }

  @Override
Answer:   public void init(MutableConfig config) {