This tutorial walks you through building a command-line application using picocli and distributing it with jDeploy. You’ll learn about jDeploy 6.0’s new CLI command support and discover how you can offer your users two installation paths — npm/npx and native installers — from a single project.

Introduction

What We’re Building

We’ll create a simple command-line tool using picocli, a powerful library for building CLI applications in Java. The tool will be fully distributable to end users on Windows, macOS, and Linux.

The Dual Distribution Model

Historically, jDeploy has always supported distributing Java applications via npm. Users could install your CLI tool with:

npm install -g your-cli
your-cli --help

Or run it without installation:

npx your-cli --help

This works well for developers who already have Node.js installed. But what about non-developer users?

jDeploy 6.0 introduces native CLI command support through the desktop installer. Now, when users install your application using the jDeploy installer, CLI commands are automatically added to their system PATH. No Node.js required — just a native executable that works like any other command-line tool.

The key insight: you don’t have to choose between these approaches. The same package.json configuration supports both. Publish to npm, and your users can install via npm/npx or download the native installer. Same app, same version, two installation paths.

dual distribution model
Figure 1. jDeploy’s dual distribution model: one package, two installation paths

Prerequisites

Before starting this tutorial, ensure you have:

  • Java 21 or higher installed

  • Node.js and npm (for the jDeploy CLI and publishing)

  • jDeploy Desktop App or jDeploy CLI (npm install -g jdeploy)

To verify your Java installation:

java -version

To install the jDeploy CLI (if not using the desktop app):

npm install -g jdeploy

Creating the Project

You can create a new picocli project using either the jDeploy Desktop App or the command line. Both methods generate identical project structures.

Option A: Using jDeploy Desktop App

Step 1: Open jDeploy and Create New Project

Launch the jDeploy Desktop App. From the main menu, click "Create new project…​".

01 jdeploy main menu
Figure 2. The jDeploy main menu with the "Create new project…​" option

Step 2: Select the Picocli Template

In the template selection dialog, find and select "Picocli CLI Maven Starter". This template is pre-configured with picocli, Maven, and jDeploy settings optimized for CLI applications.

02 template selection
Figure 3. Selecting the Picocli CLI Maven Starter template

Click Next to continue.

Step 3: Configure Project Settings

Fill in your project details:

  • Application Display Name: The human-readable name (e.g., "My CLI Tool")

  • Group ID: Your Maven group ID (e.g., "com.example")

  • Artifact ID: Your Maven artifact ID and command name (e.g., "my-cli")

  • Project Location: Where to create the project on disk

  • Publish to: Choose GitHub or npm for your distribution target

03 project configuration
Figure 4. Configuring the new project settings

Click Finish to generate the project.

Option B: Using jDeploy CLI

Open a terminal and run:

jdeploy generate -t picocli

Follow the interactive prompts to configure your project name, group ID, and other settings.

Example CLI session
$ jdeploy generate -t picocli
Project generated at: ./my-app

The generated project structure:

my-app/
├── .github/
│   └── workflows/
│       └── jdeploy.yml        # GitHub Actions workflow
├── .mvn/
│   └── wrapper/               # Maven wrapper files
├── src/
│   └── main/
│       └── java/
│           └── com/example/myapp/
│               └── Main.java  # Your CLI application
├── icon.png                   # Application icon
├── tile.png                   # Tile image for jDeploy
├── mvnw                       # Maven wrapper (Unix)
├── mvnw.cmd                   # Maven wrapper (Windows)
├── package.json               # jDeploy configuration
├── pom.xml                    # Maven build configuration
└── README.md

Understanding the Generated Project

Let’s examine the key files that make this CLI application work.

The Main Application (Main.java)

The generated Main.java demonstrates picocli’s annotation-based approach to building CLIs:

package com.example.myapp;

import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;

import java.util.concurrent.Callable;

@Command(name = "my-app",
        mixinStandardHelpOptions = true,
        version = "1.0",
        description = "My App - a picocli CLI application.")
public class Main implements Callable<Integer> {

    @Parameters(index = "0", defaultValue = "World",
            description = "The name to greet (default: ${DEFAULT-VALUE}).")
    private String name;

    @Option(names = {"-c", "--count"}, defaultValue = "1",
            description = "Number of times to print the greeting (default: ${DEFAULT-VALUE}).")
    private int count;

    @Override
    public Integer call() {
        for (int i = 0; i < count; i++) {
            System.out.println("Hello, " + name + "!");
        }
        return 0;
    }

    public static void main(String[] args) {
        String mode = System.getProperty("jdeploy.mode", "command");

        if ("gui".equals(mode)) {
            // GUI mode: show an about dialog
            javax.swing.SwingUtilities.invokeLater(() -> {
                javax.swing.JOptionPane.showMessageDialog(
                    null,
                    "My App v1.0\n\nThis is a command-line tool.\n"
                        + "Run 'my-app --help' in a terminal for usage.",
                    "About My App",
                    javax.swing.JOptionPane.INFORMATION_MESSAGE
                );
                System.exit(0);
            });
            return;
        }

        // CLI mode: run the command
        int exitCode = new CommandLine(new Main()).execute(args);
        System.exit(exitCode);
    }
}

