UltiKits CLI
@ultikits/cli 是官方命令行工具,用于将 UltiTools 插件模块发布到 UltiCloud 市场。开发者可以使用它进行身份验证、初始化项目元数据、上传 JAR 文件,以及管理已发布的模块和版本。
本页对应 @ultikits/cli 2.0.1。1.x 版本无法连接 UltiCloud,请用 npm install -g @ultikits/cli@latest 升级并重新运行 ultikits login。
推荐:本地发布
官方 UltiTools 模块由维护者在本地发布:把验收通过的 JAR 附加到 GitHub Release,再用 CLI 将同一个文件发布到 UltiCloud。第三方开发者也可以使用相同的路径。 GitHub Actions 是可选的手动工作流,不是官方模块的发布路径。
安装
npm install -g @ultikits/cli验证安装:
ultikits --version环境要求
- Node.js 18 及以上
- npm 或其他 Node 包管理器
身份验证
在发布之前,需要先通过 UltiCloud 身份验证。CLI 支持两种方式。
浏览器登录(设备授权)
ultikits loginCLI 会显示用户码和确认链接并打开浏览器。在浏览器中确认用户码后,CLI 会自动轮询并完成登录。
CLI 凭据保存在 ~/.ultikits/credentials.json。
开发者密钥
如果你需要非交互式登录(例如在远程服务器上),可以使用开发者密钥:
- 访问 panel.ultikits.com/developer
- 复制你的开发者密钥(
ucd_…) - 运行
ultikits login --token并在提示时粘贴密钥,或直接运行ultikits login --token ucd_… - CLI 会先向 UltiCloud 验证密钥,再保存
环境变量
在 CI/CD 流水线中,设置 ULTIKITS_TOKEN 环境变量即可,无需运行 ultikits login:
export ULTIKITS_TOKEN=ucd_...该值必须是开发者密钥(ucd_…)或 CLI 凭据(ucc_…),旧版 Access Token 会被拒绝。设置后它优先于已保存的凭据。--api-url 或 ULTIKITS_API_URL 可选择其他 UltiCloud API(仅限 https),默认是 https://api.ultikits.com。
其他验证命令
ultikits whoami # 查看当前身份验证状态
ultikits logout # 删除已保存的凭据创建新模块
创建新 UltiTools 模块最快的方式是使用 ultikits create:
ultikits create命令会依次提示你输入:
| 提示 | 默认值 | 说明 |
|---|---|---|
| Module name | — | 模块名称,PascalCase 格式(如 UltiHome、MyPlugin) |
| Package name | 由名称自动推导 | Java 包名(如 com.ultikits.plugins.home) |
| Description | — | 模块简短描述 |
| Author | git config user.name | 作者名 |
| API version | 6.2.2 | UltiTools-API 版本。该默认值落后于当前正式版,建议手动改为最新版。 |
CLI 2.0.1 生成以下项目结构(模板自 1.2.0 起未变):
UltiHome/
├── pom.xml # Maven 构建,包含 UltiTools-API、测试依赖、JaCoCo
├── README.md # 中英双语 README 模板
├── .gitignore
├── src/main/java/.../UltiHome.java # 带 @UltiToolsModule 的主类
├── src/main/resources/
│ ├── plugin.yml # 插件描述文件
│ └── lang/
│ ├── en.yml # 英文语言文件
│ └── zh.yml # 中文语言文件
└── src/test/java/.../ # 测试目录(空,等待编写测试)用 UltiTools-API 6.3.0 编译之前,需要迁移生成的模板:
- 选择 API 版本
6.3.0,并把src/main/resources/plugin.yml的api-version设为630;模板仍会写入620。 - 删除主类中空的
unregisterSelf()和reloadSelf()覆写。这两个方法在 6.3.0 中为 final;如需清理或重载逻辑,分别放进protected void onUnregister()和protected void onReload()。
然后编译迁移后的项目:
cd UltiHome
mvn compile也可以指定目标目录:
ultikits create my-plugin-dir框架通过 scanBasePackages 发现带注解的类
创建完成后,在 commands/ 包中添加命令类,service/ 包中添加服务类,config/ 包中添加配置类。框架通过 @UltiToolsModule(scanBasePackages) 自动发现带注解的类。
项目配置
初始化项目配置
在模块根目录下运行:
ultikits init通过交互式提示创建 ultikits.json 文件:
{
"identifyString": "myplugin",
"name": "MyPlugin",
"shortDescription": "插件简介。",
"categoryId": 1
}| 字段 | 说明 |
|---|---|
identifyString | 模块唯一标识符。3-64 个字符,小写字母、数字、. 和 _,必须以字母开头。 |
name | 在市场中显示的名称。 |
shortDescription | 简短描述,最多 200 个字符。 |
categoryId | 模块分类(见下表)。 |
分类
| ID | 分类 |
|---|---|
| 1 | 实用工具 (Utilities) |
| 2 | 经济 (Economy) |
| 3 | 世界管理 (World Management) |
| 4 | 社交聊天 (Social & Chat) |
| 5 | 权限安全 (Permissions & Security) |
| 6 | 游戏玩法 (Gameplay) |
发布
基本用法
先构建并测试模块,保留准备发布的 JAR。把这个文件附加到 GitHub Release,再用 CLI 发布相同字节的文件,不要在两次上传之间重新构建:
mvn clean verify
ultikits publish target/MyPlugin-1.2.0.jar \
--module-version 1.2.0 \
--changelog "CHANGELOG.md 中 1.2.0 节的正文"未指定 JAR 路径时,CLI 在 ./target/ 中查找文件(跳过 -sources.jar 和 -javadoc.jar)。选定 JAR 后,CLI 自动完成以下操作:
- 使用这个文件上传
- 从 JAR 中读取
plugin.yml获取名称和版本 - 读取
ultikits.json获取标识符、描述和分类 - 如果模块在 UltiCloud 上不存在则自动创建,这需要 manage 凭据(
ultikits login,或轮换为管理作用域的开发者密钥) - 创建新版本并上传 JAR
带参数发布
显式指定 JAR 路径,并通过命令行参数覆盖元数据。建议把已发布版本在 CHANGELOG.md 中对应一节的正文作为一个完整的 --changelog 参数,不要传整份文件或通用占位说明。下面的示例文本应替换为该节正文:
ultikits publish target/MyPlugin-1.2.0.jar \
--module-version 1.2.0 \
--changelog "CHANGELOG.md 中 1.2.0 节的正文"可用参数:
| 参数 | 说明 |
|---|---|
--id <identifyString> | 覆盖模块标识符 |
--name <name> | 显示名称,只在创建模块时使用 |
--module-version <version> | 覆盖版本号 |
--changelog <text> | 版本更新日志 |
--short-description <text> | 简短描述,只在创建模块时使用 |
--api-url <url> | UltiCloud API 地址(默认 ULTIKITS_API_URL、登录时保存的地址或 https://api.ultikits.com) |
-y, --yes | 跳过所有确认提示 |
非交互模式
在 CI/CD 中使用 --yes 跳过提示,配合 ULTIKITS_TOKEN 进行身份验证:
ULTIKITS_TOKEN=ucd_... ultikits publish --yes \
--module-version "1.2.0" \
--changelog "CHANGELOG.md 中 1.2.0 节的正文"元数据优先级
CLI 从多个来源解析元数据,优先级如下:
CLI 参数 > ultikits.json > JAR 中的 plugin.yml
例如,如果你传入 --module-version 1.2.0,它会覆盖 plugin.yml 中的版本号。
上传失败与重试
文件上传从未完成的版本不算已发布,不会出现在公开列表中,也不会被选为最新版本。上传失败或中断后,用同一个 JAR 再次执行相同的 ultikits publish 命令:CLI 会为待上传的版本重新申请上传票据,旧票据随之失效。待上传版本保留首次声明的大小与 SHA-256,要上传不同的文件,请在开发者中心删除该待上传版本,或发布更高的版本号。新版本号必须大于该模块已有的所有版本(包括仍在等待上传的版本)。
已发布版本不可覆盖。修改文件后应发布新版本。如果网络故障导致结果不明,先运行 ultikits versions list:列出的版本已发布,不能通过重试替换。
模块的 API 级别
自 v6.3.0 起,UltiCloud 从你发布的 JAR 根目录的 plugin.yml 中读取 api-version,与该版本一起保存。没有任何命令行参数或 ultikits.json 字段可以设置它,UltiCloud 只从 JAR 中读取。请以整数形式声明:
api-version: 630这个数字决定哪些服务器会被提供这个版本。运行 UltiTools-API 6.3.0 及以后的服务器只会被提供从 630 到其自身 API 级别的版本;运行更早框架的服务器只会被提供低于 630 的版本。见模块版本规范。
根目录 plugin.yml 没有声明整数 api-version 的 JAR 会在上传时被拒绝:版本不会发布,仍以已声明的文件处于待上传状态。见因 API 级别被拒绝上传。
模块管理
列出模块
ultikits modules list输出:
myplugin — MyPlugin (1.0.0)
anotherplugin — AnotherPlugin (无已发布版本 / no versions)
共 2 个模块 / 2 module(s)模块详情
ultikits modules info myplugin输出:
名称 / Name: MyPlugin
标识符 / Identifier: myplugin
简介 / Summary: 插件简介
分类 / Category: Utilities (#1)
最新版本 / Latest: 1.0.0
下载 / Downloads: 42
更新 / Updated: 2026-11-07T12:00:00.000Z删除模块
ultikits modules delete myplugin模块会立即消失
模块会立即从目录和下载中消失。30 天内可用 ultikits modules restore myplugin 恢复(网页面板没有恢复功能),之后永久清除。
CLI 会要求你再输入一次标识符。使用 -y 或没有终端时需加 --confirm myplugin。
其他模块命令
以下命令需要 manage 凭据(ultikits login,或轮换为管理作用域的开发者密钥):
ultikits modules create --id myplugin --name MyPlugin # 默认值取自 ultikits.json
ultikits modules update myplugin --short-description "新的简介"
ultikits modules restore myplugin版本管理
列出版本
ultikits versions list myplugin该命令从公开目录列出所有已发布和已撤回的版本及其大小和发布时间;不含待上传版本,也不发送凭据。
要核对上传的文件,读取该版本的元数据,将返回的 sha256 与 sizeBytes 与保留的 JAR 比对:
curl --fail-with-body 'https://api.ultikits.com/v1/catalogue/modules/myplugin/versions/1.2.0'撤回版本
ultikits versions withdraw myplugin 1.2.0该命令撤回已发布版本,需要 manage 凭据,且不可恢复:该版本号及更低版本不能再发布;已下载的副本不受影响。使用 -y 或没有终端时需加 --confirm [email protected]。versions delete 是同一命令的旧名称。
CI/CD — GitHub Actions
推荐使用本地发布。如果项目需要通过 CI 发布,可以使用下面的可选方案,仅在维护者手动启动时运行。推送、合并或创建 GitHub Release 都不会触发它。在模块仓库中创建 .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/[email protected] --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)"提取器接受唯一的 ## [1.2.0]、## [v1.2.0] 或 ## 1.2.0 标题,后面可以带日期。它保留子标题和围栏代码,到下一个二级标题或文件末尾结束;版本无效、节缺失、重复或正文为空时,在构建和发布之前拒绝。检查所选 ref,确保更新说明描述的是准备构建的字节。
配置步骤
- 添加工作流文件,把两处精确 JAR 路径中的
MyPlugin改为自己构建产物的最终名称。不要用通配符,以免上传源码包或未打包依赖的 JAR。 - 把开发者密钥(
ucd_…)设置为仓库的ULTIKITS_TOKENSecret,不要写进工作流正文或手动输入参数。上传作用域的密钥无法创建不存在的模块,请先创建模块。 - 保持 CLI 版本固定,检查版本变更后再更新这个固定值。
推送即发布
不要加入 push 或合并触发器,否则合并版本号改动就会自动发布。把工作流输入直接插入 run: 也可能造成 shell 注入;应像示例一样通过环境变量传递,并对变量展开放置引号。
发布新版本
- 检查源码和该版本的
CHANGELOG.md一节。 - 打开 Actions > Publish to UltiCloud > Run workflow,选择准备发布的分支或标签。
- 只输入模块版本。工作流会从检出的
CHANGELOG.md读取该版本完整节的正文,不使用多行表单输入。 - 明确启动工作流,然后核对上传的版本与文件。这个可选方案只构建一次并发布到 UltiCloud,不会创建 GitHub Release。如需在 GitHub 发布,从工作流运行页下载
module-<version>产物,解压出其中的 JAR 并附加该文件,不要重新构建。
常见问题
"Not logged in" 错误
未登录:请运行 ultikits login,或设置环境变量 ULTIKITS_TOKEN。运行 ultikits login 进行交互式验证,或设置 ULTIKITS_TOKEN 环境变量。CLI 1.x 写入的凭据文件会得到 凭据文件是旧版格式,对新接口无效,请重新 ultikits login。,请重新运行 ultikits login。
"No JAR file specified and none found in ./target/"
CLI 默认在 ./target/ 中查找 .jar 文件。确保先构建项目:
mvn clean package -DskipTests缺少必填字段
Missing required fields:
- identifyString (use --id or ultikits.json)使用 ultikits init 创建 ultikits.json,或通过 CLI 参数传入缺少的字段。
发布时凭据无效
[invalid_credential] 表示凭据错误或已过期,[role_required] 表示该账号还不是开发者。确认仓库上的 ULTIKITS_TOKEN Secret 设置正确:
gh secret list --repo YourOrg/YourPlugin该 Secret 必须是开发者密钥(ucd_…)。[key_scope_insufficient] 表示模块不存在,而上传作用域的密钥无法创建它。
因 API 级别被拒绝上传
ultikits publish 会报以下两种代码之一:
JAR 里的 plugin.yml 需要整数 api-version:补上后重新构建再发布。 [upload_api_level_missing]
上传的文件不是有效的 ZIP/JAR:检查构建产物路径。 [upload_jar_invalid]upload_api_level_missing 涵盖:根目录没有 plugin.yml 或 api-version、值为空、声明了两次,以及 '1.19' 这样不是整数的 Bukkit 风格值;消息不会指出具体是哪一种。upload_jar_invalid 表示文件不是可读的 ZIP/JAR(例如不完整的 JAR)。按模块的 API 级别声明级别并重新构建。被拒绝的版本仍以旧文件的校验和处于待上传状态:请在开发者中心删除该待上传版本后重新发布,或改用更高的版本号。