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.
- 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, andpackage. - Support portable script definitions through variable substitution.
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.
- Java 21
- Maven 3.9 or newer
Install the latest stable release for the current user with:
curl -fsSL https://raw.githubusercontent.com/tfredrich/jam4j/main/install.sh | shThe 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.
After installing, upgrade to the latest stable release with:
jam upgradeIf 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.
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"
}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 smokeNOTE: 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 upgradeConvenience 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 packageThe 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.
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)
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 packageThe 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 firstSubcommands 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.
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>: bootstrapproject.jsonfrom a Mavenpom.xml--force: overwriteproject.jsonif 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 --forceSearches 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, default20--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 databindResolves 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.0Resolves 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: includedevDependenciesin 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-classpathExecutes 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 inproject.json-a, --arg <arg>: pass an argument to the preceding script. This is parsed byjam runrather 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 testWhen 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.
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 21Use -- before script arguments that begin with - so picocli does not parse them as jam build options.
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 smokeUse -- before script arguments that begin with - so picocli does not parse them as jam test options.
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 cleanUse -- before script arguments that begin with - so picocli does not parse them as jam clean options.
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-Classin the manifest when"main"is configured inproject.json - Project classes from
--classesalways 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.jarUse -- before script arguments that begin with - so picocli does not parse them as jam package options (only relevant when a package script is defined).
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 upgradeOptions:
--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 --forceWithout --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.
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-Classin 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 packagedoes 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/LICENSEappending, Plexus component descriptors, and more;jam packageonly handlesMETA-INF/services/. NOTICE/LICENSEfiles 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 packagealways includes all production dependencies. module-info.classis 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.
mvn test
mvn package
java -jar target/jam4j-1.0.0-SNAPSHOT.jar --help
java -jar target/jam4j-1.0.0-SNAPSHOT.jar --versionmvn 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).
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:
-
Update the Maven version in
pom.xmlfrom a snapshot to the release version, for example1.0.0-SNAPSHOTto1.0.0. -
Verify the release build locally:
mvn clean package java -jar target/jam4j-1.0.0.jar --version
-
Commit the release version:
git add pom.xml git commit -m "release: 1.0.0" -
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>.jarjam4j-<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 mainsrc/main/java/com/strategicgains/jam4j/Jam.java: CLI entry pointsrc/main/java/com/strategicgains/jam4j/command: picocli commandssrc/main/java/com/strategicgains/jam4j/resolver: Maven artifact resolution and searchsrc/main/java/com/strategicgains/jam4j/model:project.jsonmodelsrc/main/java/com/strategicgains/jam4j/script: script execution and substitutionssrc/test/java: JUnit 5 and AssertJ tests
See LICENSE.