Key points:

  • @Command defines the command name, version, and description

  • @Parameters captures positional arguments

  • @Option defines named options like -c or --count

  • mixinStandardHelpOptions = true automatically adds --help and --version

  • jdeploy.mode detection allows the same JAR to behave differently when launched as a GUI app vs CLI command

The jDeploy Configuration (package.json)

The package.json file configures both npm distribution and the native installer:

{
  "name": "my-app",
  "version": "1.0-SNAPSHOT",
  "description": "jDeploy Picocli CLI Starter Project",
  "bin": {
    "my-app": "jdeploy-bundle/jdeploy.js"
  },
  "jdeploy": {
    "jdk": false,
    "javaVersion": "21",
    "jar": "target/my-app-1.0-SNAPSHOT.jar",
    "javafx": false,
    "title": "My App",
    "buildCommand": [
      "./mvnw",
      "package"
    ],
    "commands": {
      "my-app": {
        "description": "My App CLI"
      }
    }
  },
  "dependencies": { ... },
  "files": ["jdeploy-bundle"]
}

Two sections control CLI distribution:

  1. bin: The npm entry point. This enables npm install -g my-app and npx my-app.

  2. jdeploy.commands: The native installer configuration (new in jDeploy 6.0). This tells the installer to create a my-app command in the user’s PATH.

Both point to the same underlying Java application, but they use different launchers:

  • npm uses a Node.js wrapper script

  • The native installer uses a compiled native launcher

The Maven Configuration (pom.xml)

The pom.xml includes:

  • picocli dependency for CLI annotations and parsing

  • maven-jar-plugin configured with classpath and main class

  • maven-dependency-plugin to copy dependencies to target/libs/

  • jdeploy-maven-plugin to sync version numbers with package.json

<dependencies>
    <dependency>
        <groupId>info.picocli</groupId>
        <artifactId>picocli</artifactId>
        <version>4.7.6</version>
    </dependency>
</dependencies>

Building and Testing Locally

Build the Project

First, make the Maven wrapper executable (Unix/macOS):

chmod +x mvnw

Then build:

./mvnw package

On Windows, use mvnw.cmd package instead.

Test the JAR Directly

Run the built JAR to verify it works:

$ java -jar target/my-app-1.0-SNAPSHOT.jar
Hello, World!

$ java -jar target/my-app-1.0-SNAPSHOT.jar Alice
Hello, Alice!

$ java -jar target/my-app-1.0-SNAPSHOT.jar Alice -c 3
Hello, Alice!
Hello, Alice!
Hello, Alice!

$ java -jar target/my-app-1.0-SNAPSHOT.jar --help
Usage: my-app [-hV] [-c=<count>] [<name>]
My App - a picocli CLI application.
      [<name>]      The name to greet (default: World).
  -c, --count=<count>
                    Number of times to print the greeting (default: 1).
  -h, --help        Show this help message and exit.
  -V, --version     Print version information and exit.

Two Distribution Paths, One Package

This is where jDeploy 6.0 shines. Your single package.json enables two completely different installation experiences for your users.

Path 1: npm/npx (For Developers)

This approach has been available since jDeploy 1.0 and works great for users who already have Node.js installed.

How It Works

When you publish to npm, jDeploy creates a package containing:

  • A small Node.js wrapper script (jdeploy.js)

  • Your application JAR and dependencies

  • Metadata for downloading the appropriate JRE if needed

When users run your command, the Node.js wrapper:

  1. Checks for/downloads a suitable JRE

  2. Launches your JAR with the correct arguments

  3. Forwards stdin/stdout/stderr and exit codes

User Experience

# Install globally
npm install -g my-app

# Run the command
my-app --help
my-app Alice -c 3

# Or run without installing
npx my-app --help

Pros

  • Familiar workflow for developers

  • No separate installer download

  • Easy to switch between versions (npm install -g my-app@2.0)

  • Works anywhere Node.js is installed

Cons

  • Requires Node.js on the user’s machine

  • First run may be slow (JRE download)

  • Less "native" feel

Path 2: Native Installer (For Everyone)

New in jDeploy 6.0, the native installer approach provides a traditional desktop application installation experience.

How It Works

The jDeploy installer:

  1. Installs a native launcher executable to the system

  2. Bundles or downloads the appropriate JRE

  3. Registers CLI commands in the system PATH

  4. Creates Start Menu/Applications entries

The native launcher is a compiled executable (not a script) that:

  • Starts instantly (no interpreter overhead)

  • Properly handles stdin/stdout/stderr

  • Forwards exit codes correctly

  • Integrates with the OS signal handling

User Experience

Users download the installer for their platform from your GitHub releases page or website:

After installation:

# The command is immediately available in any new terminal
my-app --help
my-app Alice -c 3

Pros

  • No Node.js required

  • Native performance and behavior

  • Professional installation experience

  • Works for non-developer users

  • Automatic updates (if configured)

