Skip to content
tfredrichPublic

About

Java Artifact Manager (jam): npm-like dependency management for Java.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

jam4j

jam4j is an experimental Java Artifact Manager: a small CLI that brings an npm-like workflow to Java projects. It uses a project.json manifest to declare dependencies and scripts, resolves artifacts from Maven repositories with Apache Maven Resolver, and installs resolved JARs into a local lib/ directory.

Goals

  • Manage Java dependencies from a JSON manifest instead of a verbose Maven XML or Gradle build file.
  • Resolve Maven Central artifacts and transitive compile-scope dependencies to build an executable (aka fat) JAR.
  • Provide familiar NPM-like commands such as install, run, build, test, clean, and package.
  • Support portable script definitions through variable substitution.

Project Status

This repository is an early implementation. The CLI, manifest model, Maven Resolver integration, local installation, script execution, and basic tests are present. Broader project-generation workflows remain design goals, not complete product features.

Requirements

  • Java 21
  • Maven 3.9 or newer

Install Jam4J

Install the latest stable release for the current user with:

curl -fsSL https://raw.githubusercontent.com/tfredrich/jam4j/main/install.sh | sh

The installer verifies the release checksum and creates the same layout as install_local.sh under $HOME/.jam4j:

$HOME/.jam4j/bin/jam4j.jar
$HOME/.jam4j/bin/jam

Add the launcher to your PATH:

export PATH="$HOME/.jam4j/bin:$PATH"

Set JAM_HOME to install somewhere else in your user-owned filesystem.

Upgrade

After installing, upgrade to the latest stable release with:

jam upgrade

If the installed JAR is a local development snapshot, or its version is greater than the latest published release, force replacement with the latest stable GitHub release:

jam upgrade --force

--force bypasses the version comparison, but the release must still be a stable GitHub release and its SHA-256 checksum must verify before the installed JAR is replaced.

project.json

A project manifest can declare dependencies, dev dependencies, a main class, and named scripts:

{
  "name": "my-app",
  "version": "1.0.0",
  "main": "com.example.Main",
  "dependencies": {
    "com.fasterxml.jackson.core:jackson-databind": "2.17.1"
  },
  "devDependencies": {
    "org.junit.jupiter:junit-jupiter": "5.10.2"
  },
  "scripts": {
    "build": "javac -cp {{deps}} -d {{classes}} {{sources}}",
    "run": "java -cp {{deps}}{:}{{classes}} {{main}}",
    "test": "javac -cp {{deps:dev}} -d {{classes}} {{sources}} && javac -cp {{deps:dev}}{:}{{classes}} -d {{classes:test}} {{sources:test}} && java -cp {{deps:dev}}{:}{{classes}}{:}{{classes:test}} org.junit.platform.console.ConsoleLauncher --scan-classpath --disable-banner",
    "clean": "rm -rf {{output}}"
  }
}

Default scripts are provided for build, run, package, and clean if none are defined in project.json. Providing a definition for a script name overrides the default. For example, if build is defined in project.json, jam build runs that script instead of the default.

Dependency keys use group:artifact; values are versions. At runtime they are resolved as Maven coordinates such as group:artifact:version.

The optional sourceDir, testDir, and outputDir fields override the default directories. Omit them to use the standard Maven layout:

{
  "sourceDir": "src",   // default: "src/main/java"
  "testDir": "test",    // default: "src/test/java"
  "outputDir": "build"  // default: "target"
}

Script Variables

Scripts support cross-platform substitutions:

Token Expands to
{{deps}} Resolved classpath of production dependencies only
{{deps:dev}} Resolved classpath of production + dev dependencies
{{sources}} Space-separated *.java files under sourceDir (default src/main/java)
{{sources:test}} Space-separated *.java files under testDir (default src/test/java)
{{classNames}} Space-separated fully-qualified class names derived from sourceDir
{{classNames:test}} Space-separated fully-qualified class names derived from testDir
{{classes}} Compiled classes output directory (outputDir/classes, default target/classes)
{{classes:test}} Compiled test classes output directory (outputDir/test-classes, default target/test-classes)
{{output}} Project output directory (outputDir, default target)
{{main}} Main class from the "main" field in project.json
{{jarflags}} --main-class <mainClass> when "main" is set, otherwise empty
{/} Platform file separator
{:} Platform path separator
{~} User home directory
{./path} Project-relative path using platform separators

Scripts are executed through the system shell (sh -c on Unix/macOS, cmd /c on Windows), so standard shell operators such as &&, ||, ;, and pipes work exactly as they would on the command line.

