UltiKits CLI
@ultikits/cli is the official command-line tool for publishing UltiTools plugin modules to the UltiCloud marketplace. Developers can use it to authenticate, initialize project metadata, upload JAR files, and manage published modules and versions.
Recommended: local publishing
Official UltiTools modules are published locally by their maintainers: the accepted JAR is attached to a GitHub release and that same file is published to UltiCloud with the CLI. Third-party developers can use the same path. GitHub Actions is an optional manual workflow, not the official modules' release path.
Installation
npm install -g @ultikits/cliVerify the installation:
ultikits --versionRequirements
- Node.js 20+
- npm or any Node package manager
Authentication
Before publishing, you need to authenticate with UltiCloud. The CLI supports two methods.
Browser Login (Magic Link)
ultikits loginSelect Browser login (magic link) when prompted. A browser window will open for you to approve the authentication. The CLI polls for completion automatically.
Credentials are saved to ~/.ultikits/credentials.json.
Access Token
If you prefer non-interactive login (e.g., on a remote server), use an access token:
- Go to panel.ultikits.com/developer
- Copy your access token
- Run
ultikits loginand select Access token (from developer portal) - Paste the token when prompted
Environment Variable
For CI/CD pipelines, set the ULTIKITS_TOKEN environment variable instead of running ultikits login:
export ULTIKITS_TOKEN=your_access_tokenOther Auth Commands
ultikits whoami # Check current authentication status
ultikits logout # Remove stored credentialsScaffolding a New Module
The fastest way to start a new UltiTools module is with ultikits create:
ultikits createThe command prompts you for:
| Prompt | Default | Description |
|---|---|---|
| Module name | — | PascalCase name (e.g., UltiHome, MyPlugin) |
| Package name | Derived from name | Java package (e.g., com.ultikits.plugins.home) |
| Description | — | Short description of the module |
| Author | git config user.name | Your name |
| API version | 6.2.2 | UltiTools-API version. This default lags the current release; use the latest version instead. |
CLI 1.2.0 generates this project structure:
UltiHome/
├── pom.xml # Maven build with UltiTools-API, test deps, JaCoCo
├── README.md # Bilingual readme template
├── .gitignore
├── src/main/java/.../UltiHome.java # Main class with @UltiToolsModule
├── src/main/resources/
│ ├── plugin.yml # Plugin descriptor
│ └── lang/
│ ├── en.yml # English language file
│ └── zh.yml # Chinese language file
└── src/test/java/.../ # Test directory (empty, ready for tests)Before compiling against UltiTools-API 6.3.0, migrate the CLI 1.2.0 template:
- Select API version
6.3.0and setapi-version: 630insrc/main/resources/plugin.yml; the template still writes620. - Remove the empty
unregisterSelf()andreloadSelf()overrides from the main class. These methods are final in 6.3.0; put cleanup inprotected void onUnregister()and reload logic inprotected void onReload()if needed.
Then compile the migrated project:
cd UltiHome
mvn compileYou can also specify a target directory:
ultikits create my-plugin-dirAnnotated classes are discovered through scanBasePackages
After scaffolding, add commands in a commands/ package, services in a service/ package, and configs in a config/ package. The framework auto-discovers annotated classes via @UltiToolsModule(scanBasePackages).
Project Setup
Initialize Project Config
Run the following command in your module's root directory:
ultikits initThis creates an ultikits.json file with interactive prompts:
{
"identifyString": "myplugin",
"name": "MyPlugin",
"shortDescription": "A brief description of your plugin.",
"categoryId": 1
}| Field | Description |
|---|---|
identifyString | Unique module identifier. 3-64 characters, lowercase letters, digits, . and _. Must start with a letter. |
name | Display name shown on the marketplace. |
shortDescription | Brief description, max 200 characters. |
categoryId | Module category (see table below). |
Categories
| ID | Category |
|---|---|
| 1 | Utilities |
| 2 | Economy |
| 3 | World Management |
| 4 | Social & Chat |
| 5 | Permissions & Security |
| 6 | Gameplay |
Publishing
Basic Usage
Build and test the module, then keep the JAR you intend to release. Attach that file to your GitHub release and publish the same bytes with the CLI, without rebuilding between the two uploads:
mvn clean verify
ultikits publish target/MyPlugin-1.2.0.jar \
--module-version 1.2.0 \
--changelog "The body of the 1.2.0 section in CHANGELOG.md"If no JAR path is supplied, the CLI searches ./target/ (skipping -sources.jar and -javadoc.jar). With a selected JAR, it automatically:
- Uses that file for the upload
- Reads
plugin.ymlfrom the JAR for name and version - Reads
ultikits.jsonfor identifyString, description, and category - Creates the module on UltiCloud if it doesn't exist
- Creates a new version and uploads the JAR
Publish with Options
Specify the JAR path explicitly and override metadata with flags. Pass the body of the released version’s CHANGELOG.md section as one --changelog argument, rather than the whole file or a generic placeholder. Replace the sample text below with that section:
ultikits publish target/MyPlugin-1.2.0.jar \
--module-version 1.2.0 \
--changelog "The body of the 1.2.0 section in CHANGELOG.md"Available flags:
| Flag | Description |
|---|---|
--id <identifyString> | Override module identifier |
--name <name> | Override display name |
--module-version <version> | Override version string |
--changelog <text> | Version changelog |
--short-description <text> | Override short description |
-y, --yes | Skip all confirmation prompts |
Non-Interactive Mode
For CI/CD, use --yes to skip prompts and ULTIKITS_TOKEN for auth:
ULTIKITS_TOKEN=your_token ultikits publish --yes \
--module-version "1.2.0" \
--changelog "The body of the 1.2.0 section in CHANGELOG.md"Metadata Resolution
The CLI resolves metadata from multiple sources in this priority order:
CLI flags > ultikits.json > plugin.yml from JAR
For example, if you pass --module-version 1.2.0, it overrides the version in plugin.yml.
Failed uploads and retries
A version whose artifact upload never completed is not published: it is not listed publicly or selected as the latest version. After a failed or interrupted upload, run the same ultikits publish command again immediately with the same version; there is no waiting period for a pending upload. A fresh attempt replaces that version's pending reservation and invalidates the old upload token. An active upload of a different version still blocks a new reservation for the module.
A published version cannot be overwritten, either through CLI uploads or the legacy POST /developer/plugin/update Accesskey endpoint. Use a new version for changed bytes. If a network failure leaves the outcome unknown, check the public version-detail API first: a completed publication is not a pending upload and cannot be replaced by retrying.
The module's API level
As of v6.3.0, UltiCloud reads api-version from the root plugin.yml of the JAR you publish and stores it with the version. No flag and no ultikits.json field sets it, and a value sent beside the file is ignored. Declare it as an integer:
api-version: 630The number decides which servers are offered the version. A server on UltiTools-API 6.3.0 or later is offered versions from 630 up to its own API level; a server on an older framework is offered only versions below 630. See Module Versioning.
A JAR whose root plugin.yml does not declare an integer api-version is refused before anything is stored, and the version stays unpublished. See Upload refused for the API level.
Managing Modules
List Your Modules
ultikits modules listOutput:
myplugin — MyPlugin (1.0.0)
anotherplugin — AnotherPlugin (no versions)
2 module(s)Module Details
ultikits modules info mypluginOutput:
Name: MyPlugin
ID: myplugin
Description: A brief description
Downloads: 42
Version: 1.0.0Delete a Module
ultikits modules delete mypluginDeleting a module cannot be undone
This permanently deletes the module and all its versions. This cannot be undone.
Managing Versions
List Versions
ultikits versions list mypluginIn CLI 1.2.0, this command is not reliable for modules built for UltiTools-API 6.3.0. It omits the API-level query, so the server excludes versions with api-version: 630; it also treats the returned version strings as objects and can print undefined. Do not use its output to decide whether an upload completed.
Check the public version-detail API instead. Look up the module by its identifyString, then use the numeric data.id from the response:
curl --fail-with-body 'https://api.ultikits.com/plugin/get?identifyString=myplugin'
# Replace 123 with data.id from the response above.
curl --fail-with-body 'https://api.ultikits.com/plugin/123/versions-detail'The second response's data array contains finalized versions, including their version, changelog, fileSize and downloadLink, without the compatibility filter used by /versions. Check the intended version and notes; download its downloadLink and compare its SHA-256 with your retained JAR to verify the uploaded bytes. Pending uploads are not included.
Delete a Version
CLI 1.2.0 exposes ultikits versions delete, but the server does not implement its DELETE /developer/modules/<id>/versions/<version> route. It returns 404 and does not delete the version. Do not use it to work around the rule that published versions cannot be overwritten; publish a new version for changed bytes.
CI/CD with GitHub Actions
Local publishing is recommended. If your project needs CI publishing, this optional recipe runs only when a maintainer starts it manually. It does not publish on a push, merge or GitHub release event. Create .github/workflows/publish.yml:
name: Publish to UltiCloud
on:
workflow_dispatch:
inputs:
version:
description: Module version, for example 1.2.0
required: true
type: string
permissions:
contents: read
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Extract release notes
env:
MODULE_VERSION: ${{ inputs.version }}
shell: bash
run: |
set -euo pipefail
python3 - <<'PY'
import os
import re
from pathlib import Path
version = os.environ["MODULE_VERSION"]
if not re.fullmatch(r"(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)", version):
raise SystemExit("Expected a release version such as 1.2.0")
lines = Path("CHANGELOG.md").read_text(encoding="utf-8").splitlines()
heading = re.compile(r"^##[ \t]+(?:\[v?([^\]]+)\]|v?([0-9]+\.[0-9]+\.[0-9]+))(?=[ \t]|$)")
sections = []
start = None
fence = None
for index, line in enumerate(lines):
marker = re.match(r"^ {0,3}(`{3,}|~{3,})", line)
if fence:
if re.fullmatch(r" {0,3}" + re.escape(fence[0]) + "{" + str(len(fence)) + r",}[ \t]*", line):
fence = None
continue
if marker:
fence = marker.group(1)
continue
if re.match(r"^##(?:[ \t]|$)", line):
if start is not None:
sections.append(lines[start:index])
start = None
match = heading.match(line)
if match and (match.group(1) or match.group(2)) == version:
start = index + 1
if start is not None:
sections.append(lines[start:])
if len(sections) != 1:
raise SystemExit("Expected exactly one CHANGELOG section for " + version)
body = "\n".join(sections[0]).strip()
if not body:
raise SystemExit("The CHANGELOG section is empty")
Path("release-notes.md").write_text(body + "\n", encoding="utf-8")
PY
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: maven
- name: Set project version and verify
env:
MODULE_VERSION: ${{ inputs.version }}
shell: bash
run: |
set -euo pipefail
[[ "$MODULE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
mvn -B versions:set "-DnewVersion=$MODULE_VERSION" -DgenerateBackupPoms=false
mvn -B clean verify
- name: Retain the built JAR
uses: actions/upload-artifact@v4
with:
name: module-${{ inputs.version }}
path: target/MyPlugin-${{ inputs.version }}.jar
if-no-files-found: error
retention-days: 7
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install a pinned UltiKits CLI
run: npm install -g @ultikits/cli@1.2.0 --ignore-scripts
- name: Publish to UltiCloud
env:
ULTIKITS_TOKEN: ${{ secrets.ULTIKITS_TOKEN }}
MODULE_VERSION: ${{ inputs.version }}
shell: bash
run: |
set -euo pipefail
ultikits publish "target/MyPlugin-${MODULE_VERSION}.jar" --yes \
--module-version "$MODULE_VERSION" \
--changelog "$(cat release-notes.md)"The extractor accepts a single ## [1.2.0], ## [v1.2.0] or ## 1.2.0 heading, optionally followed by a date. It keeps subheadings and fenced code through the next level-two heading or end of file, and refuses an invalid version, missing or duplicate section, or empty body before building or publishing. Review the selected ref: its changelog must describe the bytes you intend to build.
Setup Steps
- Add the workflow file and change
MyPluginin both exact JAR paths to your build's final artifact name. Do not use a wildcard that could upload a sources or unshaded JAR. - Set the repository's
ULTIKITS_TOKENsecret with your developer portal access token; never put it in the workflow text or a dispatch input. - Keep the CLI version pinned. Review a version change before updating the pin.
Publishing on push
Do not add a push or merge trigger: merging a version bump would then release it automatically. Interpolating workflow inputs directly into run: can also create shell injection; pass them through environment variables and quote their expansions as above.
Publishing a New Version
- Review the source and the version's
CHANGELOG.mdsection. - Open Actions > Publish to UltiCloud > Run workflow and select the intended branch or tag.
- Enter only the module version. The workflow reads its complete section body from the checked-out
CHANGELOG.md; no multiline form input is used. - Start the workflow deliberately, then check the uploaded version and file. This optional recipe builds once and publishes to UltiCloud; it does not create a GitHub release. Download the
module-<version>artifact from the workflow run, extract its JAR, and attach that file to GitHub if needed, without rebuilding.
Troubleshooting
"Not logged in" Error
Not logged in. Run `ultikits login` or set ULTIKITS_TOKEN.Either run ultikits login to authenticate interactively, or set the ULTIKITS_TOKEN environment variable.
"No JAR file specified and none found in ./target/"
The CLI looks for a .jar file in ./target/ by default. Make sure you've built the project first:
mvn clean package -DskipTestsMissing Required Fields
Missing required fields:
- identifyString (use --id or ultikits.json)Create an ultikits.json with ultikits init, or pass the missing fields as CLI flags.
GitHub Actions: 403 on Publish
Verify that the ULTIKITS_TOKEN secret is set correctly on your repository:
gh secret list --repo YourOrg/YourPluginMake sure the token matches the access token from the developer portal.
Upload refused for the API level
As of v6.3.0, ultikits publish prints UltiCloud's reason when the JAR's root plugin.yml has no usable api-version, for example:
Upload failed: Cannot read the module's UltiTools API level: plugin.yml has no top-level api-version. The root plugin.yml must declare the UltiTools API level the module is built for as an integer, for example "api-version: 630". Nothing was published, and the upload reservation for this version was released: correct the JAR, then publish the version again (run ultikits publish again, or create the version again in the panel).Other reasons include a Bukkit-style value such as '1.19' that is not an integer, an empty value, a value declared twice, a JAR without a root plugin.yml, and an archive UltiCloud cannot read, such as a truncated JAR; the message always names the reason. Declare the level as shown in The module's API level, rebuild and publish again. The refusal releases the upload reservation for that version, so after you fix plugin.yml and rebuild, you can run ultikits publish again straight away.
Ling Bao