Skip to content

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.

  • 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".
  • 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.

| 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.

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:

  1. Build the host fat-jar:

    Terminal window
    cd plugwerk-java-cli-example
    ./gradlew :plugwerk-java-cli-example-app:assemble

    The fat-jar lands in plugwerk-java-cli-example-app/build/libs/*-fat.jar.

  2. Build and upload the example plugins:

    Terminal window
    ./gradlew :plugwerk-java-cli-example-hello-cmd-plugin:assemble \
    :plugwerk-java-cli-example-sysinfo-cmd-plugin:assemble

    The full upload + approve flow (POST /plugin-releases, then POST /reviews/<release-id>/approve) is documented in the example's README.

Once both plugins are published, the rest is the user-facing flow below.

This is what the example feels like once everything is set up — the loop that the host application supports, end to end:

  1. List what is published in your namespace:

    Terminal window
    JAR=plugwerk-java-cli-example-app/build/libs/*-fat.jar
    java -jar $JAR --server=http://localhost:8080 --api-key=$PLUGWERK_API_KEY list

    You see both example plugins, with their plugin IDs and versions.

  2. Install the hello plugin:

    Terminal window
    java -jar $JAR --server=http://localhost:8080 --api-key=$PLUGWERK_API_KEY \
    install io.plugwerk.example.cli.hello 0.1.0-SNAPSHOT

    Output:

    Successfully installed io.plugwerk.example.cli.hello@0.1.0-SNAPSHOT
    [plugin] Registered dynamic command: hello
  3. 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=de

    Output:

    Hallo, Plugwerk!

    The same --help machinery picocli gives the host commands also covers the dynamic ones — java -jar $JAR hello --help prints the plugin's options.

  4. Repeat with sysinfo to 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-SNAPSHOT
    java -jar $JAR sysinfo
  5. Uninstall to see the command disappear:

    Terminal window
    java -jar $JAR uninstall io.plugwerk.example.cli.hello
    java -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.

  • Read-only server operations (list, search, plus the catalog download that install uses internally) work anonymously against a namespace whose publicCatalog is true. No --api-key needed.
  • Server operations against a private namespace (publicCatalog = false) need a namespace-scoped API key, sent as the X-Api-Key header. The CLI accepts it via the --api-key flag, the PLUGWERK_API_KEY environment variable, or both.
  • uninstall is local-only. It removes the plugin's ZIP and extracted directory from the host's plugins/ 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.

| 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.