Arguments can be passed to scripts with -a or --arg:

java -jar target/jam4j-1.0.0-SNAPSHOT.jar run build -a --verbose test -a smoke

CLI Usage

NOTE: Replace java -jar target/jam4j-1.0.0-SNAPSHOT.jar below with jam if you have installed the JAR launcher.

After building:

java -jar target/jam4j-1.0.0-SNAPSHOT.jar search jackson
java -jar target/jam4j-1.0.0-SNAPSHOT.jar install com.fasterxml.jackson.core:jackson-databind:2.17.1
java -jar target/jam4j-1.0.0-SNAPSHOT.jar path
java -jar target/jam4j-1.0.0-SNAPSHOT.jar run --list
java -jar target/jam4j-1.0.0-SNAPSHOT.jar run build
java -jar target/jam4j-1.0.0-SNAPSHOT.jar upgrade

Convenience commands execute matching scripts from project.json:

java -jar target/jam4j-1.0.0-SNAPSHOT.jar build
java -jar target/jam4j-1.0.0-SNAPSHOT.jar test
java -jar target/jam4j-1.0.0-SNAPSHOT.jar clean
java -jar target/jam4j-1.0.0-SNAPSHOT.jar package

The examples above use the shaded JAR directly. If you install or alias the JAR launcher as jam, replace java -jar target/jam4j-1.0.0-SNAPSHOT.jar with jam.

Command Quick-Reference

jam
├── Global options
│   ├── -h, --help
│   ├── -V, --version
│   ├── -p, --project <file>
│   ├── --config <file>
│   ├── -c, --cache <dir>
│   ├── -r, --repo <url|name=url>                          (repeatable)
│   ├── --ignore-pom-repos
│   ├── -q, --quiet
│   └── -v, --verbose
│
├── init [options] [target-directory]                      (alias: n)
│   ├── -f, --from <file>
│   ├── --force
│   ├── --name <name>
│   ├── --version <version>
│   └── --main <class>
│
├── search [options] <query...>                            (alias: s)
│   ├── -i, --interactive
│   ├── -m, --max <count>
│   ├── --snapshots
│   ├── --local
│   └── --no-central
│
├── install [options] [artifact...]                        (alias: i)
│   ├── -d, --directory <dir>
│   └── -L, --local-only
│
├── path [options] [artifact...]                           (alias: p)
│   └── --dev
│
├── run [options] <script> [args...]                       (alias: r)
│   ├── -l, --list
│   └── -a, --arg <arg>
│
├── build [args...]
│
├── test [args...]
│
├── clean [args...]
│
├── package [args...]
│   ├── -o, --output <file>
│   └── --classes <dir>
│
└── upgrade [--force]                                      (stand-alone)

Global options

The following options apply to the overall jam invocation and must appear before the first command:

  • -p, --project <file>: project file, default ./project.json
  • --config <file>: user configuration file
  • -c, --cache <dir>: downloaded-artifact cache directory
  • -r, --repo <url|name=url>: additional repository; repeatable
  • --ignore-pom-repos: ignore repositories declared in dependency POMs
  • -q, --quiet: suppress non-essential output
  • -v, --verbose: enable verbose diagnostic output

For example:

jam --quiet clean build package
jam --project app.json --repo corp=https://repo.example install clean build package

Command chaining

The parameterless commands clean, build, test, and package can be chained and execute from left to right. Execution stops when a command fails. upgrade is standalone because it replaces the underlying jam installation.

jam clean build test package  # valid
jam --quiet upgrade           # valid
jam upgrade clean             # invalid
jam clean upgrade             # invalid
jam upgrade --quiet           # invalid: global options must come first

Detailed Command Reference

Subcommands print command-specific usage when invocation fails validation, such as an unknown option or missing required argument. Global options and command chaining are documented at the top of this section.

jam init [options] [target-directory]

Creates a scaffold project.json. If no target directory is supplied, jam writes to the current directory. The command prompts for missing project metadata and writes default build, test, run, and clean scripts.

Pass --from <pom.xml> to bootstrap from an existing Maven project. jam reads all <dependency> entries from the POM and writes them into a new project.json. Compile- and runtime-scoped dependencies are recorded under dependencies; test-, provided-, and system-scoped dependencies are recorded under devDependencies. name and version are populated from the POM's artifactId and version unless overridden with --name or --version.

Aliases: n