Cons

  • Users must download and run an installer

  • Larger initial download (includes JRE)

Side-by-Side Comparison

Aspect npm/npx Native Installer

Requires Node.js

Yes

No

Requires installer download

No

Yes

First-run speed

Slower (JRE download)

Fast (JRE bundled)

Updates

npm update -g

Auto-update or re-download

Target audience

Developers

Everyone

Command availability

After npm install -g

After installer runs

installation flow comparison
Figure 5. Installation flow comparison: npm/npx vs native installer

Why You Don’t Have to Choose

Here’s the key insight: the same package.json supports both distribution methods.

When you publish your package to npm:

  1. The npm package is available for npm install -g and npx

  2. jDeploy can generate native installers from the same package

  3. GitHub Actions can automatically build installers on every release

Your users get to choose their preferred installation method:

  • Developer installing a build tool?npm install -g my-app

  • Quick one-time use?npx my-app

  • Non-technical user? → Download the installer

  • Corporate environment without Node.js? → Download the installer

The Configuration That Enables Both

Look at your package.json again:

{
  "bin": {
    "my-app": "jdeploy-bundle/jdeploy.js"    (1)
  },
  "jdeploy": {
    "commands": {
      "my-app": {                              (2)
        "description": "My App CLI"
      }
    }
  }
}
1 The bin entry enables npm/npx distribution
2 The commands entry enables native installer CLI support

Both entries use the same command name (my-app) and point to the same underlying Java application. The only difference is the launcher mechanism.

package json convergence
Figure 6. One package.json enables both distribution methods

Publishing Your CLI

Once you’re happy with your CLI application, it’s time to publish it.

Option A: Using jDeploy Desktop App

  1. Open your project in jDeploy Desktop App

  2. Click the Publish button

  3. jDeploy will:

    • Build your application (runs ./mvnw package)

    • Create the jdeploy-bundle

    • Publish to npm and/or create GitHub release

Option B: Using jDeploy CLI

# Build and publish
jdeploy publish

For npm publishing, ensure you’re logged in:

npm login
jdeploy publish

Option C: Automated Publishing with GitHub Actions

The generated project includes a GitHub Actions workflow (.github/workflows/jdeploy.yml) that automatically:

  • Builds your application on push to snapshot branches or version tags

  • Creates native installers for all platforms

  • Uploads installers as GitHub release assets

To trigger a release:

# Create and push a version tag
git tag v1.0.0
git push origin v1.0.0

The workflow will create a GitHub release with downloadable installers.

Adding Self-Update Support

jDeploy 6.0 allows CLI commands to update themselves. Users can run my-app update to download and install the latest version.

To enable this, add the updater implementation to your command:

{
  "jdeploy": {
    "commands": {
      "my-app": {
        "description": "My App CLI",
        "implements": ["updater"]
      }
    }
  }
}

Now users can update with:

my-app update

The update is handled entirely by the native launcher — your Java code isn’t involved. The launcher checks for new versions, downloads updates, and installs them.

Self-update only works with the native installer distribution, not npm. npm users update with npm update -g my-app.

Customizing Your CLI

Now that you understand the project structure, here are some ways to extend your CLI.

Adding Subcommands

Picocli makes it easy to create complex CLIs with subcommands:

@Command(name = "my-app",
        subcommands = {
            InitCommand.class,
            BuildCommand.class,
            DeployCommand.class
        })
public class Main implements Callable<Integer> {
    // ...
}

@Command(name = "init", description = "Initialize a new project")
class InitCommand implements Callable<Integer> {
    @Override
    public Integer call() {
        System.out.println("Initializing...");
        return 0;
    }
}

Usage: my-app init, my-app build, my-app deploy

Adding Multiple CLI Commands

You can expose multiple commands from the same JAR by adding more entries to the commands section:

{
  "jdeploy": {
    "commands": {
      "my-app": {
        "description": "Main CLI interface"
      },
      "my-app-server": {
        "description": "Start the server",
        "args": ["--server-mode"]
      }
    }
  }
}

Your Java code can check for the --server-mode argument to determine which mode to run in.

Making It a Service

If your CLI can run as a background service (like a server), you can add service controller support:

{
  "jdeploy": {
    "commands": {
      "my-app-server": {
        "description": "My App Server",
        "implements": ["service_controller", "updater"]
      }
    }
  }
}

This enables:

my-app-server service install
my-app-server service start
my-app-server service status
my-app-server service stop
my-app-server service uninstall

See the jDeploy 6.0 Release Notes for full details on service controllers.

Conclusion

You’ve learned how to:

  1. Create a picocli CLI project using jDeploy’s template

  2. Build and test your CLI locally

  3. Understand the dual distribution model (npm vs native installer)

  4. Publish your CLI for both distribution paths

  5. Extend your CLI with self-updates, subcommands, and services

The key takeaway: jDeploy 6.0 lets you reach both developer and non-developer users with a single project configuration. Publish to npm, and your users can choose the installation method that works best for them.

Resources

Example Projects