Java CLI example
The plugwerk-java-cli-example project shows how to build a standalone Java CLI whose subcommands can be added at runtime by installing PF4J plugins from a Plugwerk server. There is no Spring container, no web server, no auto-configuration magic — just a plain JVM, picocli for argument parsing, and PF4J's DefaultPluginManager for plugin lifecycle.
Who it is for
Section titled “Who it is for”- Plugin developers who want to ship a CLI command (or a small set of related commands) as a plugin into someone else's application without forking it.
- Host-application developers building a CLI / desktop / batch tool and want third parties — or a separate team inside the same organisation — to extend it dynamically.
- Anyone who needs a working blueprint for "Plugwerk in a non-Spring host".
What it demonstrates
Section titled “What it demonstrates”- A custom extension-point interface (
CliCommand) that ties picocli into the PF4J extension model. - The Plugwerk client plugin sitting alongside the CLI's own plugins in the same
plugins/directory. - The full marketplace loop in five built-in subcommands:
list,search,install,uninstall,update. - Dynamic command registration: a plugin you install today provides a brand-new subcommand on the next invocation, with picocli help, options, and error handling — no recompile of the host.
Project layout at a glance
Section titled “Project layout at a glance”| Module | Purpose |
| ----------------------------------------------- | ------------------------------------------------------------------------------- |
| plugwerk-java-cli-example-api | Defines CliCommand, the PF4J ExtensionPoint every command plugin implements |
| plugwerk-java-cli-example-app | The host: picocli root command + DefaultPluginManager wiring |
| plugwerk-java-cli-example-hello-cmd-plugin | Greets with --name and --language (en/de/es) |
| plugwerk-java-cli-example-sysinfo-cmd-plugin | Prints Java/OS/heap info; --all for all system properties |
The interface plugins implement is small enough to read at a glance:
public interface CliCommand extends ExtensionPoint { CommandLine toCommandLine();}A plugin returns its own picocli CommandLine and the host registers it as an additional subcommand on startup.
Run it
Section titled “Run it”The full bootstrap (start the local Plugwerk server, create a default namespace, mint an API key into $PLUGWERK_API_KEY) lives in the Quick start section of the examples-repo root README. After that you only need two example-specific things:
-
Build the host fat-jar:
Terminal window cd plugwerk-java-cli-example./gradlew :plugwerk-java-cli-example-app:assembleThe fat-jar lands in
plugwerk-java-cli-example-app/build/libs/*-fat.jar. -
Build and upload the example plugins:
Terminal window ./gradlew :plugwerk-java-cli-example-hello-cmd-plugin:assemble \:plugwerk-java-cli-example-sysinfo-cmd-plugin:assembleThe full upload + approve flow (
POST /plugin-releases, thenPOST /reviews/<release-id>/approve) is documented in the example's README.
Once both plugins are published, the rest is the user-facing flow below.
A user flow
Section titled “A user flow”This is what the example feels like once everything is set up — the loop that the host application supports, end to end:
-
List what is published in your namespace:
Terminal window JAR=plugwerk-java-cli-example-app/build/libs/*-fat.jarjava -jar $JAR --server=http://localhost:8080 --api-key=$PLUGWERK_API_KEY listYou see both example plugins, with their plugin IDs and versions.
-
Install the
helloplugin:Terminal window java -jar $JAR --server=http://localhost:8080 --api-key=$PLUGWERK_API_KEY \install io.plugwerk.example.cli.hello 0.1.0-SNAPSHOTOutput:
Successfully installed io.plugwerk.example.cli.hello@0.1.0-SNAPSHOT[plugin] Registered dynamic command: hello -
Use the new subcommand — it is now part of the CLI's surface as if it had always been there:
Terminal window java -jar $JAR hello --name=Plugwerk --language=deOutput:
Hallo, Plugwerk!The same
--helpmachinery picocli gives the host commands also covers the dynamic ones —java -jar $JAR hello --helpprints the plugin's options. -
Repeat with
sysinfoto see a second plugin contribute a different command:Terminal window java -jar $JAR --server=http://localhost:8080 --api-key=$PLUGWERK_API_KEY \install io.plugwerk.example.cli.sysinfo 0.1.0-SNAPSHOTjava -jar $JAR sysinfo -
Uninstall to see the command disappear:
Terminal window java -jar $JAR uninstall io.plugwerk.example.cli.hellojava -jar $JAR hello # unknown subcommand on next run
The same flow works against any Plugwerk server — a development instance via docker compose up -d, a shared team server, or a production install. Only the --server URL and the --api-key change.
Authentication in one paragraph
Section titled “Authentication in one paragraph”- Read-only server operations (
list,search, plus the catalog download thatinstalluses internally) work anonymously against a namespace whosepublicCatalogistrue. No--api-keyneeded. - Server operations against a private namespace (
publicCatalog = false) need a namespace-scoped API key, sent as theX-Api-Keyheader. The CLI accepts it via the--api-keyflag, thePLUGWERK_API_KEYenvironment variable, or both. uninstallis local-only. It removes the plugin's ZIP and extracted directory from the host'splugins/folder and tells PF4J to unload the plugin. It does not call the Plugwerk server, so it never needs an API key.
API keys are read-only over the REST API in general — uploads, approvals, and any admin action need a JWT. See Authentication for the full overview.
Configuration reference
Section titled “Configuration reference”| Option | Short | Env | Default | Description |
| -------------- | ----- | -------------------- | ------------------------- | --------------------------------- |
| --server | -s | PLUGWERK_SERVER_URL| http://localhost:8080 | Plugwerk server base URL |
| --namespace | -n | PLUGWERK_NAMESPACE | default | Namespace slug |
| --plugins-dir| | PLUGWERK_PLUGINS_DIR| ./plugins | PF4J plugins directory |
| --api-key | -k | PLUGWERK_API_KEY | (none) | Namespace-scoped API key |
--plugins-dir is resolved relative to the current working directory. Use an absolute path when invoking the JAR from a different folder.
Where the source lives
Section titled “Where the source lives”- Repository: plugwerk/examples
- This example:
plugwerk-java-cli-example/ - Per-example README — full setup, configuration reference, "Writing your own CLI plugin" walkthrough:
plugwerk-java-cli-example/README.md
Related pages
Section titled “Related pages”- First plugin upload — the minimal "upload a plugin" walkthrough that this example builds on
- Plugin lifecycle — the draft → published → archived flow you trigger when uploading the example plugins
- Spring Boot + Thymeleaf example — the same loop but with web pages instead of CLI subcommands