Options:

  • -f, --from <file>: bootstrap project.json from a Maven pom.xml
  • --force: overwrite project.json if it already exists
  • --name <name>: project name
  • --version <version>: project version
  • --main <class>: main class

Examples:

jam init
jam init --name my-app --version 0.1.0 --main com.example.Main ./my-app
jam init --force
jam init --from pom.xml
jam init --from pom.xml --force

jam search [options] <query...>

Searches for Maven artifacts. By default, search includes Maven Central and returns up to 20 results. Multi-word queries are accepted.

Aliases: s

Options:

  • -i, --interactive: prompt for result numbers to install after searching
  • -m, --max <count>: maximum number of results to return, default 20
  • --snapshots: include snapshot versions
  • --local: include artifacts from the local Maven cache
  • --no-central: skip Maven Central and search only the local cache or configured repositories

Examples:

jam search jackson
jam search -m 5 "junit jupiter"
jam search --local --no-central assertj
jam search --interactive jackson databind

jam install [options] [artifact...]

Resolves and installs artifacts into the local library directory. Artifacts must use Maven coordinates in group:artifact:version form. If artifacts are supplied and the project file exists, jam also records them in dependencies in project.json.

If no artifacts are supplied, jam install reads all dependencies and devDependencies from project.json and installs them.

Aliases: i

Options:

  • -d, --directory <dir>: directory to install artifacts into
  • -L, --local-only: copy artifacts instead of creating symlinks

Examples:

jam install
jam install com.fasterxml.jackson.core:jackson-databind:2.17.1
jam install -d vendor/lib org.assertj:assertj-core:3.25.3
jam --ignore-pom-repos install com.hazelcast:hazelcast:5.5.0

jam path [options] [artifact...]

Resolves artifacts and prints a platform-specific Java classpath. If artifacts are supplied, those coordinates are resolved. If no artifacts are supplied, jam path reads dependencies from project.json (production only by default).

Aliases: p

Options:

  • --dev: include devDependencies in the classpath (production + dev)

Examples:

jam path
jam path --dev
jam path org.junit.jupiter:junit-jupiter:5.10.2
java -cp "$(jam path):target/classes" com.example.Main
java -cp "$(jam path --dev):target/classes:target/test-classes" org.junit.platform.console.ConsoleLauncher --scan-classpath

jam run [options] <script> [args...]

Executes one or more named scripts from project.json. Before running scripts, jam resolves project dependencies and makes them available via script variables: {{deps}} for production dependencies only, and {{deps:dev}} for production + dev dependencies. If no run script is defined but a "main" class is set, jam run synthesizes a default: java -cp {{deps}}{:}{{classes}} {{main}}.

Aliases: r

Options:

  • -l, --list: list scripts defined in project.json
  • -a, --arg <arg>: pass an argument to the preceding script. This is parsed by jam run rather than declared as a picocli option, so it appears after a script name.

Examples:

jam run --list
jam run build
jam run build -a --release -a 21
jam run clean build test

When several script names are supplied, jam runs them in order. Arguments following a script are passed to that script until another known script name is encountered.

jam build [args...]

Runs the build script from project.json. This is a convenience command equivalent to jam run build, with any supplied arguments forwarded to the script.

Options:

Example:

jam build -- --release 21

Use -- before script arguments that begin with - so picocli does not parse them as jam build options.

jam test [args...]

Runs the test script from project.json. This is a convenience command equivalent to jam run test, with any supplied arguments forwarded to the script.

Options:

Example:

jam test smoke

Use -- before script arguments that begin with - so picocli does not parse them as jam test options.

jam clean [args...]

Runs the clean script from project.json. This is a convenience command equivalent to jam run clean, with any supplied arguments forwarded to the script.

Options:

Example:

jam clean

Use -- before script arguments that begin with - so picocli does not parse them as jam clean options.

jam package [options] [--] [args...]

Builds a fat (uber) JAR containing the project's compiled classes and all production dependencies merged into a single archive. If a package script is defined in project.json, that script runs instead and the native build is skipped.

