Maven Project Setup
Add a CLI module to an existing Maven project using the shade plugin and crest-maven-plugin.
Most projects already have a multi-module Maven structure with domain logic, services, and APIs. To add a CLI powered by Crest, create a dedicated module that pulls in your existing code and packages it as an executable.
Project Structure
A typical layout adds a mytool-cli module alongside your existing modules:
myproject/
myproject-core/ # Domain logic, services
myproject-api/ # Shared interfaces
myproject-cli/ # CLI module (new)
src/main/java/
com/example/cli/
MyCommands.java # @Command methods
MyLoader.java # Loader implementation
pom.xml
pom.xml # Parent POM
CLI Module POM
The CLI module’s pom.xml has three parts: dependencies on Crest and your project modules, the shade plugin to create an uber jar, and the crest-maven-plugin to generate descriptors and create the executable.
<project>
<parent>
<groupId>com.example</groupId>
<artifactId>myproject</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>myproject-cli</artifactId>
<dependencies>
<!-- Your project modules -->
<dependency>
<groupId>com.example</groupId>
<artifactId>myproject-core</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Crest runtime -->
<dependency>
<groupId>org.tomitribe</groupId>
<artifactId>tomitribe-crest</artifactId>
</dependency>
<dependency>
<groupId>org.tomitribe</groupId>
<artifactId>tomitribe-crest-api</artifactId>
</dependency>
<!-- Classpath scanning (optional — see Loader below) -->
<dependency>
<groupId>org.tomitribe</groupId>
<artifactId>tomitribe-crest-xbean</artifactId>
</dependency>
</dependencies>
<build>
<defaultGoal>package</defaultGoal>
<plugins>
<!-- 1. Create an uber jar with all dependencies -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<shadedArtifactAttached>true</shadedArtifactAttached>
<shadedClassifierName>all</shadedClassifierName>
<dependencyReducedPomLocation>
${project.build.directory}/reduced-pom.xml
</dependencyReducedPomLocation>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.tomitribe.crest.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
<!-- 2. Generate descriptors + create executable -->
<plugin>
<groupId>org.tomitribe</groupId>
<artifactId>crest-maven-plugin</artifactId>
<version>${crest.version}</version>
<executions>
<execution>
<goals>
<goal>descriptor</goal>
<goal>executable</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
What Each Plugin Does
maven-shade-plugin
Creates a single jar containing all dependencies (your modules, Crest, transitive deps). The ManifestResourceTransformer sets org.tomitribe.crest.Main as the entry point so java -jar works. The shadedArtifactAttached option keeps the original jar and produces the uber jar with an -all classifier.
crest-maven-plugin
Provides two goals:
descriptor — Runs at compile time to scan your compiled classes for @Command, @Editor, @CrestInterceptor, and @GlobalOptions annotations and generate a crest-commands.txt file. With scanDependencies it also scans your runtime dependencies, the same set the shade plugin inlines. It also generates the META-INF/services/org.tomitribe.crest.api.Loader service file pointing to the built-in CrestCommandsLoader, unless you provide your own.
executable — Runs at package time to prepend a shell stub to the shaded jar, making it directly executable on Unix systems without typing java -jar. After mvn package, you get an executable named after your artifact:
$ ./myproject-cli --help
The default shell stub passes -Dcmd="$0" so Crest can show the script name in help output, and $JAVA_OPTS so users can set JVM flags via the environment. The executable is attached as a build artifact by default, so mvn install and mvn deploy publish it alongside the jar.
Descriptor Goal Configuration
The descriptor goal scans your compiled classes and generates crest-commands.txt. Set scanDependencies to also scan the jars that will end up in your uber jar. Use excludes to remove classes from the scan results and includes to add classes that the scanner wouldn’t find:
<plugin>
<groupId>org.tomitribe</groupId>
<artifactId>crest-maven-plugin</artifactId>
<version>${crest.version}</version>
<executions>
<execution>
<goals>
<goal>descriptor</goal>
<goal>executable</goal>
</goals>
<configuration>
<scanDependencies>true</scanDependencies>
<configuration>
<excludes>
<exclude>com.example.internal.*</exclude>
<exclude>com.example.DebugCommands</exclude>
</excludes>
<includes>
<include>com.example.extra.ManualCommand</include>
</includes>
</configuration>
</execution>
</executions>
</plugin>
Exclude patterns support * as a wildcard that matches any characters. For example, com.example.internal.* matches all classes in that package and its sub-packages.
scanDependencies is off by default. When on, every artifact resolved at runtime scope is scanned, which is the same set maven-shade-plugin inlines by default, so commands living in a library you depend on are discovered without that library shipping its own descriptor. Dependencies that are still unpackaged reactor modules are scanned as directories. The Crest runtime itself is never scanned. If a dependency contributes commands you do not want, drop them with excludes.
Executable Goal Configuration
All parameters have sensible defaults. Override them only when needed:
| Parameter | Default | Description |
|---|---|---|
flags | -Dcmd="$0" $JAVA_OPTS | JVM arguments embedded in the shell stub |
programFile | ${project.artifactId} | Name of the generated executable |
classifier | all | Classifier of the shaded jar to use as input |
attachProgramFile | true | Attach the executable as a build artifact |
scriptFile | — | Path to a custom shell script instead of the default stub |
inputFile | — | Specific jar file to use instead of finding by classifier |
Example with custom flags:
<plugin>
<groupId>org.tomitribe</groupId>
<artifactId>crest-maven-plugin</artifactId>
<version>${crest.version}</version>
<executions>
<execution>
<goals>
<goal>descriptor</goal>
<goal>executable</goal>
</goals>
<configuration>
<flags>-Dcmd="$0" $JAVA_OPTS -Xmx2G</flags>
</configuration>
</execution>
</executions>
</plugin>
Build and Run
mvn package
./myproject-cli/target/myproject-cli help
The target/ directory will contain:
myproject-cli-1.0-SNAPSHOT.jar— original jarmyproject-cli-1.0-SNAPSHOT-all.jar— uber jar with all dependenciesmyproject-cli— directly executable binary
Command Discovery
Crest needs to find your @Command, @Editor, and @CrestInterceptor classes. You have three options:
Option A: crest-maven-plugin (recommended)
The descriptor goal scans your compiled classes at build time and generates a crest-commands.txt file plus the service loader wiring. No runtime scanning, no extra dependencies. Use excludes and includes in the plugin configuration if you need to control what’s discovered.
Option B: Explicit Loader
For full control, create a Loader that lists your classes explicitly:
public class MyLoader implements Loader {
@Override
public Iterator<Class<?>> iterator() {
return Loader.of(
DeployCommands.class,
ConfigCommands.class,
StatusCommands.class,
AuditInterceptor.class,
InstantEditor.class
).iterator();
}
}
Register it in META-INF/services/org.tomitribe.crest.api.Loader:
com.example.cli.MyLoader
When an explicit Loader is present, it is authoritative — Crest uses it and does not fall back to other discovery mechanisms.
Option C: Main.builder()
For full programmatic control, write your own main() method instead of using org.tomitribe.crest.Main:
public class MyCli {
public static void main(String[] args) throws Exception {
new Main.builder()
.command(DeployCommands.class)
.command(ConfigCommands.class)
.load(AuditInterceptor.class)
.load(InstantEditor.class)
.name("mytool")
.version("1.0")
.build()
.run(args);
}
}
Then update the shade plugin’s <mainClass> to com.example.cli.MyCli.