The native build:

  • Merges all production dependency JARs into the output archive
  • Concatenates META-INF/services/* entries so SPI registrations from all JARs survive
  • Strips JAR signature files so the repacked archive remains valid
  • Sets Main-Class in the manifest when "main" is configured in project.json
  • Project classes from --classes always override dependency entries with the same path

Output JAR defaults to target/<name>-<version>.jar (using name and version from project.json).

Options:

  • -o, --output <file>: output JAR path (default: target/<name>-<version>.jar)
  • --classes <dir>: compiled classes directory (default: <outputDir>/classes)

Examples:

jam package
jam package --output target/myapp.jar
jam package --classes target/classes --output dist/myapp-1.0.jar

Use -- before script arguments that begin with - so picocli does not parse them as jam package options (only relevant when a package script is defined).

jam upgrade

Checks GitHub for the latest stable jam4j release, verifies its SHA-256 checksum, and updates the installed JAR in $JAM_HOME/bin (default: $HOME/.jam4j/bin).

The command requires a previously installed local installation created by install_local.sh or an equivalent layout containing $HOME/bin/jam4j.jar.

The process inspects the manifest of the existing jam4j.jar to determine the current version.

Example:

jam upgrade

Options:

  • --force: download and install the latest stable GitHub release even when the installed version compares as equal to or newer than that release. This is intended for replacing local snapshot builds with a published release.

Example:

jam upgrade --force

Without --force, an installed version that is equal to or newer than the latest release is left unchanged. With --force, the latest release asset is downloaded and checksum-verified before it atomically replaces the installed JAR.

Comparison with Maven Shade Plugin

The same:

  • Strips JAR signature files (*.SF, *.RSA, *.DSA, *.EC) so the repacked archive stays valid
  • Merges META-INF/services/* by concatenation, preserving all SPI registrations across all JARs
  • Sets Main-Class in the manifest
  • First-in-wins for duplicate class/resource entries — the same as Shade's default behavior without custom transformers

Different:

  • No package relocation — Shade can rewrite bytecode (via ASM) to rename packages and eliminate version conflicts at runtime; jam package does not. If two dependencies require incompatible versions of the same library, one version is silently dropped.
  • No pluggable resource transformers — Shade ships transformers for XML merging, NOTICE/LICENSE appending, Plexus component descriptors, and more; jam package only handles META-INF/services/.
  • NOTICE/LICENSE files are not merged — first-in-wins, so license attributions from some dependencies may be silently dropped from the output archive.
  • No artifact or class filtering — Shade can exclude specific JARs or class patterns from the output; jam package always includes all production dependencies.
  • module-info.class is not handled specially — the entry from whichever JAR is processed first is used as-is; Shade has dedicated handling for multi-release JARs and JPMS modules.

If any of these limitations matter for your project, define a package script in project.json and invoke the Maven Shade Plugin (or another tool) directly.

Build and Test

mvn test
mvn package
java -jar target/jam4j-1.0.0-SNAPSHOT.jar --help
java -jar target/jam4j-1.0.0-SNAPSHOT.jar --version

mvn package builds a shaded standalone JAR with com.strategicgains.jam4j.Jam as the entry point. The packaged CLI reports the Maven project version and build timestamp, for example jam 1.0.0-SNAPSHOT (build 2026-05-04T21:18:42Z).

Release Flow

CI runs on branch pushes and pull requests. It builds the shaded JAR and smoke-tests the CLI.

Versioned releases are created from Git tags. To publish a release:

  1. Update the Maven version in pom.xml from a snapshot to the release version, for example 1.0.0-SNAPSHOT to 1.0.0.

  2. Verify the release build locally:

    mvn clean package
    java -jar target/jam4j-1.0.0.jar --version
  3. Commit the release version:

    git add pom.xml
    git commit -m "release: 1.0.0"
  4. Create and push a matching tag:

    git tag v1.0.0
    git push origin main
    git push origin v1.0.0

    You can also push the branch and all local tags together:

    git push origin main --tags

The release workflow runs only for tags matching v*. It verifies that the tag version matches pom.xml, rejects *-SNAPSHOT versions, builds the package, and publishes a GitHub Release with:

  • jam4j-<version>.jar
  • jam4j-<version>.jar.sha256

After publishing, bump pom.xml to the next development snapshot and commit it:

git add pom.xml
git commit -m "chore: start 1.0.1-SNAPSHOT"
git push origin main

Source Layout

  • src/main/java/com/strategicgains/jam4j/Jam.java: CLI entry point
  • src/main/java/com/strategicgains/jam4j/command: picocli commands
  • src/main/java/com/strategicgains/jam4j/resolver: Maven artifact resolution and search
  • src/main/java/com/strategicgains/jam4j/model: project.json model
  • src/main/java/com/strategicgains/jam4j/script: script execution and substitutions
  • src/test/java: JUnit 5 and AssertJ tests

License

See LICENSE.

About

Java Artifact Manager (jam): npm-like dependency management for Java.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages